dsh-moli 1.1.16 → 1.1.18
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 +36 -42
- package/README.zh.md +31 -41
- package/index.js +27 -5
- package/lib/moli.js +225 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -209,7 +209,7 @@ An unresolvable bundle is **skipped silently** apart from one stderr line
|
|
|
209
209
|
|
|
210
210
|
## Usage
|
|
211
211
|
|
|
212
|
-
`moli_fetch` parameters: `url` (required), `dump`, `writeTo`, `eval`, `scroll`, `wait`,
|
|
212
|
+
`moli_fetch` parameters: `url` (required), `dump`, `writeTo`, `selector`, `eval`, `scroll`, `wait`,
|
|
213
213
|
`waitSelector`, `waitScript`, `waitResponseUrl`, `waitResponseJson`, `delayMs`, `timeoutMs`,
|
|
214
214
|
`maxChars`, `headers`, `userAgent`, `layout`, `scrollbars`, `noJs`, `noCss`, `resources`,
|
|
215
215
|
`withFrames`, `insecureTls`, `failOnHttpError`, `profileDir`, `cookieFile`, `proxy`.
|
|
@@ -218,6 +218,7 @@ An unresolvable bundle is **skipped silently** apart from one stderr line
|
|
|
218
218
|
moli_fetch url=https://example.com dump=markdown # token-efficient text
|
|
219
219
|
moli_fetch url=https://example.com dump=semantic_tree_text # nodes with backend ids
|
|
220
220
|
moli_fetch url=https://example.com eval="document.title" # one computed value
|
|
221
|
+
moli_fetch url=https://example.com selector=".video-card" # one container, not the page
|
|
221
222
|
moli_fetch url=https://example.com dump=screenshot writeTo=/abs/path/a.png
|
|
222
223
|
moli_fetch url=https://example.com dump=pdf writeTo=/abs/path/a.pdf
|
|
223
224
|
moli_fetch url=https://example.com dump=markdown scroll=800 # trigger lazy loading first
|
|
@@ -225,6 +226,35 @@ moli_fetch url=https://example.com dump=markdown scroll=800 # trigger lazy loa
|
|
|
225
226
|
|
|
226
227
|
Screenshot and PDF dumps need `writeTo`; the bytes are written verbatim rather than printed.
|
|
227
228
|
|
|
229
|
+
### `selector` — read one container, not the page
|
|
230
|
+
|
|
231
|
+
A listing page is mostly noise if you only want the items. `selector` returns the text
|
|
232
|
+
of the elements a CSS selector matches, which on a node-dense feed measured **1,829
|
|
233
|
+
characters against 95,605** for the same page as Markdown.
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
moli_fetch url=https://example.com/feed selector=".video-card"
|
|
237
|
+
moli_fetch url=https://example.com/feed selector="h2" scroll=2000 # scroll first, then read
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
All matches are joined with newlines and the result reports how many there were:
|
|
241
|
+
"matched 48 element(s)" is the difference between a complete read and a partial one,
|
|
242
|
+
and it is the only signal you get that the selector was too broad or too narrow.
|
|
243
|
+
|
|
244
|
+
Two cases are deliberately errors rather than empty answers, because the engine
|
|
245
|
+
cannot tell an empty selection from a page with no text — an expression returning
|
|
246
|
+
`""` exits 0 with a blank line:
|
|
247
|
+
|
|
248
|
+
- **nothing matched** — `ok: false`, "no element matches". A selector that is not on
|
|
249
|
+
the page is almost always a spelling mistake or a page that has not loaded yet.
|
|
250
|
+
- **matched, but no text** — `ok: false`, "match but none of them has any text", which
|
|
251
|
+
is what an icon or an image container gives you. Use `eval` if you need attributes.
|
|
252
|
+
|
|
253
|
+
`selector` cannot be combined with `dump` or `eval`: a selector returns one
|
|
254
|
+
container's text, a dump returns the whole page, and only one expression value can be
|
|
255
|
+
returned — so one of them could only be silently ignored, which is the failure mode
|
|
256
|
+
this plugin keeps having to fix.
|
|
257
|
+
|
|
228
258
|
### `scroll` — reach what only appears on a scroll
|
|
229
259
|
|
|
230
260
|
The engine serializes the whole document, so a plain fetch already contains everything the DOM holds.
|
|
@@ -489,6 +519,11 @@ Eight suites live in [`verify/`](verify/), and all are runnable against a workin
|
|
|
489
519
|
`bottom`, nonsense rejected, the caller's `eval` value preserved) and then, when an engine is
|
|
490
520
|
present, fetches a page whose `scroll` handler fills a slot and asserts the content appears with
|
|
491
521
|
`scroll` and the placeholder appears without it — a property of the output, not of an internal call.
|
|
522
|
+
- `selector-verify.mjs` covers the two silent-wrong-answer cases a container read invites: a selector
|
|
523
|
+
matching nothing is an error rather than empty content, and a selector matching several elements
|
|
524
|
+
reports the count and joins them. It also asserts that the selector is quoted as data rather than
|
|
525
|
+
spliced into the expression, that `selector` with `dump` or `eval` is refused instead of one of them
|
|
526
|
+
being ignored, and that it composes with `scroll`.
|
|
492
527
|
- `http-verify.mjs` asserts the parameter-to-flag mapping directly, because a parameter that never
|
|
493
528
|
reaches the engine is indistinguishable from a feature that does not work. It then fetches a 404 and
|
|
494
529
|
checks that `failOnHttpError` reports it while keeping the body, that a healthy page is unaffected,
|
|
@@ -542,47 +577,6 @@ On the versions themselves: npm allows a version to be unpublished only briefly
|
|
|
542
577
|
individually, so 1.1.11 and 1.1.12 remain installable but are marked deprecated and point here.
|
|
543
578
|
`npm install dsh-moli` resolves to 1.1.13.
|
|
544
579
|
|
|
545
|
-
## Releasing
|
|
546
|
-
|
|
547
|
-
Publishing goes through GitHub Actions' OIDC identity, so **no npm token exists to expire**:
|
|
548
|
-
|
|
549
|
-
```bash
|
|
550
|
-
npm version patch # or minor / major
|
|
551
|
-
git push && git tag v1.1.6 && git push origin v1.1.6
|
|
552
|
-
```
|
|
553
|
-
|
|
554
|
-
`.github/workflows/publish.yml` checks that the tag matches `package.json`, runs the offline suites,
|
|
555
|
-
and publishes. One-time setup on npmjs.com: **package → Settings → Trusted Publisher → GitHub
|
|
556
|
-
Actions**, with user `27painter`, repository `dsh-moli`, workflow filename `publish.yml`, and the
|
|
557
|
-
`npm publish` action allowed.
|
|
558
|
-
|
|
559
|
-
**Do not publish with a stored token.** This machine was configured with a classic token that bypasses
|
|
560
|
-
2FA, and npm is retiring those; when it was revoked, the publish failed with
|
|
561
|
-
|
|
562
|
-
```
|
|
563
|
-
E404 Not Found - PUT https://registry.npmjs.org/dsh-moli
|
|
564
|
-
The requested resource 'dsh-moli@1.1.5' could not be found or you do not have permission
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
which reads as a missing package and is actually an authorisation failure. Run
|
|
568
|
-
`node scripts/preflight-publish.mjs` before any local publish: it reports whether the credential can
|
|
569
|
-
authenticate and names that misleading 404 when it cannot.
|
|
570
|
-
|
|
571
|
-
### Deprecating a version: do it on the website
|
|
572
|
-
|
|
573
|
-
Publishing is scripted; deprecating deliberately is not. Use the package's page on npmjs.com —
|
|
574
|
-
**package → Settings → Deprecate** — and name the versions and the reason.
|
|
575
|
-
|
|
576
|
-
`npm deprecate dsh-moli@1.1.11 "…"` produces the same misleading failure as above when the local
|
|
577
|
-
credential is dead, and there is a second reason not to reach for a token here. npm restricted
|
|
578
|
-
2FA-bypass granular tokens from package-management operations on 31 July 2026, so the kind of token
|
|
579
|
-
that used to make `deprecate` scriptable is being withdrawn, while `publish` has a supported
|
|
580
|
-
token-free path through OIDC. A release process that needs a token only for the rare act of
|
|
581
|
-
deprecating is a process that will be broken the next time it is needed, which is exactly what
|
|
582
|
-
happened with 1.1.11 and 1.1.12.
|
|
583
|
-
|
|
584
|
-
The 1.0.0 and 1.0.1 deprecations were done this way, from the website.
|
|
585
|
-
|
|
586
580
|
## License
|
|
587
581
|
|
|
588
582
|
MIT. See [LICENSE](LICENSE).
|
package/README.zh.md
CHANGED
|
@@ -195,15 +195,16 @@ profile 插件需要两半都齐:依赖边(`dependencies`)+ `dsh.profile.b
|
|
|
195
195
|
|
|
196
196
|
## 用法
|
|
197
197
|
|
|
198
|
-
`moli_fetch` 参数:`url`(必填)、`dump`、`writeTo`、`eval`、`scroll`、`wait
|
|
199
|
-
`waitScript`、`waitResponseUrl`、`waitResponseJson`、`delayMs`、`timeoutMs
|
|
200
|
-
`userAgent`、`layout`、`scrollbars`、`noJs`、`noCss`、`resources
|
|
201
|
-
`failOnHttpError`、`profileDir`、`cookieFile`、`proxy`。
|
|
198
|
+
`moli_fetch` 参数:`url`(必填)、`dump`、`writeTo`、`selector`、`eval`、`scroll`、`wait`、
|
|
199
|
+
`waitSelector`、`waitScript`、`waitResponseUrl`、`waitResponseJson`、`delayMs`、`timeoutMs`、
|
|
200
|
+
`maxChars`、`headers`、`userAgent`、`layout`、`scrollbars`、`noJs`、`noCss`、`resources`、
|
|
201
|
+
`withFrames`、`insecureTls`、`failOnHttpError`、`profileDir`、`cookieFile`、`proxy`。
|
|
202
202
|
|
|
203
203
|
```
|
|
204
204
|
moli_fetch url=https://example.com dump=markdown # 省 token 的正文
|
|
205
205
|
moli_fetch url=https://example.com dump=semantic_tree_text # 带节点 id 的结构
|
|
206
206
|
moli_fetch url=https://example.com eval="document.title" # 取一个值
|
|
207
|
+
moli_fetch url=https://example.com selector=".video-card" # 只取一个容器,不要整页
|
|
207
208
|
moli_fetch url=https://example.com dump=screenshot writeTo=/绝对路径/a.png
|
|
208
209
|
moli_fetch url=https://example.com dump=pdf writeTo=/绝对路径/a.pdf
|
|
209
210
|
moli_fetch url=https://example.com dump=markdown scroll=800 # 先触发懒加载再抓取
|
|
@@ -211,6 +212,29 @@ moli_fetch url=https://example.com dump=markdown scroll=800 # 先触发懒加
|
|
|
211
212
|
|
|
212
213
|
截图与 PDF 必须给 `writeTo`;字节是原样落盘的,不会打印到输出里。
|
|
213
214
|
|
|
215
|
+
### `selector` —— 只取一个容器,不要整页
|
|
216
|
+
|
|
217
|
+
列表页里如果只要条目,正文大部分都是噪声。`selector` 返回 CSS 选择器匹配到的元素文本——在一个节点密集的
|
|
218
|
+
信息流上实测**1,829 字符,而同一页面走 markdown 是 95,605**。
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
moli_fetch url=https://example.com/feed selector=".video-card"
|
|
222
|
+
moli_fetch url=https://example.com/feed selector="h2" scroll=2000 # 先滚再取
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
所有匹配项以换行拼接,并且结果会报告匹配到多少个:**"matched 48 element(s)"** 是"读全了"和"只读到一部分"
|
|
226
|
+
的区别,也是你判断选择器太宽或太窄的唯一信号。
|
|
227
|
+
|
|
228
|
+
两种情况**刻意做成错误而不是空答案**,因为引擎分不清"没匹配到"和"页面没有文本"——表达式返回 `""` 时是
|
|
229
|
+
exit 0 加一个空行:
|
|
230
|
+
|
|
231
|
+
- **没有任何匹配** —— `ok: false`,"no element matches"。页面上不存在的选择器几乎总是拼写错误或页面还没加载。
|
|
232
|
+
- **匹配到了但没有文本** —— `ok: false`,说明匹配到了但都没文本,这通常会发生在图标或图片容器上。需要属性就
|
|
233
|
+
用 `eval`。
|
|
234
|
+
|
|
235
|
+
`selector` 不能与 `dump` 或 `eval` 同传:选择器返回一个容器的文本、dump 返回整页、而一次只能返回一个表达式
|
|
236
|
+
值——所以其中一个只能被**静默忽略**,而这正是这个插件一路在修的那类失败。
|
|
237
|
+
|
|
214
238
|
### `scroll` —— 拿到"只有滚动后才出现"的内容
|
|
215
239
|
|
|
216
240
|
引擎会序列化整个文档,所以普通抓取**已经包含 DOM 里的一切**。它拿不到的是**尚未存在的内容**:首次
|
|
@@ -444,6 +468,9 @@ profile。无论生效的是哪个代理,loopback 地址一律绕过它。
|
|
|
444
468
|
- `scroll-verify.mjs` 在无引擎时校验表达式构造(像素值、负数、`top`、`bottom`、无效输入被拒、调用方的 `eval`
|
|
445
469
|
值被保留),有引擎时抓取一个"滚动处理器填充槽位"的页面,断言带 `scroll` 时内容出现、不带时只有占位符——
|
|
446
470
|
断言的是**输出**的性质,而不是内部调用。
|
|
471
|
+
- `selector-verify.mjs` 覆盖"按容器取值"最容易出现的两个静默错答案:选择器无匹配时报错而不是空内容、匹配多个
|
|
472
|
+
元素时报告数量并拼接。它还断言选择器是**作为数据被引号包裹**而不是拼进表达式、`selector` 与 `dump`/`eval`
|
|
473
|
+
同传会被拒绝(而不是静默忽略其中一个)、以及它能与 `scroll` 组合。
|
|
447
474
|
- `http-verify.mjs` 直接断言"参数 → 引擎选项"的映射,因为**参数没送到引擎和功能不生效,从外面看是一样的**。
|
|
448
475
|
随后抓一个 404,验证 `failOnHttpError` 会报告失败同时保留响应体、健康页面不受影响、以及错误页的截图仍然被
|
|
449
476
|
写为合法 PNG 而不是被当文本解码。它还覆盖那两个"静默给错答案"的情形:`eval` 配 `dump` 会带
|
|
@@ -494,43 +521,6 @@ Chromium 后端、原始 CDP 逃生舱、按调用的 CDP 端点、持久化浏
|
|
|
494
521
|
关于这两个版本本身:npm 只允许在很短的时间窗口内撤回发布,且无法单独撤回某一个版本,因此 1.1.11 与
|
|
495
522
|
1.1.12 仍然可安装,但已被标记为废弃并指向这里。`npm install dsh-moli` 会安装到 1.1.13。
|
|
496
523
|
|
|
497
|
-
## 发布
|
|
498
|
-
|
|
499
|
-
发布走 GitHub Actions 的 OIDC 身份,**本机不存任何 npm token,因此没有会失效的凭据**:
|
|
500
|
-
|
|
501
|
-
```bash
|
|
502
|
-
npm version patch # 或 minor / major
|
|
503
|
-
git push && git tag v1.1.6 && git push origin v1.1.6
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
`.github/workflows/publish.yml` 会先校验 tag 与 `package.json` 版本一致、跑离线测试套件,然后发布。
|
|
507
|
-
npmjs.com 上需要一次性配置:**package → Settings → Trusted Publisher → GitHub Actions**,用户
|
|
508
|
-
`27painter`、仓库 `dsh-moli`、workflow 文件名 `publish.yml`,并允许 `npm publish` 动作。
|
|
509
|
-
|
|
510
|
-
**不要再用本机存储的 token 发布。** 这台机器原先配的是一个绕过 2FA 的 classic token,npm 正在淘汰这类
|
|
511
|
-
token;它被吊销后,发布会报:
|
|
512
|
-
|
|
513
|
-
```
|
|
514
|
-
E404 Not Found - PUT https://registry.npmjs.org/dsh-moli
|
|
515
|
-
The requested resource 'dsh-moli@1.1.5' could not be found or you do not have permission
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
**这看起来像"包不存在",实际是认证失败。** 任何本地发布之前先跑
|
|
519
|
-
`node scripts/preflight-publish.mjs`:它会告诉你凭据能否认证,不能时明确点出这个误导性的 404。
|
|
520
|
-
|
|
521
|
-
### 废弃某个版本:去网站操作,别用命令行
|
|
522
|
-
|
|
523
|
-
发布是脚本化的,**废弃则刻意不做成脚本**。用 npmjs.com 上的包页面——**package → Settings →
|
|
524
|
-
Deprecate**——选版本、写理由。
|
|
525
|
-
|
|
526
|
-
`npm deprecate dsh-moli@1.1.11 "…"` 在本机凭据失效时会给出上面那个同样误导的失败,而且这里还有第二个
|
|
527
|
-
理由不要依赖 token:npm 在 **2026 年 7 月 31 日**起禁止 2FA-bypass 类型的 granular token 执行**包管理**
|
|
528
|
-
操作,也就是说"以前能让 `deprecate` 脚本化"的那类凭据正在被取消;而 `publish` 有受支持的、免 token 的
|
|
529
|
-
OIDC 路径。**一个只在"偶尔废弃版本"时才需要 token 的流程,注定在下次需要它时是坏的**——1.1.11 与 1.1.12
|
|
530
|
-
遇到的正是这种情况。
|
|
531
|
-
|
|
532
|
-
1.0.0 与 1.0.1 的废弃就是通过网站完成的。
|
|
533
|
-
|
|
534
524
|
## 许可
|
|
535
525
|
|
|
536
526
|
MIT,见 [LICENSE](LICENSE)。
|
package/index.js
CHANGED
|
@@ -94,12 +94,16 @@ const FETCH_PARAMETERS = {
|
|
|
94
94
|
dump: {
|
|
95
95
|
type: 'string',
|
|
96
96
|
enum: ['html', 'markdown', 'json', 'semantic_tree', 'semantic_tree_text', 'screenshot', 'screenshot_full', 'pdf'],
|
|
97
|
-
description: 'Output format. Omit for rendered HTML. `markdown`
|
|
97
|
+
description: 'Output format. Omit for rendered HTML. `markdown` is the cheapest way to read a page. `semantic_tree_text` is for locating things by structure (role, name, backend id), not for saving tokens: its size follows the page\'s node density and it is often larger than markdown — measured at 1.8x on a node-dense page — so prefer it only when you need the structure. `json` carries status and final_url; screenshot/pdf need writeTo.',
|
|
98
98
|
},
|
|
99
99
|
writeTo: {
|
|
100
100
|
type: 'string',
|
|
101
101
|
description: 'Absolute path to save the output. Required for screenshot/pdf; optional for text dumps larger than maxChars.',
|
|
102
102
|
},
|
|
103
|
+
selector: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'Return the text of the elements this CSS selector matches instead of the whole page. This is the cheap way to read one container out of a noisy listing page: the cards on a feed measured 1,829 characters against 95,605 for the same page as markdown. All matches are joined with newlines and the result reports how many there were; matching nothing is an error, not empty content, because the engine cannot distinguish an empty selection from a page with no text. Not compatible with `dump` or `eval` — one of them could only be ignored.',
|
|
106
|
+
},
|
|
103
107
|
eval: {
|
|
104
108
|
type: 'string',
|
|
105
109
|
description: 'JavaScript expression evaluated once the page is ready; its value is returned as text or JSON instead of the page. Combining it with `dump` does NOT return both: the engine discards the expression value and dumps the resulting page state instead (its own documented semantics). Use `eval` alone to read a value, or `dump` alone to read the page.',
|
|
@@ -175,6 +179,8 @@ const FETCH_OUTPUT_SCHEMA = {
|
|
|
175
179
|
omitted: { type: 'integer' },
|
|
176
180
|
evalFailed: { type: 'boolean' },
|
|
177
181
|
evalDiscarded: { type: 'boolean' },
|
|
182
|
+
selector: { type: 'string' },
|
|
183
|
+
matched: { type: 'integer' },
|
|
178
184
|
truncated: { type: 'boolean' },
|
|
179
185
|
error: { type: 'string' },
|
|
180
186
|
hint: { type: 'string' },
|
|
@@ -183,6 +189,7 @@ const FETCH_OUTPUT_SCHEMA = {
|
|
|
183
189
|
sandboxTlsBlock: { type: 'boolean' },
|
|
184
190
|
unreachable: { type: 'boolean' },
|
|
185
191
|
possiblyUnreachable: { type: 'boolean' },
|
|
192
|
+
conditionTimedOut: { type: 'boolean' },
|
|
186
193
|
note: { type: 'string' },
|
|
187
194
|
},
|
|
188
195
|
};
|
|
@@ -226,14 +233,23 @@ function renderFetch(_args, value) {
|
|
|
226
233
|
if (value.sandboxTlsBlock === true) lines.push('Cause: the DSH sandbox blocks Schannel client credentials (HTTPS only).');
|
|
227
234
|
if (value.unreachable === true) lines.push('Cause: the host did not answer; a local forward proxy is the usual fix on this network.');
|
|
228
235
|
else if (value.possiblyUnreachable === true) lines.push('Cause: the host did not answer in time; it may be blocked, or the page may simply be slow.');
|
|
236
|
+
// A wait that never resolved is the one failure that is definitely not the
|
|
237
|
+
// network: the page arrived and was parsed. Saying so up front keeps the caller
|
|
238
|
+
// from spending their time on a proxy that was never the problem.
|
|
239
|
+
else if (value.conditionTimedOut === true) lines.push('Cause: the page loaded, but the wait condition never became true — this is not a network problem.');
|
|
229
240
|
if (typeof value.hint === 'string' && value.hint !== '') lines.push(`Next: ${value.hint}`);
|
|
230
|
-
|
|
241
|
+
// An HTTP error keeps its body under `failOnHttpError`. Reporting the failure
|
|
231
242
|
// without the body would throw away the error page, which is the part worth
|
|
232
|
-
// reading -- and for a binary dump the body is the file that was written.
|
|
233
|
-
|
|
243
|
+
// reading -- and for a binary dump the body is the file that was written. A
|
|
244
|
+
// selector failure is different: there is no server body to show, and the
|
|
245
|
+
// message already says what did not match.
|
|
246
|
+
if (value.httpError === true && value.kind === 'binary' && typeof value.path === 'string') {
|
|
234
247
|
lines.push(`The error page was still written to ${value.path} (${String(value.bytes)} bytes).`);
|
|
235
|
-
} else if (typeof value.content === 'string' && value.content !== '') {
|
|
248
|
+
} else if (value.httpError === true && typeof value.content === 'string' && value.content !== '') {
|
|
236
249
|
lines.push('', 'The server\'s response body:', '', value.content);
|
|
250
|
+
} else if (typeof value.selector === 'string') {
|
|
251
|
+
lines.push(`Selector: ${JSON.stringify(value.selector)}`
|
|
252
|
+
+ (typeof value.matched === 'number' ? ` (matched ${String(value.matched)})` : ''));
|
|
237
253
|
}
|
|
238
254
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
239
255
|
}
|
|
@@ -250,6 +266,12 @@ function renderFetch(_args, value) {
|
|
|
250
266
|
if (value.evalDiscarded === true) {
|
|
251
267
|
parts.push('Note: the `eval` expression was dropped because `dump` was given too — the engine discards the expression value in that combination. What follows is the page, not the expression.');
|
|
252
268
|
}
|
|
269
|
+
// A selector answer is a list, so say how many elements produced it: "48 matched,
|
|
270
|
+
// 34 had text" is the difference between a complete read and a partial one, and it
|
|
271
|
+
// is the only signal the caller has that the selector was too broad or too narrow.
|
|
272
|
+
if (typeof value.selector === 'string' && typeof value.matched === 'number') {
|
|
273
|
+
parts.push(`Selector ${JSON.stringify(value.selector)} matched ${String(value.matched)} element(s).`);
|
|
274
|
+
}
|
|
253
275
|
if (typeof value.note === 'string' && value.note !== '') parts.push(value.note);
|
|
254
276
|
parts.push(String(value.content ?? ''));
|
|
255
277
|
return [{ type: 'text', text: parts.join('\n\n') }];
|
package/lib/moli.js
CHANGED
|
@@ -44,6 +44,44 @@ const SCROLL_TOKENS = {
|
|
|
44
44
|
bottom: 'document.documentElement.scrollHeight',
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Build the expression that returns the text of the elements a CSS selector matches.
|
|
49
|
+
*
|
|
50
|
+
* This exists because reading one container should not cost the whole page: on a
|
|
51
|
+
* node-dense listing page the full markdown dump measured 95,605 characters while
|
|
52
|
+
* the same page's video cards came to 1,829. `--eval` can already do this, but it
|
|
53
|
+
* requires writing the JavaScript; a selector is the same idea with no syntax to get
|
|
54
|
+
* wrong, which is what a model that is not fluent in the DOM needs.
|
|
55
|
+
*
|
|
56
|
+
* It works within one navigation -- the value is returned by the engine's own
|
|
57
|
+
* evaluation, so the "page is gone by the time the result is assembled" limitation
|
|
58
|
+
* does not apply.
|
|
59
|
+
*
|
|
60
|
+
* The result is prefixed with a machine-readable count, because the engine prints an
|
|
61
|
+
* empty string for an expression that yields `""`, and an empty selection is
|
|
62
|
+
* otherwise indistinguishable from a page with no text. A selector that matches
|
|
63
|
+
* nothing is reported as a failure rather than as empty content: the caller picked a
|
|
64
|
+
* selector that is not on the page, and telling them so is the whole point.
|
|
65
|
+
*
|
|
66
|
+
* @param selector - a CSS selector.
|
|
67
|
+
* @returns a JavaScript expression, or undefined when no selector was given.
|
|
68
|
+
*/
|
|
69
|
+
export function selectorScript(selector) {
|
|
70
|
+
if (typeof selector !== 'string' || selector.trim() === '') return undefined;
|
|
71
|
+
// JSON.stringify quotes the selector safely for embedding; a selector is data, not
|
|
72
|
+
// code, and must never be spliced into the expression raw.
|
|
73
|
+
const quoted = JSON.stringify(selector);
|
|
74
|
+
return `(() => {
|
|
75
|
+
const nodes = [...document.querySelectorAll(${quoted})];
|
|
76
|
+
const texts = nodes.map(node => (node.innerText ?? node.textContent ?? '').trim()).filter(text => text !== '');
|
|
77
|
+
return '[dsh-moli:selector]' + JSON.stringify({
|
|
78
|
+
matched: nodes.length,
|
|
79
|
+
withText: texts.length,
|
|
80
|
+
joined: texts.join('\\n'),
|
|
81
|
+
});
|
|
82
|
+
})()`;
|
|
83
|
+
}
|
|
84
|
+
|
|
47
85
|
/**
|
|
48
86
|
* Build the expression that scrolls the page, to be run by the engine's own
|
|
49
87
|
* `--eval` before the dump.
|
|
@@ -99,38 +137,52 @@ export function scrollScript(scroll) {
|
|
|
99
137
|
}
|
|
100
138
|
|
|
101
139
|
/**
|
|
102
|
-
*
|
|
140
|
+
* Build the one expression the engine will run from `scroll`, `selector` and `eval`.
|
|
103
141
|
*
|
|
104
|
-
* The
|
|
142
|
+
* The engine accepts a single `--eval`, so everything that has to happen before the
|
|
143
|
+
* page is captured is composed here, in the order the caller expects: scroll first,
|
|
144
|
+
* then read (a selector or the caller's expression).
|
|
105
145
|
*
|
|
106
|
-
* What gets returned depends on whether anything else will produce the output,
|
|
107
|
-
*
|
|
146
|
+
* What gets returned depends on whether anything else will produce the output, and
|
|
147
|
+
* getting this wrong breaks the feature in one direction or the other:
|
|
108
148
|
*
|
|
109
149
|
* - **A `dump` was requested.** The dump produces the output, so the expression is
|
|
110
|
-
* there only
|
|
150
|
+
* there only for its side effects and its value is discarded by the engine anyway.
|
|
111
151
|
* It must still be passed, or the scroll never runs.
|
|
112
|
-
* - **No `dump
|
|
113
|
-
*
|
|
114
|
-
* - **No `dump`, no caller expression
|
|
115
|
-
* evaluates to, so passing the scroll expression here
|
|
116
|
-
* the scroll's return value. `undefined` is returned
|
|
117
|
-
* default output -- the serialized document --
|
|
118
|
-
* shipped broken: `moli_fetch url=... scroll=600`
|
|
152
|
+
* - **No `dump`.** The expression's value *is* the output, whether it is a selector
|
|
153
|
+
* or the caller's own expression.
|
|
154
|
+
* - **No `dump`, no selector, no caller expression, only `scroll`.** The engine
|
|
155
|
+
* prints whatever `--eval` evaluates to, so passing the scroll expression here
|
|
156
|
+
* would replace the page with the scroll's return value. `undefined` is returned
|
|
157
|
+
* instead, leaving the engine's default output -- the serialized document --
|
|
158
|
+
* intact. This is the case that shipped broken: `moli_fetch url=... scroll=600`
|
|
159
|
+
* answered `600`.
|
|
119
160
|
*
|
|
120
161
|
* @param evalExpression - the caller's expression, if any.
|
|
121
162
|
* @param scroll - the caller's `scroll` value, if any.
|
|
122
163
|
* @param hasDump - whether the caller also asked for a dump.
|
|
164
|
+
* @param selector - the caller's CSS selector, if any.
|
|
123
165
|
* @returns the expression to pass, or undefined to let the engine decide.
|
|
124
166
|
*/
|
|
125
|
-
export function combineEval(evalExpression, scroll, hasDump = false) {
|
|
167
|
+
export function combineEval(evalExpression, scroll, hasDump = false, selector = undefined) {
|
|
126
168
|
const move = scrollScript(scroll);
|
|
169
|
+
const read = selectorScript(selector);
|
|
127
170
|
const caller = evalExpression === undefined || evalExpression === '' ? undefined : evalExpression;
|
|
128
|
-
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
//
|
|
133
|
-
|
|
171
|
+
|
|
172
|
+
// A selector is a read, so it replaces the caller's expression rather than being
|
|
173
|
+
// chained with it; `buildArgs` rejects the two together, so only one is ever set.
|
|
174
|
+
// The selector script is already a call expression; a caller's expression has to be
|
|
175
|
+
// wrapped so its value is what the engine reports.
|
|
176
|
+
const body = read !== undefined ? { code: read, wrapped: true } : (caller === undefined ? undefined : { code: caller, wrapped: false });
|
|
177
|
+
|
|
178
|
+
if (move === undefined) return body === undefined ? undefined : body.code;
|
|
179
|
+
if (body === undefined) {
|
|
180
|
+
// No read at all: keep the scroll only when a dump will produce the output.
|
|
181
|
+
return hasDump ? move : undefined;
|
|
182
|
+
}
|
|
183
|
+
// The read goes last so its value is the one the engine reports; losing it would
|
|
184
|
+
// contradict the documented `eval` behaviour.
|
|
185
|
+
return `${move}; ${body.wrapped ? body.code : `(() => (${body.code}))()`}`;
|
|
134
186
|
}
|
|
135
187
|
|
|
136
188
|
/**
|
|
@@ -246,22 +298,44 @@ export function isSandboxTlsFailure(message) {
|
|
|
246
298
|
}
|
|
247
299
|
|
|
248
300
|
/**
|
|
249
|
-
* Classify a transport failure
|
|
301
|
+
* Classify a transport failure: did the host never answer, or did a wait condition
|
|
302
|
+
* simply never become true?
|
|
250
303
|
*
|
|
251
304
|
* A blocked host fails at connect time (`Failed to connect`, reset, refused) or,
|
|
252
305
|
* when the socket never opens, as a readiness timeout with nothing to show. A
|
|
253
306
|
* readiness timeout can also mean a genuinely slow page, so that weaker case is
|
|
254
307
|
* reported separately as {@link possiblyUnreachable}.
|
|
255
308
|
*
|
|
309
|
+
* **But not every readiness timeout is about the network.** The engine reports the
|
|
310
|
+
* condition it was waiting for, and three of them mean the page was already there:
|
|
311
|
+
*
|
|
312
|
+
* ... while waiting for a selector
|
|
313
|
+
* ... while waiting for a script to become truthy
|
|
314
|
+
* ... while waiting for a subresource response
|
|
315
|
+
*
|
|
316
|
+
* In those cases the document was fetched and parsed; what did not happen is the
|
|
317
|
+
* selector appearing, the script becoming true, or the matching response arriving.
|
|
318
|
+
* Telling the caller to retry or configure a proxy then sends them to look at their
|
|
319
|
+
* network for a problem that is in their selector -- measured: `waitSelector` on an
|
|
320
|
+
* element that is not on the page used to report "the host did not answer in time".
|
|
321
|
+
* A timeout "while waiting for response headers" is the opposite case and stays
|
|
322
|
+
* classified as possibly unreachable, because that one really is the server.
|
|
323
|
+
*
|
|
256
324
|
* @param message - condensed stderr text.
|
|
257
|
-
* @returns
|
|
325
|
+
* @returns `unreachable` (connect-time), `possiblyUnreachable` (network timeout), and
|
|
326
|
+
* `conditionTimedOut` (a wait that the page itself never satisfied).
|
|
258
327
|
*/
|
|
259
328
|
export function isUnreachableFailure(message) {
|
|
260
329
|
const text = String(message);
|
|
261
330
|
const connectFailure = /Failed to connect|Could not connect|Connection was reset|Connection refused/iu.test(text);
|
|
331
|
+
const anyTimeout = /Timeout was reached|readiness timed out/iu.test(text);
|
|
332
|
+
const conditionTimedOut = /readiness timed out .*?while waiting for (?:a selector|a script|a subresource response)/iu.test(text);
|
|
262
333
|
return {
|
|
263
334
|
unreachable: connectFailure,
|
|
264
|
-
|
|
335
|
+
// A condition timeout is not evidence about the network: the page arrived. Only
|
|
336
|
+
// the remaining timeouts are worth mentioning as a reachability possibility.
|
|
337
|
+
possiblyUnreachable: connectFailure || (anyTimeout && !conditionTimedOut),
|
|
338
|
+
conditionTimedOut,
|
|
265
339
|
};
|
|
266
340
|
}
|
|
267
341
|
|
|
@@ -369,13 +443,10 @@ export function buildArgs(args) {
|
|
|
369
443
|
// Screenshots and PDFs silently produce nothing without the layout renderer.
|
|
370
444
|
if (needsLayout) argv.push('--layout');
|
|
371
445
|
if (args.scrollbars === true) argv.push('--scrollbars');
|
|
372
|
-
// `scroll`
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
// see combineEval -- because with a dump the expression exists only for its side
|
|
377
|
-
// effect, and without one it would become the output.
|
|
378
|
-
const evaluation = combineEval(args.eval, args.scroll, args.dump !== undefined);
|
|
446
|
+
// `scroll` and `selector` ride along in the same expression: the engine takes a
|
|
447
|
+
// single `--eval`. `hasDump` matters -- see combineEval -- because with a dump the
|
|
448
|
+
// expression exists only for its side effect, and without one it becomes the output.
|
|
449
|
+
const evaluation = combineEval(args.eval, args.scroll, args.dump !== undefined, args.selector);
|
|
379
450
|
if (evaluation !== undefined) argv.push('--eval', evaluation);
|
|
380
451
|
if (args.wait !== undefined && args.wait !== '') argv.push('--wait', args.wait);
|
|
381
452
|
if (args.waitSelector !== undefined && args.waitSelector !== '') argv.push('--wait-selector', args.waitSelector);
|
|
@@ -419,6 +490,30 @@ export function runFetch(options) {
|
|
|
419
490
|
const args = options.insecureTls === true && options.args.insecureTls === undefined
|
|
420
491
|
? { ...options.args, insecureTls: true }
|
|
421
492
|
: options.args;
|
|
493
|
+
// `selector` replaces the whole-page output, so combining it with either reader is a
|
|
494
|
+
// call that cannot do what it says: one of the two would be silently ignored. That
|
|
495
|
+
// is the failure mode this plugin keeps having to fix, so it is refused up front and
|
|
496
|
+
// as a structured result, the same way a binary dump with no writeTo is.
|
|
497
|
+
if (typeof args.selector === 'string' && args.selector.trim() !== '') {
|
|
498
|
+
if (args.dump !== undefined) {
|
|
499
|
+
return {
|
|
500
|
+
ok: false,
|
|
501
|
+
url: args.url,
|
|
502
|
+
selector: args.selector,
|
|
503
|
+
error: '`selector` and `dump` cannot be combined: a selector returns one container\'s text and a dump returns the whole page, so one of them could only be ignored',
|
|
504
|
+
hint: 'Use `selector` to read part of the page, or `dump` to read all of it.',
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
if (args.eval !== undefined && args.eval !== '') {
|
|
508
|
+
return {
|
|
509
|
+
ok: false,
|
|
510
|
+
url: args.url,
|
|
511
|
+
selector: args.selector,
|
|
512
|
+
error: '`selector` and `eval` cannot be combined: only one expression value can be returned',
|
|
513
|
+
hint: 'Put the extra work in `eval` and drop `selector`, or keep `selector` and read the text it returns.',
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
422
517
|
const argv = buildArgs(args);
|
|
423
518
|
const timeoutMs = args.timeoutMs ?? 30000;
|
|
424
519
|
const isBinary = BINARY_DUMPS.has(args.dump);
|
|
@@ -536,7 +631,7 @@ export function runFetch(options) {
|
|
|
536
631
|
// the socket never opens, as a readiness timeout with nothing to show. A
|
|
537
632
|
// readiness timeout can also mean a genuinely slow page, so it is reported
|
|
538
633
|
// as *possibly* unreachable and worded as a suggestion rather than a verdict.
|
|
539
|
-
const { unreachable, possiblyUnreachable } = isUnreachableFailure(stderr);
|
|
634
|
+
const { unreachable, possiblyUnreachable, conditionTimedOut } = isUnreachableFailure(stderr);
|
|
540
635
|
// The engine reports a thrown expression as
|
|
541
636
|
// `Reason: JavaScript evaluation failed: TypeError: ... at <anonymous>:1:6`
|
|
542
637
|
// which is a bug in the call, not an install or network problem. Falling
|
|
@@ -547,15 +642,27 @@ export function runFetch(options) {
|
|
|
547
642
|
// status 1" leaves the caller to guess whether an image exists; the useful fact
|
|
548
643
|
// is that it does not, and that a text dump would have captured this page.
|
|
549
644
|
const binaryFailure = isBinary && httpStatusFrom(stderr) !== undefined;
|
|
645
|
+
// Name the wait that never resolved. "Retry or set a proxy" is actively
|
|
646
|
+
// misleading here: the page arrived, so the network is fine, and the caller
|
|
647
|
+
// would spend their time on the one part of the setup that is working.
|
|
648
|
+
const conditionHint = args.waitSelector !== undefined
|
|
649
|
+
? `The selector ${JSON.stringify(args.waitSelector)} never matched. The page loaded, so this is not a network problem: check the spelling, and confirm the element is there with \`selector\` (it reports the match count, where a wait only times out) or \`dump\`.`
|
|
650
|
+
: args.waitScript !== undefined
|
|
651
|
+
? 'The wait expression never became truthy. The page loaded, so this is not a network problem: check the expression against the page with `eval`, and remember it is polled rather than evaluated once.'
|
|
652
|
+
: (args.waitResponseUrl !== undefined || args.waitResponseJson !== undefined)
|
|
653
|
+
? 'No response matching that condition arrived. The page loaded, so this is not a network problem: the request may never be made, or its payload may differ from what you matched on. Use `dump` with a short timeout to see what the page does, or an `eval` that inspects the fetch.'
|
|
654
|
+
: 'The wait condition never became true. The page loaded, so this is not a network problem: check the condition.';
|
|
550
655
|
const hint = evalFailure
|
|
551
656
|
? 'The eval expression threw. Check that the page has reached the state you expect (`wait` or `waitSelector`), that the selector matches an element, and that the expression is valid JavaScript. The engine error names the type and position.'
|
|
552
657
|
: binaryFailure
|
|
553
658
|
? 'The engine could not render this page, so no file was written. An HTTP error still produces a body for a text dump (markdown, html or json), so ask for one of those if you need to see what the server returned.'
|
|
554
659
|
: sandboxTls
|
|
555
660
|
? 'The DSH file sandbox blocks Schannel client credentials, so moli cannot do TLS at low integrity. Retry the same call with sandbox_permissions "danger-full-access" (with a justification), use an http:// URL, or pass proxy to reach a local forward proxy.'
|
|
556
|
-
:
|
|
557
|
-
?
|
|
558
|
-
:
|
|
661
|
+
: conditionTimedOut
|
|
662
|
+
? conditionHint
|
|
663
|
+
: possiblyUnreachable
|
|
664
|
+
? 'The host did not answer in time. Reachability to some hosts varies on this network, so retrying may succeed; if a local forward proxy is available, setting the plugin config field `proxy` is the other option. A certificate problem would report a schannel error instead of a timeout.'
|
|
665
|
+
: 'Run moli_probe to confirm the install, then retry; use waitSelector or eval for a page that renders late.';
|
|
559
666
|
const failureStatus = httpStatusFrom(stderr);
|
|
560
667
|
return {
|
|
561
668
|
ok: false,
|
|
@@ -565,6 +672,7 @@ export function runFetch(options) {
|
|
|
565
672
|
sandboxTlsBlock: sandboxTls,
|
|
566
673
|
unreachable,
|
|
567
674
|
possiblyUnreachable,
|
|
675
|
+
...(conditionTimedOut ? { conditionTimedOut: true } : {}),
|
|
568
676
|
...(evalFailure ? { evalFailed: true } : {}),
|
|
569
677
|
hint,
|
|
570
678
|
};
|
|
@@ -602,6 +710,54 @@ export function runFetch(options) {
|
|
|
602
710
|
}
|
|
603
711
|
|
|
604
712
|
const text = outcome.stdout.toString('utf8');
|
|
713
|
+
// A selector result comes back as our own sentinel, because an empty selection and
|
|
714
|
+
// a page with no text are both an empty string from the engine -- verified: an
|
|
715
|
+
// expression returning `""` exits 0 with an empty line. Reporting "matched
|
|
716
|
+
// nothing" is the entire value of asking by selector, so it must not be silent.
|
|
717
|
+
if (typeof args.selector === 'string' && args.selector.trim() !== '') {
|
|
718
|
+
const payload = parseSelectorResult(text);
|
|
719
|
+
if (payload === undefined) {
|
|
720
|
+
return {
|
|
721
|
+
ok: false,
|
|
722
|
+
url: args.url,
|
|
723
|
+
selector: args.selector,
|
|
724
|
+
error: `the selector result could not be read: the engine returned ${JSON.stringify(text.slice(0, 120))}`,
|
|
725
|
+
hint: 'The page may not have finished rendering. Try waitSelector, or wait for the content with waitResponseUrl.',
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
if (payload.matched === 0) {
|
|
729
|
+
return {
|
|
730
|
+
ok: false,
|
|
731
|
+
url: args.url,
|
|
732
|
+
selector: args.selector,
|
|
733
|
+
matched: 0,
|
|
734
|
+
error: `no element matches ${JSON.stringify(args.selector)}`,
|
|
735
|
+
hint: 'The selector found nothing on this page. Check the spelling, wait for the content to arrive (`waitSelector`, `waitResponseUrl`), or use `dump` to see what the page actually contains.',
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
if (payload.withText === 0) {
|
|
739
|
+
return {
|
|
740
|
+
ok: false,
|
|
741
|
+
url: args.url,
|
|
742
|
+
selector: args.selector,
|
|
743
|
+
matched: payload.matched,
|
|
744
|
+
error: `${String(payload.matched)} element(s) match ${JSON.stringify(args.selector)} but none of them has any text`,
|
|
745
|
+
hint: 'The elements are probably images, icons or empty containers. Use `eval` if you need their attributes rather than their text.',
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
ok: true,
|
|
750
|
+
url: args.url,
|
|
751
|
+
kind: 'text',
|
|
752
|
+
selector: args.selector,
|
|
753
|
+
content: payload.joined,
|
|
754
|
+
chars: payload.joined.length,
|
|
755
|
+
matched: payload.matched,
|
|
756
|
+
...(payload.matched > payload.withText
|
|
757
|
+
? { note: `${String(payload.matched - payload.withText)} of the ${String(payload.matched)} matched elements had no text and were left out.` }
|
|
758
|
+
: {}),
|
|
759
|
+
};
|
|
760
|
+
}
|
|
605
761
|
// `eval` together with `dump` is the engine's documented behaviour -- it discards
|
|
606
762
|
// the expression's value and dumps the page state the expression produced -- but
|
|
607
763
|
// it reads as a successful answer to a question that was never answered. The
|
|
@@ -667,9 +823,14 @@ export function runFetch(options) {
|
|
|
667
823
|
const headLen = Math.ceil(budget * 0.6);
|
|
668
824
|
const tailLen = budget - headLen;
|
|
669
825
|
omitted = text.length - budget;
|
|
670
|
-
|
|
826
|
+
// The marker inside the body is only a position signal: where the gap is and how
|
|
827
|
+
// big it is. Everything actionable -- the head/tail split, the total, the writeTo
|
|
828
|
+
// escape -- lives in the note, which is rendered once at the top. Both used to
|
|
829
|
+
// say the same thing, and on a 2,000-character budget the two copies cost 12.6%
|
|
830
|
+
// of everything the model received.
|
|
831
|
+
const mark = `\n\n… [dsh-moli] ${String(omitted)} characters omitted …\n\n`;
|
|
671
832
|
content = text.slice(0, headLen) + mark + text.slice(text.length - tailLen);
|
|
672
|
-
note = `
|
|
833
|
+
note = `Truncated to fit maxChars: the first ${String(headLen)} and the last ${String(tailLen)} of ${String(text.length)} characters are shown, with ${String(omitted)} omitted in the middle. Pass writeTo to keep all ${String(text.length)}.`;
|
|
673
834
|
}
|
|
674
835
|
// `eval` together with `dump` is the engine's documented behaviour -- it discards
|
|
675
836
|
// the expression's value and dumps the page state the expression produced -- but
|
|
@@ -694,6 +855,35 @@ export function runFetch(options) {
|
|
|
694
855
|
};
|
|
695
856
|
}
|
|
696
857
|
|
|
858
|
+
/**
|
|
859
|
+
* Read the payload {@link selectorScript} prefixes onto its result.
|
|
860
|
+
*
|
|
861
|
+
* The engine prints the expression value verbatim, so the script adds a sentinel to
|
|
862
|
+
* tell a selector answer apart from page text. Anything without the sentinel is not
|
|
863
|
+
* a selector answer and comes back as undefined, which the caller reports as a
|
|
864
|
+
* failure rather than dressing it up as content.
|
|
865
|
+
*
|
|
866
|
+
* @param text - the engine's stdout.
|
|
867
|
+
* @returns `{ matched, withText, joined }`, or undefined.
|
|
868
|
+
*/
|
|
869
|
+
function parseSelectorResult(text) {
|
|
870
|
+
const marker = '[dsh-moli:selector]';
|
|
871
|
+
const at = String(text).indexOf(marker);
|
|
872
|
+
if (at < 0) return undefined;
|
|
873
|
+
try {
|
|
874
|
+
const raw = JSON.parse(String(text).slice(at + marker.length).trim());
|
|
875
|
+
if (raw === null || typeof raw !== 'object') return undefined;
|
|
876
|
+
if (typeof raw.matched !== 'number' || typeof raw.joined !== 'string') return undefined;
|
|
877
|
+
return {
|
|
878
|
+
matched: raw.matched,
|
|
879
|
+
withText: typeof raw.withText === 'number' ? raw.withText : 0,
|
|
880
|
+
joined: raw.joined,
|
|
881
|
+
};
|
|
882
|
+
} catch {
|
|
883
|
+
return undefined;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
697
887
|
/**
|
|
698
888
|
* Read the HTTP status out of the engine's stderr reason.
|
|
699
889
|
*
|
package/package.json
CHANGED