* 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>
* fix(codex): resolve the ChatGPT executable inside Codex.app
* fix(codex): try the ChatGPT executable first and sync the launch doc
* fix(codex): scope executable process detection to app bundle
* fix(codex): resolve symlinked app process identity
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(pinterest): add Pinterest adapter suite
Adds `opencli pinterest` with 19 commands over Pinterest's internal resource API
(`POST /resource/<Name>Resource/<action>/`, form-urlencoded `source_url` + `data`,
`X-CSRFToken` + `X-Pinterest-PWS-Handler` headers, `bookmark` paging).
- Read: search-pins / search-boards / search-users, pin, user, user-pins,
user-boards, board-pins, board-sections, download. Reads work anonymously
because Pinterest issues a csrftoken to logged-out sessions too.
- Write: save (boardless repin lands in "Quick saves"), pin-create /
pin-update / pin-delete, board-create / board-update / board-delete,
board-section-create / board-section-delete.
- Deletes require `--confirm`; without it the command resolves and names the
target, then exits non-zero via ArgumentError (pin title + board for
pin-delete, pin count for board-delete, section title for
board-section-delete).
- Boards are addressed by `<username>/<slug>`, a board URL, or the numeric
`boardId` (BoardResource accepts `board_id` and reports the board's url, which
is reused so the id path costs no extra round trip); sections by id or slug.
Display names are not accepted — a name alone cannot say which account a board
belongs to. A Pinterest site route such as a `/pin/<id>/` URL is rejected as
such instead of being parsed as the board `pin/<id>`.
- Board URLs are percent-decoded before use: Pinterest hands out encoded slugs
for non-ASCII board names, and posting those verbatim answers HTTP 404. Slugs
are compared Unicode-normalized so an NFD-composed accent still matches.
- Sections can only be set by a follow-up move. PinResource/create and
RepinResource/create accept a section key, answer HTTP 200, and file the pin
at the board root anyway; only PinResource/update honours it (under
`board_section_id`, not `section_id`). So `save --section` and
`pin-create --section` create then move, and report the created pin id if the
move fails rather than claiming success.
- Omitting an optional text flag leaves the field alone; passing an empty string
clears it. Pinterest refuses link edits on pins it scraped, and answers 401
for that, so its own message is surfaced rather than only "log in".
- Typed errors throughout: ArgumentError for bad refs, unknown sections,
`--section` without `--board`, and limits (validated before any request, with
no silent clamp); AuthRequiredError on 401 (and 403 on writes only, since
reads are anonymous); CommandExecutionError for malformed payloads and
unresolvable targets.
Live-verified end-to-end against a logged-in account: all 10 read commands, and
the full write cycle (board-create → board-section-create → board-update →
pin-create → pin-update → save → the three deletes, preview and confirmed),
including section placement checked on each pin's own `section` field, non-ASCII
board/section slugs, board-id addressing, and clearing a description. 131 tests;
full suite 6258 passed; `tsc --noEmit` clean; `opencli validate` 0 errors;
typed-error-lint and silent-column-drop both new=0; doc coverage 174/174.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(pinterest): drop board-update --privacy empty default
`coerceAndValidateArgs` applies an arg's default and then enforces `choices`
against it, so `default: ''` on a public|secret flag rejected every run that
omitted `--privacy`:
$ opencli pinterest board-update janedoe/my-board --name Foo
error: ARGUMENT Argument "privacy" must be one of: public, secret. Received: ""
Leaving the default off keeps the flag optional; the command already reads it
as `String(kwargs.privacy ?? '')`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(amazon): honor the input marketplace instead of rewriting to amazon.com
* fix(amazon): reject amazon.<label>.<tld> look-alikes and localize the auth hint
* fix(amazon): allow only known marketplace domains
---------
Co-authored-by: OpenCLI-sol <opencli-sol@users.noreply.github.com>
* enrich(ctrip): add train ticket search command
ctrip search already suggests railway stations but there was no way to query the
actual departures. ctrip train <from> <to> --date fills that gap on the public
trains.ctrip.com list page, browser-mode + cookie like flight/hotel-search. Rows
are read by stable class-keyed fields rather than positional innerText;
incomplete cards are dropped, not sentinel-filled.
* enrich(ctrip): add hotel detail command
Single-hotel profile from the detail-page SSR: rating sub-scores, hot facilities, check-in/out policy.
* enrich(ctrip): add bus ticket search command
Intercity coach search via the newbus results deep link (landing SPA does not hydrate under the bridge).
* enrich(ctrip): add ferry ticket search command
Passenger ferry sailings via the ship.ctrip.com results deep link, sibling of bus.
* enrich(ctrip): add cruise package search command
Resolves a departure port name to its legacy per-port code, then reads the .route_info cards.
* enrich(ctrip): add tour package search command
Group and self-guided tour search via the vacations sv=<destination> deep link, stable-class cards.
* enrich(ctrip): add flight+hotel package search command
Shares the vacations product extractor with tour (freetravel section); folds a 万 count multiplier into the shared parser.
* enrich(ctrip): raise CommandExecutionError on rendered-but-unparsed results
Matches the drift handling bus/ferry/train use, so genuine-empty stays EmptyResultError.
* enrich(ctrip): generalize shared list helpers, drop dead train constants
parseListLimit / parsePlaceName replace the train-named helpers now reused across bus/ferry/cruise/tour/package with neutral hints; ferry ship-name/duration read by pattern, not position.
* enrich(ctrip): add attraction listing command
* enrich(ctrip): add round-trip flight search command
* enrich(ctrip): scope attraction to city id and harden flight-round
* fix(ctrip): repoint one-way flight to Ctrip's migrated .flight-item cards
* fix(ctrip): harden travel adapter boundaries
* fix(ctrip): preserve raw limit strings
* test(ctrip): avoid adapter src import
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(trip): add Trip.com international flight search adapter
Trip.com is the English-facing sibling of the ctrip adapter. trip flight
searches worldwide one-way flights, browser-mode + cookie like ctrip flight.
Results are read from .result-item cards by stable data-testid anchors rather
than positional innerText; incomplete cards are dropped, not sentinel-filled.
Closes#2157
* enrich(trip): add hotel-search command
* enrich(trip): add hotel detail command
Single-hotel profile from the detail-page SSR (same shape ctrip hotel uses); also documents the existing hotel-search command.
* enrich(trip): add round-trip flight search command
Reuses the shared .result-item flight extractor against a triptype=rt search URL.
* enrich(trip): rename parseFlightLimit to parseListLimit
The 1-50 limit parser is shared by hotel-search and both flight commands, so a neutral name reads truer than the flight-specific one.
* enrich(trip): add attractions and experiences search command
Anchors on each things-to-do card's stable detail link (name + per-row url) and reads rating/reviews/booked/price by data-format pattern, since the cards use hashed CSS-module classes.
* enrich(trip): add train route timetable command
Reads the per-country SEO route timetable (departure/arrival times, stations, duration, changes) by stable class fields; per-journey fares sit behind the booking step.
* enrich(trip): add car-rental listing command
* enrich(trip): add airport-transfer listing command
* enrich(trip): add tour-package search command
* enrich(trip): add public destination-suggest command
* enrich(trip): add flight+hotel package search command
* docs(trip): note eSIM plans surface via attraction search
* enrich(trip): add live-promotions deals command
* enrich(trip): treat empty deals parse as drift, not empty result
* enrich(trip): split tour no-match (empty) from schema drift
* fix(trip): type public fetch drift failures
* fix(trip): require package flight identity
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(kimi/usage): read quota from membership subscription page
Replace the /code/console page with /membership/subscription?tab=quota so the command surfaces the total usage percentage plus 5h/7h rate limits, gift quota, and booster balance.
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(deepseek/usage): add usage command for platform.deepseek.com
Reads DeepSeek platform usage data from https://platform.deepseek.com/usage
via internal API (get_user_summary) for account-level data and DOM extraction
for time-dimension summary cards.
Output columns:
- balance / bonusBalance (充值/赠送余额)
- cumulativeSpend (累计消费金额)
- monthlySpend / monthlyApiCalls / monthlyTokens (本月数据)
- currentTokenEstimation (当前可用 Tokens 预估)
- timePeriod / periodSpend / periodApiCalls / periodTokens (时间维度)
* test(usage): harden kimi and deepseek usage contracts
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(toutiao): add recommend channel feed, fix hot --limit being ignored
hot declared func(_page, kwargs) while browser:false commands receive a single
args object, so kwargs was always undefined and --limit silently fell back to
30. Its unit tests passed only because they called func(null, kwargs) by hand.
* fix(toutiao): require recommend article identity
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
The "Create a file like clis/<site>/<command>.ts" example predates #928, which
converted the entire adapter layer from TypeScript to JavaScript. The repo now
ships 0 .ts and 1259 .js built-in adapters, so a contributor following the doc
creates a file in the wrong format. Update the example to JavaScript (keeping a
pointer to the still-supported TypeScript path), and fix the adapter test
command, which pointed at src/ rather than the adapter's own clis/ test file.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add ChatGPT Deep Research result extraction
* fix(chatgpt): bind deep research results to requested conversation
* fix(chatgpt): preserve deep research payload failures
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(adapters): add 懂车帝 (dongchedi) + 瓜子二手车 (guazi) car adapters
Two no-login PUBLIC adapters for Chinese car platforms. Both read
server-rendered data (no cookies, no signature, no browser) and ship
pure parsers unit-tested against frozen real-data fixtures.
dongchedi (6 commands) — parses __NEXT_DATA__ SSR JSON:
search 车系搜索 + 指导价/经销商价
series 车系概览(品牌/价格/懂车分/销量排名/款型数)
models 款型列表 + 价格
specs 配置概览(尺寸/动力/四驱/悬挂/气囊)
score 懂车分 8 维评分 + 同级对比
koubei 车主口碑/评价正文
(Dongchedi's /motor XHR APIs are ByteDance-signature gated; the SSR
pages expose the same data unsigned, so the adapter reads those.)
guazi (2 commands) — parses m.guazi.com mobile SSR HTML:
browse 分城市在售二手车列表(售价/里程/年份)
car 车源详情(售价/上牌/里程/过户/配置/车况)
(Desktop www.guazi.com is signature-locked; mobile SSR is open. Deep
pagination/filtering uses the signed API and is intentionally omitted.)
Gates green: tsc, doc-coverage --strict, silent-column-drop (new=0),
typed-error-lint (no new), 24 adapter tests passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(adapters): add 汽车之家 (autohome) — brand catalog + 口碑 ratings
Third no-login PUBLIC car adapter (search by brand, not free text).
autohome (2 commands):
brand 按品牌列出全部车系 + 厂商指导价(grade/carhtml/<INITIAL>.html,
中文品牌名→拼音首字母目录页,DL 块按品牌定位)
score 车系口碑评分:总分 + 各维度 + 故障率PPH + 竞品对比
(k.autohome.com.cn/<id> 的 __NEXT_DATA__.baseData,免登录免签名)
Deliberately omitted (would be silently-wrong without a browser running
Autohome's signing/anti-scrape code): free-text keyword search (signature
gated) and full per-trim 参数配置 (rotating CSS font-glyph obfuscation).
Use dongchedi search/specs/koubei for those. Documented in the adapter doc.
Gates green: tsc, doc-coverage --strict (170/170), silent-column-drop
(new=0), typed-error-lint (no new); 31 adapter tests passing across the
three car adapters.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(car-adapters): fail closed on parser drift
* fix(guazi): fail closed on empty SSR listings
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(github): add `github trending` adapter
Add a PUBLIC adapter that lists repositories from
https://github.com/trending — the trending view is a public HTML page with
no official REST API, so the data was previously unreachable through opencli.
The adapter fetches the page server-side (no browser, no auth) and parses
each repo's full name, description, primary language, total stars, forks,
and stars gained in the period.
Flags:
- `--since` daily | weekly | monthly (default daily)
- `--language` filter by language slug, e.g. python, rust, "c++"
- `--limit` 1..25 (GitHub lists at most 25)
Typed errors: ArgumentError for bad --since / --limit, CommandExecutionError
on request/HTTP failure, EmptyResultError when the page yields no repos.
Tests cover parsing (stars/forks/language/description/url, missing language),
limit truncation, language+since URL building, argument validation, the
empty-result path, and non-ok HTTP.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(github-trending): rename to standalone `github-trending repos`
Move the trending scraper out of the `github` site namespace into a
dedicated `github-trending` adapter to avoid confusion with the bundled
`gh` external CLI. Command is now `opencli github-trending repos`
(site=github-trending, name=repos), leaving room for a future
`developers` subcommand. The `github` site retains only login/whoami.
Regenerated cli-manifest.json; 8 fixture tests + typecheck pass.
* fix(github-trending): fail closed on parser drift
---------
Co-authored-by: minh <claude@ttfy.cc>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
Final head 5f5661c270.
Contract: adds a public read-only Internet Archive adapter with archive search, item, wayback, and snapshots commands. All commands are read access, browser:false, with no login, write, upload, or browser UI side effects. Source of truth is Internet Archive Advanced Search response.docs, /metadata/<identifier>, Wayback available closest snapshot, and CDX JSON header/rows. Fail-closed boundaries: search requires response.docs array, true empty maps to EmptyResultError, rows require stable identifier and numeric downloads; item requires metadata.identifier equals requested identifier and files array, while missing metadata/404 remains empty; wayback distinguishes no closest snapshot true empty from available:true missing URL or 14-digit timestamp malformed CommandExecutionError; snapshots requires top-level CDX array, header array, required columns, and per-row timestamp/original/statuscode/mimetype cells, with malformed shapes typed CommandExecutionError rather than empty snapshot URLs or empty status columns.
Validation: lead+aux final green. GitHub final gate OPEN/non-draft/CLEAN, required checks SUCCESS, adapter/smoke skipped. Local reviewer validation covered current-main merge-tree clean, targeted/full clis/archive 1 file/18 tests, typecheck, build manifest 1231, docs-build, doc coverage 165/165, prod audit clean, node --check touched Archive files/tests, diff-check, typed-error-lint and silent-column-drop no new.
Final head 2279aca321.
Contract: chatgpt model keeps the existing write surface and supports instant, medium, high, extra-high, and pro intelligence levels; thinking remains a backward-compatible alias for high. Unsupported or unknown requested levels fail upfront with ArgumentError. The selection path requires a logged-in ChatGPT composer and native click. The model selector prefers stable test id / exact visible option text, covering current English Instant/Medium/High/Extra High/Pro and Chinese 极速/均衡/高级/超高/专业 labels. Unknown localization falls back to order only when composer-intelligence-picker-content exists and exactly five visible menuitemradio options are present; otherwise it typed-fails with CommandExecutionError instead of treating an ordinary menu or drifted DOM as success. Postcondition re-reads current selector/test id after click; when label recognition is unavailable, it reopens the picker and verifies the target checked index in the five-option intelligence picker. High vs Extra High matching uses longest/exact ordering to avoid substring false success. No ask/send/read/image/history surface changes.
Validation: lead+aux content green. Remote statusCheckRollup is empty, merged under the standing no-check override after final poll confirmed OPEN/non-draft, head unchanged, and MERGEABLE/UNSTABLE with no conflict/dirty/content blocker. Local reviewer validation covered current-main replay clean, focused ChatGPT utils+commands tests 77/77, full ChatGPT 6 files/118, typecheck, build manifest 1227, docs-build, doc coverage 164/164, prod audit clean, node --check touched files/tests, diff-check, typed-error-lint and silent-column-drop no new, and merge-tree clean.
Final head 53686e6a62.
Contract: xiaohongshu ask remains a browser-backed write command. Answer success still requires the same-send message_id/conversation_id plus a finished non-empty answer. Source identity/url/xsec_token are still trusted only from a 24-hex note id, xhsdiscover://item/<id>, or trusted XHS note URL. New source metadata is a minimal optional enrichment: note_type, user_id, and published_at are forwarded from the 点点 source payload and omitted when empty; like_count is parsed only from non-negative safe integers, strict decimal compact counts with 万/w/W/亿 and optional +, or legal pure digit/thousands strings. Malformed values such as 1e2, 0x10, 1..2万, bad comma grouping, negatives, and decimal numbers are omitted rather than coerced into successful counts. No extra note/detail round-trip or new write surface.
Validation: lead+aux final green. GitHub final gate OPEN/non-draft/CLEAN, required checks SUCCESS, adapter/smoke skipped. Local reviewer validation covered focused clis/xiaohongshu/ask.test.js 1 file/15, full XHS adapter 20 files/270, typecheck, build manifest 1227, docs-build, node --check ask/touched tests plus dist/src/main.js, diff-check clean, and local merge-tree clean.
Final head d41552312e.
Contract: xiaohongshu publish gains text-image publishing behind --card-text. --card-text/--images require at least one content source. Normal image suffix/path validation and text-image gif append are pre-navigation ArgumentError. Text-image flow enters 文字配图, writes/verifies each card, waits for a new active empty card before multi-card input, generates previews, then Next enters the standard editor before title/body/topics. Explicit --card-style must resolve/click or typed-fail with CommandExecutionError, with no silent fallback to 基础. After Generate/Next and image append, visible media count postconditions must prove cards and appended images landed. Publish/draft final success requires a success marker or leaving the publish page; warning-like text cannot fake success.
Validation: lead+aux final green on d4155231. GitHub final gate OPEN/non-draft/CLEAN with required checks SUCCESS and adapter/smoke skipped. Local reviewer validation covered publish tests 31/31, full Xiaohongshu 17 files/246 tests, typecheck, build manifest 1225, docs-build, doc coverage 164/164, production audit clean, node --check, diff-check, typed-error-lint/silent-column-drop no new, and merge-tree clean.
Adds Xiaohongshu saved and liked collection read commands.\n\nFinal review contract:\n- saved/liked are read-only current-user collection scrapers; no write, unlike, or favorite side effects.\n- --limit validates strictly in 1..100 before browser navigation.\n- Collection page location is read back after goto and after each scroll before trusting captures/DOM.\n- Location must be exactly https://www.xiaohongshu.com/user/profile/<resolvedUserId>; /login or login-wall text maps to AuthRequiredError; other host/path/profile drift maps to CommandExecutionError.\n- Browser Bridge {session,data} envelopes are unwrapped; malformed location, non-array intercepted requests, and non-array DOM extraction fail closed.\n- API/DOM notes require stable note id and xsec_token; output URLs round-trip to note/detail.\n- Auth/private/empty/malformed/API/parser/selector drift typed boundaries are preserved.\n\nValidation:\n- Lead and aux final green on ef12a197.\n- Final GitHub poll: open, non-draft, head ef12a197, statusCheckRollup empty only; merged under standing no-check override because content review is green and no conflict/dirty/content blocker remains.\n- Focused collection/saved/liked tests 3 files / 19.\n- Full XHS adapter 20 files / 253.\n- typecheck, build/manifest 1227, docs-build.\n- node --check touched collection files + dist/src/main.js.\n- typed-error-lint and silent-column-drop no new issues; listing-id advisory 13; diff-check clean.
Adds targeted Discord desktop-app read/navigation commands with fail-closed identity checks.\n\nFinal review contract:\n- Surface remains read/navigation only: goto, channels, servers, threads, read, and thread-read; no Discord write command is introduced.\n- Browser Bridge evaluate results are unwrapped and shape-guarded at the Node boundary for route state, list rows, messages, and thread lists.\n- True empty list/read/thread-read results map to EmptyResultError; malformed rows or browser output map to CommandExecutionError.\n- Targeted read and thread-read verify message rows bind to the requested channel/thread via channel_id; stale, wrong-channel, parent-channel, or missing identity rows typed-fail.\n- List rows require stable identities: channels require Channel/guild_id/channel_id/url, servers require Server/guild_id/url, threads require Thread/guild_id/channel_id/thread_id/url.\n\nValidation:\n- Lead final and aux final green on e84a7d85.\n- merge-tree clean against base 8ed8ca26.\n- Discord app tests 19/19.\n- build manifest 1225, typecheck, docs-build, doc coverage 164/164, node --check touched files/tests, diff-check.\n- typed-error-lint and silent-column-drop no new issues.\n- GitHub required checks SUCCESS; adapter/smoke skipped.