* fix(errors): duck-typing for cross-package CliError in toEnvelope
Plugins resolve their own copy of @jackwener/opencli (own node_modules), so
fails across package copies and every plugin error
degrades to code: UNKNOWN with the hint lost. Switch to shape-based detection
(code + message strings, optional hint) — real CliError instances behave
identically, plain Errors still map to UNKNOWN.
Also serializes error-like plain objects carrying code/message. Adds tests
for the cross-package shape, plain-object passthrough, and UNKNOWN fallback.
* fix(errors): require exitCode when duck-typing CliError
The shape check accepted any object with string code+message, which also
matches Node system errors (ENOENT, ECONNREFUSED, EACCES) and library
errors carrying a string code. Those would surface their errno as the
envelope code, widening the machine-readable contract callers switch on:
before this commit: ENOENT -> code "ENOENT"
intended/base: ENOENT -> code "UNKNOWN"
CliError's constructor always assigns exitCode (defaulting to
GENERIC_ERROR) while Node system errors never do, so requiring a numeric
exitCode separates genuine cross-package CliError copies from foreign
errors without adding any new concept.
Tests: make the cross-package mock faithful by giving it exitCode, pin
the bare {code,message} object as UNKNOWN, and add an ENOENT regression.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(dribbble): distinguish empty states from drift
* fix(dribbble): handle promoted and nested list items
---------
Co-authored-by: OpenCLI-sol <opencli-sol@users.noreply.github.com>
turndown pulls in @mixmark-io/domino, which publishes its full test suite
to npm: 959 files / 7 MB, ~94% of the package's file count. Upstream has
been unmaintained since 2024 (mixmark-io/domino#2), so remove the test
directory in our postinstall instead. Runs before the CI early-return so
packaged app bundles (OpenCLIApp stages node_modules into Resources/)
shrink as well. Best-effort: resolution failure or a missing dir never
fails the install.
Co-authored-by: exe.dev user <exedev@koala-fife.exe.xyz>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The published tarball ships 601 compiled *.test.js files, ~100 *.test.d.ts,
stray *.test.ts sources, and clis/**/__fixtures__/ HTML snapshots — none of
which are used at runtime. Excluding them shrinks the package from 2340 to
1638 files and from 14.0 MB to 9.2 MB unpacked (tarball 3.1 MB -> 2.2 MB),
which noticeably speeds up npm install.
Verified with npm pack --dry-run against the extracted 1.8.7 tarball
contents: no .test.* or __fixtures__ entries remain.
Co-authored-by: exe.dev user <exedev@koala-fife.exe.xyz>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
renderMarkdown escaped pipes but not newlines, so a cell containing a
line break terminated the table row and broke the markdown structure.
Render embedded newlines as <br>, matching how renderCsv already
handles multi-line values.
* fix(trip): read airport codes from leaf nodes, not the dropped font-black class
`buildFlightExtractJs` sourced the departure/arrival IATA codes from
`[class*="font-black"]`. Trip.com no longer emits that class, so `codes` was
always empty, the `codes.length < 2` guard dropped every card, and `flight` /
`flight-round` failed with EMPTY_RESULT on every route — including the
`LON -> NYC` example in the adapter docs.
The codes are still in the DOM as plain leaf nodes, and the extractor already
walks the leaves to collect times. Filter the codes out of that same scan and
drop the `font-black` dependency.
The unit fixture pinned the stale markup (`<div class="font-black_x">LGW</div>`),
which is why 114 adapter tests stayed green while the adapter returned nothing
against the live site. Replace it with markup captured from Trip.com in 2026-08
so the fixture can actually falsify the extractor.
Verified against the live site: 0/8 result cards parsed before, 8/8 after;
`opencli trip flight` now returns rows for PVG->LHR, SHA->LON and LON->NYC.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(trip): mark later-day arrivals with a +N suffix
Overnight legs lost a day. The extractor built `arrivalTime` from the `HH:MM`
and `AM`/`PM` leaf nodes alone, so a China Southern SHA -> LGW leg departing
19:50 on 2026-09-07 and landing 06:30 on 2026-09-08 was reported as a plain
`6:30 AM`, indistinguishable from a same-day arrival.
Each card already carries `data-testid="flight-time-<ISO>"` on both legs. Diff
the date halves of those two anchors and append `+N` to the arrival time, which
is how Trip.com itself renders it (the card shows a literal `+1` beside the
arrival airport).
Same-day arrivals are unchanged, so the `columns` contract is untouched and only
the previously-wrong rows move.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(trip): bind flight routes to endpoint anchors
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
Clears the esbuild, nanoid, and postcss dev-chain advisories
(6 findings → 3). package-lock.json only; package.json ranges
untouched; production dependency audit remains at zero. The remaining
vite/launch-editor + vitepress findings have no in-range fix upstream
yet (dev-only, Windows-only vector) and are documented in the PR.
undici 7.29.0 (pinned since the CVE-2026-13697 fix) requires Node
>=20.18.1, but engines still promised >=20.0.0. First-principles call:
Node 20 reached EOL in April 2026, so 20.0-20.17 users are on an
unpatched runtime either way — staying on the maintained undici 7.x
line and updating the declaration honestly beats downgrading to the
aging 6.x line for a contract that protects nobody. Updates engines,
the lock root mirror, and both README Node claims.
Collapse seven local unwrapEvaluateResult copies (ask, feed, search,
creator-notes, delete-note, follow, unfollow) into a single
clis/xiaohongshu/shared.js export, and point rednote/search at it.
The three variants differed only in an Array.isArray guard and
condition order; arrays cannot carry the session/data envelope keys
across the CDP JSON boundary, so behavior is identical for every
reachable payload. The canonical copy keeps the guard.
Contract tests move to shared.test.js (strengthened with identity
assertions); the direct duplicates in ask.test.js / search.test.js are
deleted as subsumed. creator-notes __test__ drops the imported key.
* fix(xiaohongshu): harvest search rows during scroll instead of first/last screen union
The search results page is a virtualized masonry list: cards scrolled past
are evicted from the DOM. The previous flow extracted once, scrolled to the
bottom, then extracted again, so the result set was only the union of the
first and last screens -- capped near 20 rows regardless of --limit.
Harvest inside the scroll loop instead. A single page.evaluate now drives
the scroll and accumulates rows into a page-land Map keyed by note id, so
nothing is lost when a card is recycled. On a query that previously returned
34 rows, `--limit 100` now saturates at 100 across repeated runs.
Three related fixes ride along:
- Rows are merged rather than deduplicated on first sight. Masonry cards
render in stages -- the link appears before the title -- so a row first
seen with an empty title used to be cached empty and then dropped by the
title filter. Empty fields are now backfilled on a later encounter,
non-empty fields are never overwritten, and an unsigned /explore/ URL can
be upgraded to an xsec_token-signed one but never downgraded. The likes
emptiness check treats '0' as a placeholder because the extractor writes
'0' for an unrendered count.
For the same reason the target check counts only rows that already have a
title. Counting raw discoveries let the loop stop the moment 100 cards
were known, before the last screen had rendered its titles, and the filter
then silently cut the output back to 84.
- "No new rows this round" is no longer, on its own, a reason to stop.
It was observed firing at scrollTop=4500 of scrollHeight=6960 -- squarely
mid-page, where Xiaohongshu had merely paused lazy-loading. Stopping now
requires the plateau to coincide with either a real bottom or wedged
scrolling, alongside the target/round/wall-clock bounds.
- Risk-control interstitials are detected and raise SECURITY_BLOCK instead
of surfacing as a silently short result set indistinguishable from the
truncation bug above.
Scrolling advances by a viewport-sized step rather than jumping to
document.body.scrollHeight, which would skip whole screens of cards. Round
and wall-clock budgets are derived from the already-validated --limit, so
no new CLI arguments and no cli-manifest.json change. Small --limit values
now finish sooner than before, since reaching the target ends the loop.
buildScrollUntilJs is untouched and buildSearchExtractJs keeps its signature
and semantics, so clis/rednote/search.js -- which imports both -- is
unaffected.
* fix(xiaohongshu): harden virtual search harvesting
* fix(xiaohongshu): validate harvested search rows
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(bilibili): add --top to fetch pinned comments
The comments command already fetches /x/v2/reply/main, whose response
includes top_replies (置顶评论) alongside regular replies but discarded
them. Add a --top flag that returns only the pinned comments.
- --top reads data.top_replies (reusing formatReplyRow)
- --top is mutually exclusive with --parent (楼中楼 threads have no
top_replies) and raises ArgumentError before any request
- generalize requireReplies to accept a key; absent top_replies is
treated as empty, and an empty pinned list raises EmptyResultError
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(bilibili): harden pinned comment contract
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
The star history chart in the README was broken because its data source is no longer reliable under GitHub's current stargazer API restrictions. Update the chart in both README.md and README.zh-CN.md to use a working endpoint so the chart renders correctly again.
Co-authored-by: OctoBored <212877535+OctoBored@users.noreply.github.com>
#2326 pinned undici 7.29.0 in package.json without regenerating the
lockfile, so npm ci fails with EUSAGE on main and every PR merge ref.
Regenerated with npm install --package-lock-only.
Reuses the existing LinkedIn shared evaluate envelope unwrap helper across general messaging commands while preserving subsystem-local copies for later batches.