* feat: add DuckDuckGo, Brave, and Yahoo web search adapters
Add three new search engine adapters with browser-based DOM extraction:
- duckduckgo/search: Search DuckDuckGo via html.duckduckgo.com
Supports region, time filters, and XHR-based pagination (--offset)
- duckduckgo/suggest: Search suggestion autocomplete (no browser needed)
- brave/search: Search Brave Search via search.brave.com
Supports GET-based pagination (--offset)
- yahoo/search: Search Yahoo (Bing-powered) via search.yahoo.com
Supports GET-based pagination (--page)
All search adapters use Strategy.PUBLIC with browser:true, navigating
the target site and extracting results via page.evaluate() DOM queries.
Includes full test coverage (16 tests).
* fix: use clampInt from shared utils and add adapter docs
- Replace Math.max/Math.min patterns with clampInt() from _shared/common.js
to pass the typed-error-lint gate (4 silent-clamp violations resolved)
- Add adapter documentation for duckduckgo, brave, and yahoo to fix
the doc-coverage CI check
- Regenerate cli-manifest.json and typed-error-lint-baseline.json
* fix: avoid silent-column-drop overlap in brave/yahoo extractors
Change buildExtractorJs to return arrays instead of objects whose keys
matched columns. This prevents silent-column-drop audit false positives
as per opencli-adapter-author conventions.
* fix(search): tighten browser search adapters
* chore(search): drop baseline churn
* fix(duckduckgo): execute search extractor safely
* fix(yahoo): reject unsafe redirect targets
---------
Co-authored-by: huzekang <huzekang@opencode.ai>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(xiaohongshu,rednote): unwrap page.evaluate envelope in search adapter
`page.evaluate(...)` returns a `{ session, data }` envelope rather than
the raw IIFE return value, but the search adapters were calling
`Array.isArray(payload)` directly on the envelope. `Array.isArray` is
always false on the envelope, so every search result was silently
dropped — status=success, exit 0, empty array, no error.
The rednote adapter had this same bug; both share `buildSearchExtractJs`
from `xiaohongshu/search.js`.
Introduces `unwrapEvaluateResult(payload)` as a shared helper in
`clis/xiaohongshu/search.js` (re-exported via the existing import line
from `rednote/search.js`). The helper is a defensive ternary: it
unwraps when payload looks like an envelope with an array `.data`,
otherwise it passes the value through unchanged. This keeps the change
back-compat with bridge versions that return the raw value, and
preserves the existing `Array.isArray(payload)` typecheck at each call
site.
Verified manually against `opencli xiaohongshu search "补墙洞"` (a query
known to return 20+ results in a logged-in browser tab): previously
`[]`, now returns the expected ranked rows with all declared columns
(`rank, title, author, likes, published_at, url`) populated.
Adds 5 unit tests for `unwrapEvaluateResult` covering raw array passthrough,
envelope unwrap, non-envelope object passthrough, null/undefined safety,
and the "data is not an array" guard. The existing 19 search tests in
`clis/xiaohongshu/search.test.js` still pass — the unwrap is invisible
to the existing mocks which already return raw arrays.
* fix(xhs): unwrap search evaluate envelopes
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* refactor(notion): replace built-in CDP adapter with external ntn CLI
Notion has shipped an official CLI at https://ntn.dev. It uses the
public Notion API (blocks / databases / properties / comments) instead
of reverse-engineering the Desktop UI, so it survives Notion app
updates and exposes a wider command surface than the in-tree adapter
could.
Changes:
- `src/external-clis.yaml` — register `ntn` as first-class external CLI
(binary `ntn`, homepage ntn.dev, install via the shell-pipe script
on mac/linux)
- `clis/notion/` — entire directory removed (8 commands: status /
search / read / new / write / sidebar / favorites / export)
- `docs/adapters/desktop/notion.md` — removed
- `docs/.vitepress/config.mts` — drop nav entry
- `docs/adapters/index.md` — drop adapter row
- `README.md` / `README.zh-CN.md` — drop notion from feature lines,
drop adapter table row, add `ntn` to CLI hub examples
- `docs/index.md` / `docs/zh/index.md` / `docs/guide/getting-started.md`
— drop notion from electron-control feature copy
- `skills/opencli-usage/SKILL.md` — drop notion from electron list
- `cli-manifest.json` — rebuilt with --allow-removals=8
Migration for users:
`curl -fsSL https://ntn.dev | bash` (or `opencli external install ntn`)
Then use `opencli ntn <command>` in place of `opencli notion <command>`.
Rationale: the in-tree adapter was reverse-engineered against Notion
Desktop CDP and shipped only 8 commands. The official CLI gives users
the full Notion API surface and reduces our maintenance burden to zero.
Same pattern as gh / obsidian / lark-cli / tg-cli / discord-cli / wx-cli.
Verification:
- `npx tsc --noEmit` clean
- `npx vitest run --project unit` → 1091/1 skipped
- `npm run build` (with --allow-removals=8) — manifest 809 entries
- grep notion in user-facing docs (README / docs / skills) — only
descriptive mentions remain in non-blocking places (comparison /
site-recon / electron how-to / design doc), no broken adapter
references
* fix(notion): align ntn external migration
* docs(notion): clarify ntn manual install
Mirrors PR #1464 (list-tweets) and the timeline/search/tweets/likes/thread
family: spread `...extractMedia(legacy)` into the row and surface
`has_media` + `media_urls` columns. Pure parity, no behavior change for
existing callers — media keys do not collide with the original columns.
- bookmarks.js: import `extractMedia` from ./shared.js, spread into
extractBookmarkTweet row, append columns, export __test__.
- bookmark-folder.js: same change on extractFolderTweet, export
extractFolderTweet via __test__.
- bookmarks.test.js (new): baseline + photo + video + entities-only
fallback + dedup + envelope + empty-envelope (8 tests).
- bookmark-folder.test.js: update existing baseline expectation with
has_media/media_urls, add 3 new media tests (photo / mp4 / no-media).
- cli-manifest.json: regenerated; only the two `columns` entries change.
Reverse-validated: tests fail when extractMedia spread is removed.
Audits unchanged: typed-error-lint 189/189, silent-column-drop 102/103
(pre-existing main resolution noted but not consumed here).
* feat(twitter/list-tweets): include media via extractMedia (parity with timeline/search)
list-tweets was the only X recall path that dropped media. timeline.js and
search.js both call extractMedia(legacy) and emit has_media/media_urls;
list-tweets returned only text fields, so downstream consumers (e.g.
ml-scout's rate UI) couldn't render image/video thumbnails on tweets pulled
from a list timeline.
Changes:
- Import extractMedia from ./shared.js
- Spread extractMedia(legacy) into extractTimelineTweet return
- Add has_media, media_urls to columns array (--format columns parity)
- Update unit test to assert the new shape; add coverage for photo and
video extraction
* chore(manifest): rebuild cli-manifest.json for list-tweets media columns
---------
Co-authored-by: ml-scout <ml-scout@anthropic.com>
The opencli external-CLI name is the user-typed subcommand; the binary is
what gets executed. The convention everywhere else (`gh`, `docker`,
`obsidian`, `vercel`, `dws`) is `name == binary`. Three entries violated
the convention: `tg-cli` / `discord-cli` / `wx-cli` registered an
opencli name with a `-cli` suffix that does NOT exist on the binary,
forcing the awkward double-prefix `opencli discord-cli dc` instead of
`opencli discord dc`.
The README's example column already showed the desired form
(`opencli tg search`, `opencli discord recent`, `opencli wx search`) —
only the yaml registration was out of sync.
Renames in `src/external-clis.yaml`:
* `name: tg-cli` → `name: tg` (binary: `tg`)
* `name: discord-cli`→ `name: discord` (binary: `discord`)
* `name: wx-cli` → `name: wx` (binary: `wx`)
The `binary`, `homepage`, and `install` fields are unchanged — the
underlying packages (`kabi-tg-cli`, `kabi-discord-cli`, `@jackwener/wx-cli`)
keep their published names.
Other entries left as-is: `lark-cli`, `wecom-cli`, and `dws` already have
`name == binary` (their actual binaries are `lark-cli`, `wecom-cli`, `dws`).
BREAKING CHANGE: `opencli tg-cli ...`, `opencli discord-cli ...`,
`opencli wx-cli ...` no longer resolve. Use `opencli tg ...`,
`opencli discord ...`, `opencli wx ...` instead. The feature is recent
(shipped 2026-05) so impact is expected to be minimal.
* feat(twitter): default tweets to logged-in user + fix sibling envelope-unwrap silent bug
Primary: make `opencli twitter tweets` default to the logged-in user
when no username is given, so agents can pull their own posts without
needing to know their own handle. Mirrors the existing self-detection
pattern in twitter/profile and twitter/likes (AppTabBar_Profile_Link
probe on /home, then UserByScreenName lookup). Description + help
string now mention the default so agents discover it.
Consistency pass — profile/likes/following/followers: the
self-detection in these four siblings was silently broken because
page.evaluate() primitive returns come back through the CDP bridge
wrapped as `{session: 'site:twitter', data: '/<handle>'}` (same
envelope root cause as #1525). They called `.replace()` directly on
the envelope object → TypeError surfaced as AUTH_REQUIRED 'Could not
detect logged-in user', even for logged-in users. Wrap each probe
with unwrapBrowserResult so the bare href string survives. Also:
- Add an explicit page.goto('/home') + page.wait(primaryColumn)
before the probe in likes/following so the AppTabBar sidebar is
guaranteed rendered (framework pre-nav lands on bare x.com without
the sidebar mounted).
- following.js: switch its probe from the function-literal form
`() => {...}` to a template-string. Confirmed live: function-literal
silently drops primitive returns entirely — bridge returns
`{session}` with no `data` field at all, while template-string
returns `{session, data}` as expected.
Out of scope (pre-existing, flagged as follow-up): likes/following
have additional downstream evaluate paths (userId/GraphQL fetch) that
still drop or envelope their results; they return [] or
'Could not find user' even after this PR. Same daemon-side bug class
as #1525.
Live-verified:
opencli twitter tweets --limit 2 → own tweets (@jakevin7)
opencli twitter profile → own profile
Tests 227/227, audits typed-error-lint 189 + silent-column-drop 103
unchanged, manifest stable at 816 entries.
* fix(twitter): validate self-detected handles
* fix(twitter): unwrap downstream self evaluate results
* fix(twitter): unwrap page.evaluate primitive returns in lists/list-tweets/following
The opencli >=1.7.x browser bridge wraps page.evaluate's primitive return
values as { session, data: <value> }. Adapters that destructure .data
inline (e.g. data.queryId, data.viewer) keep working because the wrapper
spreads object-typed responses to the top level, but ones that consume
the return value as a bare string broke:
- twitter list-tweets: the dynamically resolved queryId (a string) became
{session, data:"..."}. Interpolating that into the GraphQL URL produced
/i/api/graphql/[object Object]/ListLatestTweetsTimeline, giving "HTTP
400: queryId may have expired".
- twitter lists: same on ListsManagementPageTimeline queryId.
- twitter following: same shape bug on the href read from the profile
link, producing "TypeError: href.replace is not a function" when no
--user is given.
Add a small unwrap() helper at each call site so primitive returns are
extracted from the wrapper before use. Object-typed GraphQL responses
are left as-is since they rely on spread semantics.
* fix(twitter): rewrite list-add to use ListAddMember GraphQL mutation
In 2026-05 X replaced the "Add/remove from Lists" modal dialog with a
full-page route (/i/lists/add_member). The previous UI flow no longer
works:
Save button not found in dialog (X expected text Save/Done).
Dialog structure may have changed.
The mutation that the dialog used to fire (ListAddMember) is still the
right primitive — and the surrounding adapter already calls X GraphQL
APIs directly to resolve userId and verify member_count. Drop the UI
flow entirely and call ListAddMember directly via fetch in the page
context.
Wins:
- Works again on current X UI (verified 2026-05-12 on x.com).
- ~10x faster: no goto-profile + click-caret + scroll-dialog round trips.
- One less moving piece — no dependency on Chrome extension's nativeClick
for this command.
Implementation notes:
- LIST_ADD_MEMBER_QUERY_ID is a 2026-05 fallback; resolveTwitterQueryId
does live lookup from the loaded client-web bundle, matching the
pattern already used elsewhere in the twitter clis.
- X's ListAddMember response routinely contains a non-fatal partial
decode error on default_banner_media_results (code 214, Validation /
BadRequestError) alongside a fully populated data.list. We treat the
call as failed only when data.list / member_count is missing, and
ignore decode-flavored errors confined to banner fields.
- Same opencli >=1.7.x { session, data } primitive-wrap behavior that
the previous commit addressed applies here: userId from the
UserByScreenName call needs unwrap before being interpolated into
the mutation body, otherwise X parses "[object Object]" as user_id
and returns "strconv.ParseInt ... invalid syntax".
Verified flows:
- noop (already a member) → status: noop, member_count unchanged.
- new add (e.g. @AnthropicAI on a fresh list) → status: success,
member_count incremented.
Trade-off: rejection signals (e.g. X declining to add @deepseek_ai)
look indistinguishable from noop at the response level, since X returns
HTTP 200 with member_count unchanged. Documented in the success message.
* fix(twitter): integrate list media and harden list-add
---------
Co-authored-by: wangyan <wy@wang-yan-Air.local>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(zhihu): add answer-detail to fetch a single answer's full content
The existing `zhihu answer` adapter is a write (post an answer); the
listing `zhihu question` truncates each answer's body to 200 chars.
There was no way to fetch one specific answer's full content by id.
New read adapter `zhihu answer-detail`:
- Accepts a bare numeric answer id, a typed target `answer:<qid>:<aid>`,
or a full Zhihu answer URL (the form you paste from a browser).
- Calls `/api/v4/answers/<aid>?include=content,voteup_count,...,question`
inside the cookie-bearing page context (Strategy.COOKIE).
- Returns a single row with id / author / votes / comments /
question_id / question_title / url / created_at / updated_at /
content. The content column is the full stripped answer body by
default — no silent truncation. `--max-content N` is an opt-in user
cap (mirroring the wikipedia `page` flag), and `--max-content 0`
(the default) means "no cap, full content".
Important precision note: Zhihu answer ids since 2024 routinely
exceed `Number.MAX_SAFE_INTEGER` (the test fixture uses the real id
`1937205528846655537`). `data.id` is round-tripped through browser
`JSON.parse` and would round to `1937205528846655500`, so the adapter
deliberately ignores `data.id` for the canonical row id and anchors
it to the already-validated input string instead. A regression test
locks this contract in by mocking `data.id = 0` and asserting the row
still carries the parsed input id.
Typed errors: bad input → INVALID_INPUT; 401/403 → AuthRequiredError;
other HTTP / null → FETCH_ERROR. No silent fallbacks, no sentinel
strings.
Live-verified against the example URL — fetched 5547 votes / 165
comments / 1937205528846655537-end-to-end. 16 unit tests, audits
unchanged (typed-error-lint 189/189, silent-column-drop 103/103),
manifest 816→817.
* fix(zhihu): tighten answer-detail contracts
* fix(google-scholar/search): wrap evaluate return to fix serialization
Same issue as google/search: page.evaluate() serializes JS arrays as
plain objects across the CDP boundary, causing Array.isArray() to
return false. The adapter silently returned [] instead of results.
Also replace fixed page.wait(3) with selector-based wait for
.gs_r.gs_or.gs_scl with a 3s fallback.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(google-scholar): type search evaluate payload
* chore: rerun google scholar search checks
---------
Co-authored-by: cxiao <chuda.xiao@wuerzburg-dynamics.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(xiaohongshu): parseLikes should handle 2.1w / 1.5万 / 1.2k shortforms
Xiaohongshu renders top-popular comment like-counts as shortened
strings like '2.1w' / '1.1万' / '1.2k' once they exceed ~10 000.
The previous parseLikes only matched bare digits via /^\d+$/ and
silently returned 0 for any shortform, which inverted the sort
order: the highest-liked comments (often 10k+) ranked last while
mid-tier comments with plain numeric counts (e.g. 7569) appeared
on top.
Repro on any popular xiaohongshu thread (>10 000 likes on a top
comment): with --format json the most-upvoted parent rows show
"likes": 0.
This patch keeps the original fast path for plain integers and
adds a single regex for the well-known shortform suffixes:
- w / 万 -> *10000
- k / 千 -> *1000
- trailing '+' tolerated (e.g. '999+')
- unknown shapes still fall back to 0 (no behavior change)
Note: parseLikes runs inside the IIFE injected via page.evaluate(),
so the existing comments.test.js mock harness (which stubs
evaluate's return value directly) does not exercise it. A future
refactor that exports parseLikes for direct testing would be a
separate change.
Affects both top-level comments and 楼中楼 sub-replies (same
helper).
* fix(xiaohongshu): parse comment like shortforms safely
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* chore: drop util.styleText to support Node v20+
util.styleText was added in Node v21.7.0 / v20.12.0. v21.0.0-v21.6.x and
v20.0.0-v20.11.x throw `SyntaxError: ... styleText` at startup because the
import resolves before any user code runs (a real user reported this on
v21.2.0).
OpenCLI is primarily agent-facing — terminal colors are noise to consumers,
and the [OK] / [WARN] / [FAIL] / ℹ / ⚠ / ✖ markers we already write carry
the semantic info that colors only repeated. Strip styleText entirely from
logger / output / doctor / tui / update-check / cli / download/progress /
commands/daemon and clean up the resulting awkward `${'literal'}` template
fragments. engines.node now reads ">=20.0.0".
This removes the Node-version coupling that A/B fixes would only have
papered over.
* fix(runtime): truly support Node v20+ by aligning guard + undici
Follow-up to the styleText removal: declaring engines.node >=20.0.0 is
not enough on its own. Two coupled barriers remained:
- src/runtime-detect.ts: MIN_SUPPORTED_NODE_MAJOR = 21 explicitly
rejected v20 at startup
- undici@^8.0.2 declares engines.node >=22.19.0; Node 20/21 crash on
webidl.util.markAsUncloneable before any user code runs
Lower the guard to 20 and downgrade undici to ^6.25.0 (engines >=18.17,
retains Agent / EnvHttpProxyAgent / fetch / Dispatcher). Smoke-tested
--help / doctor / list on Node v20.0.0, v21.2.0, v22.22.2. 213/213
targeted unit tests pass.
* feat(zhihu): paginate question answers and recommendations
* fix(zhihu): drop Math.min limit clamp and 'unknown' sentinel
Two audit-driven fixes on top of feat/zhihu-pagination-recommend:
1. question.js: replace `Math.min(answerLimit, 20)` with a named
constant `ZHIHU_PAGE_SIZE = 20`. The Zhihu API caps `limit` at 20
per request anyway, and the pagination loop already trims to the
user-requested `answerLimit` via `answers.length >= answerLimit`,
so the Math.min silent-clamp was both unnecessary and tripped the
silent-clamp audit. Updates the existing unit test to expect the
API-max page size in the fetch URL with an explanatory comment.
2. recommend.js: rebuild the dedup key without the `'unknown'`
sentinel. The old form `\`\${target.type || 'unknown'}:\${target.id}\``
collapsed distinct typed items into the same bucket whenever
`target.type` was missing, and tripped the silent-sentinel audit.
New form: prefer `type:targetId`, fall back to `__feed:item.id`,
and when neither id is available keep the row but skip dedup
(surfacing potentially-duplicate items beats silently dropping
them).
Audits unchanged (typed-error-lint 189/189, silent-column-drop
103/103). All 88 zhihu tests pass.
---------
Co-authored-by: lihaidong <lihaidong@kingsoft.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
Issue #1506 reports `opencli xiaohongshu search` returning `[]` even though
the page visibly has results. Trace evidence: xhs ships a render variant
where each note card is a bare `<section>` (no `note-item` class), so
the three `section.note-item` selectors in this file all match zero
elements.
Three call sites in the shared search IIFEs now use the same defensive
selector strategy: try the legacy `section.note-item` class first, then
fall back to any `<section>` that wraps a `/search_result/...` or
`/explore/...` link. The change is in the xiaohongshu file so the
rednote adapter (which imports `buildSearchExtractJs` and
`buildScrollUntilJs` from here) picks it up automatically.
Extraction-side title selector also gets a fallback: when no
`.title` / `.note-title` element matches, read the first `<span>`
inside the search-result link, which is where the bare-section render
puts the caption per the trace.
## Verification
`npx vitest run --project adapter clis/xiaohongshu/`: 105/105 green
(existing test suite unchanged, passes on both legacy and fallback paths).
Live verify on rednote (same code path, account-safe):
```
$ opencli rednote search "美食" --limit 3 -f json
[ {rank:1, title:"在朋友家吃过一次..."}, {rank:2, title:"我的15💰晚餐..."}, {rank:3, title:"干净饮食🫛..."} ]
```
Legacy `section.note-item` path is exercised here (rednote still renders
the class) and returns identical row shape to before the fix, confirming
no regression on the working path.
Live verify on xiaohongshu cannot be performed here (no logged-in xhs
session on the test machine; xhs account-ban risk per the project's
operational guidance). The fix is structural: the new `<section>` shape
the issue reporter traced is reachable through the fallback, and the
existing test fixture keeps the legacy path green.
`npx tsc --noEmit` clean. `npm run build` 815 manifest entries unchanged
shape. `silent-column-drop` / `typed-error-lint` baselines unchanged.
Closes#1506
Refs #1500
* feat(reddit/read): add --expand-more via /api/morechildren + 7-kind discriminated union
PR B of the rdt-cli parity follow-up (after PR #1491, see #1481 thread).
Closes the second-largest gap: Reddit's "[+N more replies]" stubs were
opaque markers in the comment tree. With --expand-more, the adapter
follows them by POST-ing the t1 ids to /api/morechildren.json, then
re-threads the returned things back into the tree by parent_id before
walking it.
New args:
- `--expand-more` (bool, default false) — turn on stub expansion.
- `--expand-rounds <N>` (int, default 2, range [1, 5]) — Reddit returns
fresh "more" stubs at the expansion depth boundary, so up to N rounds
are run. Strictly validated via `parseExpandRounds` — out-of-range
raises ArgumentError BEFORE `page.goto`, no silent clamp.
Boy-Scout: the in-browser script now returns a 7-kind discriminated
union instead of a flat row array (matching the PR #1428 / #1491
sediment). Each kind maps 1:1 to a typed error on the Node side:
- `inaccessible` → EmptyResultError
401/403/404 on /comments/<id>.json (post-specific access, not
session-level auth — applies the PR #1491 review-side sediment
"inaccessible-resource vs session-auth").
- `auth` → AuthRequiredError
401/403 on /api/morechildren (expand-write endpoints often demand
a logged-in session even when the read endpoint is anonymous).
- `http` → CommandExecutionError
- `malformed` → CommandExecutionError
200 with unexpected envelope shape — schema drift, not empty.
- `parser-drift` → CommandExecutionError
tree had t1 entries but the walker produced no rows (PR #1491
review-side sediment "post-construction 0 rows + pre-walk
non-empty = parser drift, not legitimate empty").
- `expand-failed`→ CommandExecutionError
/api/morechildren returned a non-empty json.errors array.
- `ok` → returns rows[].
Intermediate keys (kind / detail / httpStatus / where / rows /
expandMeta) deliberately avoid the declared columns (type / author /
score / text) per the PR #1329 silent-column-drop sediment.
Tests:
clis/reddit/read.test.js — 11 tests
- Adapter shape (browser / siteSession / columns / args)
- --expand-more / --expand-rounds present with correct types/defaults
- parseExpandRounds default / range / non-integer rejection
- Pre-navigation validation (bad --expand-rounds doesn't reach goto)
- kind=ok happy path (POST + L0 rows)
- 6-kind error → typed error mapping
- Unknown envelope shape → CommandExecutionError
- Evaluate script embeds expandMore/expandRounds/sort/limit literals
- Evaluate script contains /api/morechildren POST scaffolding
- Evaluate script never names declared columns as intermediate keys
Full reddit suite 48/48; full project 3402/3402.
Audits: typed-error-lint 189/189 (0 new), silent-column-drop 103/103
(0 new). Manifest 815 → 815 (existing read entry gets 2 new args).
Existing --limit / --depth / --replies / --max-length keep their
original Math.max-style behaviour (grandfathered in the baseline);
only the new --expand-rounds flag fails fast per the typed-errors
standard.
Refs: https://github.com/jackwener/rdt-cli (browse.read --expand-more)
* fix(reddit): preserve expanded comment tree order
* fix(reddit): fail on partial morechildren expansion
* fix(twitter): repair search and tweets readback
* fix(twitter): prefer baked operation features when bundle parse returns empty
The bundle parser in resolveTwitterOperationMetadata locates the queryId via
`queryId:"..."` inside a ~2500-char snippet around the operationName marker,
then independently extracts `featureSwitches:[...]` and `fieldToggles:[...]`
via separate regexes. When minification rearranges the snippet (or the
snippet window truncates before the array), either regex can miss while
queryId still resolves; keysToFlags(undefined) then returns {}.
sanitizeTwitterOperationMetadata previously accepted any object as
features / fieldToggles, including {}. Twitter's GraphQL endpoint rejects
SearchTimeline / UserTweets requests with empty features (HTTP 400),
surfacing a misleading "queryId may have expired" error — the queryId is
fresh; only the feature flags are missing.
Guard against this by deferring to the baked fallback whenever the resolved
map is empty. Adds a JSDOM-free unit test that, reverse-validated, fails on
the un-fixed code with the exact silent-fallback shape.
Refs PR #1512
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
page.evaluate() serializes JS arrays as plain objects, causing
Array.isArray() to return false and the adapter to throw NOT_FOUND
even when results exist. Wrap the return value in {items: results}
and extract via wrapper.items to avoid the type check issue.
Co-authored-by: cxiao <chuda.xiao@wuerzburg-dynamics.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Per @WAWQAQ direction (DM): trim PR-time CI to fast-feedback only.
adapter-test (~30-60s) is the next-largest PR wait after e2e-headed
(which #1521 just removed). Adapter authors typically run focused tests
locally before pushing (`npm run test:adapter`); CI duplication adds
queue latency without catching new classes of bugs.
PR-time CI surface now:
- typecheck / unit (~1 min)
- lint gates (typed-error / silent-column-drop)
- build × 3 platforms
Adapter test guards (still strict):
- push to main / dev
- nightly cron
- workflow_dispatch (manual when an adapter-heavy PR really wants the
signal before merge)
Same gate as smoke-test (`if: github.event_name == 'push' || schedule
|| workflow_dispatch`) for consistency.
Per-PR e2e-headed Chrome was the dominant PR-time wait (~10-15 min on
two platforms) and on fork PRs blocks behind maintainer approval, while
the actually-blocking failures it caught in the last 30 days were all
e2e-test migrations missed by the authoring PR (#1461 / #1505 workspace
->session) rather than real regressions the unit/typecheck tier missed.
PR feedback path is now:
- typecheck / unit / lint / adapter / build ← `pull_request` (ci.yml)
- extension typecheck / build ← `pull_request` (build-extension.yml)
- docs build ← `pull_request` (doc-check.yml)
- security audit ← `pull_request` (security.yml)
E2E-headed Chrome guards:
- push to main / dev (watched paths)
- push v* tag (release)
- nightly cron 08:00 UTC (added: catches Chrome version drift / flake
drift even when no commits touch watched paths)
- workflow_dispatch (manual when a PR really wants e2e signal)
smoke-test was already gated on `schedule || workflow_dispatch` only
(ci.yml), so no change needed there.
`pageScopedResult()` in extension/src/background.ts was spreading the
lease's session into the result `data` for every page-scoped command. For
the `exec` action — which routes user JavaScript through page.evaluate()
— this contaminated arbitrary user-JS returns:
* Array / primitive returns came back as `{ session, data: <value> }`
envelopes. Adapters that did `Array.isArray(result)` got `false` and
treated the page as having no rows. Visible repro:
`opencli google search ...` and `opencli xiaohongshu search ...` —
Chrome rendered results correctly but adapters extracted an empty array
(reported in #1518 from the Browser Bridge v1.0.12 envelope).
* Plain-object returns had an extra `session` key spliced in, silently
overwriting any user `session` field with the lease's value.
Fix in the extension layer instead of compensating client-side:
`pageScopedResult` now returns `{ id, ok, data, page }` — the same form
it had before #1461 added the workspace→session refactor. Client-side
unwrapping is no longer needed and the original PR #1518 `Page.evaluate`
heuristic is dropped (it only covered the array path and would have
missed the plain-object path).
Two adapter improvements kept from the original PR:
* `clis/google/search.js` — wait for `#rso a h3` (with a 5s timeout)
before extracting. On Chrome 148 / Linux Wayland the DOM can settle
before SERP anchors are populated, so the existing fixed `wait 2`
could return empty even with the envelope fix.
* `clis/xiaohongshu/search.js` — extract initially visible cards before
scrolling, then merge post-scroll rows by URL. Xiaohongshu's
virtualized masonry can evict the initial note cards from the DOM
after scroll, causing extraction to return [] even though the
browser had rendered results correctly.
Extension version bumped to 1.0.14.
Repro environment (from #1518):
* OpenCLI 1.7.18
* Browser Bridge extension 1.0.12 → 1.0.14
* Chrome 148.0.7778.96
* Linux Wayland, Node 22.22.1
Tests: extension/src/background.test.ts navigate same-url assertion
updated to no longer expect `session` in `data`. Three Page.evaluate
unwrap test cases removed.
* fix(xueqiu/kline,earnings-date): format dates in Asia/Shanghai instead of UTC (#1465)
`xueqiu/kline` and `xueqiu/earnings-date` formatted bar timestamps with
`new Date(ts).toISOString().split('T')[0]`. That string is the UTC
calendar date, always one day earlier than the date xueqiu shows in its
UI (which is Beijing-aligned for every market). Issue #1465 reports
"5月10日跑的,5月8号的k线没有" because the May 8 China trading-day bar
was labeled 2026-05-07. Same off-by-one was present in `earnings-date.js`.
Routes both call sites through a new `formatChinaDate(ts)` helper in
`clis/xueqiu/utils.js` built on `toLocaleDateString('en-CA', { timeZone:
'Asia/Shanghai' })`. Verified live against SZ300136 and AAPL: both now
match the dates shown on xueqiu.com.
Tests: `clis/xueqiu/utils.test.js` (new) pins the Asia/Shanghai semantic
with 4 cases (China midnight, late-evening, 16:00 UTC day boundary, and
nullish input). `npx vitest run --project adapter clis/xueqiu/` 49/49,
`npx tsc --noEmit` clean, `npm run build` 815 entries unchanged shape.
Closes#1465
* fix(xueqiu): stabilize China date formatting
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
`OPENCLI_KEEP_TAB` was a debugging shortcut, not a config dimension. It
let users override `--keep-tab` globally via the shell environment,
which contradicts the per-command lifecycle model: `siteSession:'persistent'`
already pins persistent site tabs as a hard adapter-metadata constraint,
and `--keep-tab true|false` covers the ad-hoc override case. The env
just leaked process state across every browser command in the shell.
Changes:
- src/execution.ts: `resolveKeepTab()` drops the
`normalizeBooleanOption('OPENCLI_KEEP_TAB', process.env.OPENCLI_KEEP_TAB)`
fallback. `--keep-tab` is now the single user override.
- src/execution.test.ts: two regression tests rewritten to use the
`executeCommand(cmd, {}, false, { keepTab: 'true' })` signature
instead of the env. Logic and assertions unchanged.
- README.md / README.zh-CN.md / skills/opencli-usage/SKILL.md:
drop the env table row. `--keep-tab` documentation stays.
- CHANGELOG.md: BREAKING entry under Unreleased.
Note: the 1.7.15 CHANGELOG entry still references the env historically;
that's intentional, historical entries are not retroactively edited.
Verification:
- npx tsc --noEmit pass
- npx vitest run --project unit --project extension → 1144/1145 pass
(1 unrelated skip)
- typed-error-lint baseline 189
- silent-column-drop baseline 103
* refactor(browser): replace --session flag with <sessionname> positional
The `--session <name>` flag was semantically required but syntactically
optional, which is an anti-pattern. Required + flag is a contradiction:
flag form implies "optional", required is a runtime patch on top. Session
is OpenCLI's "operation target" identifier — the natural form for that is
a positional argument, like `docker exec <container> <cmd>` or
`git checkout <branch>`.
New surface:
opencli browser <sessionname> open https://x.com
opencli browser <sessionname> click 12
opencli browser <sessionname> bind
opencli browser <sessionname> unbind
Commander 14 cannot natively combine a parent positional with subcommand
dispatch — the parent's positional is shadowed by subcommand matching. To
bridge that, main.ts now pre-processes argv: when the token after `browser`
is non-flag and not a known subcommand name, it is treated as the
sessionname and rewritten to the internal `--session <name>` flag form
before commander parses it. Help text on the `browser` command is
overridden via `.usage('<sessionname> <command> [options]')` so users see
the positional form.
Reserved subcommand names (33) are listed in cli-argv-preprocess.ts and
tested for parity with cli.ts subcommand registrations. If a future
subcommand is added, the test fails loudly.
Synced surfaces:
- README.md / README.zh-CN.md — all examples
- docs/guide/browser-bridge.md (+ zh)
- skills/opencli-browser/SKILL.md (bind/unbind, examples, table)
- skills/opencli-usage/SKILL.md
- tests/e2e/browser-tabs.test.ts
- CHANGELOG.md (Unreleased BREAKING)
The internal `--session` flag and the unit tests calling
`program.parseAsync(['...', 'browser', '--session', 'foo', ...])` are
preserved as a stable internal API: tests bypass main.ts pre-processing
and exercise commander directly. The pre-processor has its own targeted
test file (cli-argv-preprocess.test.ts, 10 tests, all green).
Verification:
- npx tsc --noEmit — pass
- npx vitest run --project unit — 1073/1074 pass (1 unrelated skip)
- npx vitest run --project extension — 61/61 pass
- npm run check:typed-error-lint — baseline 189
- npm run check:silent-column-drop — baseline 103
* fix(cli-argv): only rewrite when `browser` is the root command
The preprocessor was looping through every argv slot and would mis-rewrite
occurrences of the literal word `browser` deeper in argv (e.g. `opencli
adapter init browser/x` or arg values containing `browser`).
Now the preprocessor walks past leading root flags + their values to
identify the root command token, and only acts when that token is
`browser`. The full set of root value-consuming flags
(`ROOT_VALUE_FLAGS`) is documented inline and kept in sync with the
`program.option()` calls in cli.ts.
Adds regression tests:
- `opencli adapter init browser x` not rewritten
- URL/path values containing `browser` not rewritten
- `list browser state` (different root command) not rewritten
- `--profile work browser foo state` correctly identifies `foo` as
sessionname (not as --profile's value)
- `--profile=work` long-form-with-equals consumes one slot only
- boolean flags (`-v`) don't consume the next value
12/12 preprocessor tests pass.
* fix(cli-argv): hide --session flag, fail-fast on retired form, rename to <session>
Three blockers in #1505 review:
1. `--session` flag was still visible in `opencli browser --help` and could
be used as a public entrance, contradicting "positional only" UX.
Fix: switch from `.requiredOption()` to `.addOption(new Option(...).hideHelp())`.
The flag is preserved as an internal API for the daemon protocol and direct
`program.parseAsync` callers (tests), but is no longer documented or
surfaced in structured help.
2. `opencli browser --session foo state` still succeeded. Now the argv
preprocessor throws `BrowserSessionArgvError` when root `browser` is
followed by `--session`, and main.ts catches it and exits with a
user-facing usage error pointing to the positional form.
3. Missing-session error message exposed the internal flag:
`required option '--session <name>' not specified`. Now `getBrowserSession()`
in the action body throws `<session> is a required positional argument:
opencli browser <session> <command>`, and commander no longer guards the
hidden option.
Also (per @WAWQAQ) rename placeholder `<sessionname>` -> `<session>` everywhere
user-facing — shorter, matches CLI convention. The help text "<session> is a
required positional: pass the name of the browser session..." carries the
"name" semantics in description, not in the placeholder itself.
Sync surfaces:
- src/cli.ts — usage line, addOption with hideHelp, descriptions
- src/cli-argv-preprocess.ts — throw on --session form
- src/cli-argv-preprocess.test.ts — refusal test for old form
- src/cli.test.ts — assertions updated for hidden option + new error path
- src/help.ts — read `_usage` private field to respect `.usage()` override
(commander's `.usage()` getter returns auto-generated form if not set,
which would otherwise pollute every namespace's usage string)
- src/main.ts — catch BrowserSessionArgvError, stderr + exit
- README.md / README.zh-CN.md
- docs/guide/browser-bridge.md / docs/zh/guide/browser-bridge.md
- skills/opencli-browser/SKILL.md / skills/opencli-usage/SKILL.md
- CHANGELOG.md
Manual smoke tests (against built dist):
- `opencli browser --help` shows `Usage: opencli browser <session> <command> [options]`
- `opencli browser --help` Options block does NOT show `--session`
- `opencli browser --session foo state` → friendly error, no commander stacktrace
- `opencli browser state` → `<session> is a required positional argument: opencli browser <session> <command>`
- `opencli browser foo state` → parses correctly
* fix: inject <session> into subcommand help paths and drop stale sessions ref
Two follow-up blockers from #1505 review:
1. Subcommand help and structured help still rendered the command path
without the parent's positional. `opencli browser foo state --help`
showed `Usage: opencli browser state [options]`, which would lead
users (and agents reading structured help) to think
`opencli browser state` was a valid invocation. Now:
- `commanderPath()` injects an ancestor's leading-positional placeholder
(extracted from its `.usage()` override) between the ancestor's name
and the next path segment when building paths upward.
- `commandPathFromRoot()` strips placeholder segments (e.g. `<session>`)
from the relative `name` field so agents can still address subcommands
by their leaf name; placeholders remain in the `command` / `usage`
display paths.
- `program.configureHelp({ commandUsage: ... })` is applied recursively
to every descendant of `browser`, because commander does NOT inherit
`configureHelp` into subcommands.
Result:
opencli browser <session> click --help
-> Usage: opencli browser <session> click [target] [options]
Daemon, plugin, adapter, profile namespaces (no `.usage()` override)
are unaffected.
2. `skills/opencli-browser/SKILL.md` still referenced
`opencli browser sessions`, which was removed in #1470. Replaced the
sentence with the underlying invariant ("Bound sessions have no
OpenCLI idle-close timer; the binding lasts until `unbind`, tab close,
window close, or daemon restart") without mentioning the deleted
command.
Tests:
- cli.test.ts: structured help expectations updated to include
`<session>` in command/usage paths (3 tests)
- cli-argv-preprocess.test.ts: 12 tests still green
- 1136/1137 unit+extension green (1 unrelated skip)
- typed-error-lint baseline 189
- silent-column-drop baseline 103
* feat(reddit): add whoami, home, subreddit-info read commands
Closes gap against jackwener/rdt-cli — three commands the existing 17 reddit
adapters were missing:
- `reddit whoami` — show the currently logged-in identity (fields:
Username, ID, Post / Comment / Total Karma, Account Created, Gold, Mod,
Verified Email, Has Mail, Inbox Count). Probes `/api/me.json` with
two-pronged auth detection (401/403 OR `data.name` missing on 200 —
Reddit returns 200 with an empty body for stale anon sessions, see PR
#1428).
- `reddit home` — personalized Best feed (`/best.json`). Distinct from
the public `frontpage`/`r/all` command: enforces login via the same
two-pronged auth check rather than silently degrading to the
unauthenticated default feed. `--limit` accepts [1, 100] — out-of-range
raises `ArgumentError` before navigation, no silent clamp.
- `reddit subreddit-info` — subreddit metadata (Name, Title, Subscribers,
Active Now, NSFW, Type, Description, Created, URL) from
`/r/<X>/about.json`. Banned / private / quarantined / 404 subreddits
raise `EmptyResultError` so the output table never holds a silent
sentinel row.
All three use Strategy.COOKIE + siteSession:'persistent' matching the
existing reddit adapters, validate args upfront before `page.goto`, and
use the 5-kind discriminated-union pattern (kind: auth/http/missing/
exception/ok) from PR #1428 to map page.evaluate results to typed errors
on the Node side. Intermediate object keys deliberately avoid the
declared columns (`field`/`value`/`rank`/etc.) per the silent-column-drop
audit sediment from PR #1329.
Tests: 28 new (whoami 6, home 9, subreddit-info 13); full reddit suite
38/38. Audits: typed-error-lint 189/189 (0 new), silent-column-drop
103/103 (0 new). Manifest 812 → 815.
Refs: https://github.com/jackwener/rdt-cli
* fix(reddit): tighten new read command failure contracts
* fix(reddit): treat inaccessible subreddit info as empty
* chore(scripts): auto-refresh dist/ before build-manifest
`build-manifest.ts` is invoked via tsx so its own imports go to TS source,
but the adapter `.js` files it loads import `@jackwener/opencli/registry`
through package exports, which resolves to `dist/src/registry-api.js`.
When `dist/` is stale relative to `src/` (e.g. a contributor edits
`src/registry.ts` and runs only `npm run build-manifest` instead of the
full `npm run build`), the stale dist drops fields like `siteSession`
from the rebuilt manifest. CI catches the resulting diff via the
"cli-manifest.json is up-to-date" gate, but locally it surfaces as
mysterious unrelated diff lines for adapter files the contributor never
touched.
Add an npm pre-script that runs `tsc --build` (incremental, ~0.6s when
warm) so `npm run build-manifest` is safe to use directly. `npm run build`
is unchanged — it still does the full `clean-dist + tsc + copy-yaml +
build-manifest` sequence, and `prebuild-manifest` will be a no-op there
since TS is already compiled by the time it runs.
Verified:
- `rm -rf dist && npm run build-manifest` now restores dist via the
pre-hook and produces a 0-line diff against committed manifest
- `npm run build` still produces the same clean output
* fix(scripts): force manifest dist refresh
* fix(scripts): avoid duplicate manifest compile
* feat(ctrip): add hotel-search + flight browser-mode commands
Closes#1481.
Two new browser-mode commands on top of the existing public `search` /
`hotel-suggest` pair:
- `ctrip hotel-search <city> --checkin --checkout [--limit]` reads
`window.__NEXT_DATA__.props.pageProps.initListData.hotelList` on
`hotels.ctrip.com/hotels/list`. SSR-rendered first page ships ~13
entries; the server ignores `&pageSize=N` so limit caps at 30 with
default 10. AuthRequiredError surfaces when Ctrip redirects to the
captcha gate.
- `ctrip flight <from> <to> --date [--limit]` searches one-way flights on
`flights.ctrip.com/online/list/oneway-…`. The post-load XHR is not
currently captured by the daemon network buffer (per the known
daemon_capture_pipeline_bug_2026_05_07 in agent memory), so rows are
pulled from `.flight-list > span > div` cards via a position-anchored
innerText parser. A generic `buildScrollUntilJs(selector, target)`
helper mirrors the PR #1487 xiaohongshu scroll-until pattern with the
selector parameterised. Round-trip + airline filters are out of scope
for v1.
All argument validation (IATA / ISO date / city ID / limit range) fires
upfront before any `page.goto`, per the PR #1387 boundary standard. No
silent clamps, no sentinel rows: rows missing required fields are
dropped, and end-state checks raise `ArgumentError` /
`AuthRequiredError` / `EmptyResultError` as appropriate. The new
`mapHotelRow` / `pickHotelMapCoords` / `buildFlightExtractJs` /
`buildScrollUntilJs` helpers live in `clis/ctrip/utils.js` alongside the
existing suggest helpers.
Docs at `docs/adapters/browser/ctrip.md` now distinguish the public
suggest commands from the browser-mode commands and document each
command's columns + caveats.
Verified:
- 61/61 vitest tests in `clis/ctrip/ctrip.test.js` (including JSDOM
exercises of `buildFlightExtractJs` and full `mapHotelRow` shape parity)
- `check:typed-error-lint` 189/189 (0 new)
- `check:silent-column-drop` 103/103 (0 new)
- `build-manifest` clean — 812 entries total (was 810)
* fix(ctrip): harden browser search failure contracts
* fix(ctrip): tighten browser empty-vs-parser failures
* docs(skill/adapter-author): warn aria-label / placeholder / title is locale-dependent
aria-label changes with the browser's UI language (chrome://settings/languages).
A button labelled `aria-label="Submit"` in English Chrome becomes
`aria-label="提交"` in Chinese Chrome, so CSS selectors hardcoded to one
locale silently match zero elements — `notEmpty` / `types` never fire because
the adapter just returns 0 rows.
First-principles framing in adapter-template:
- Split DOM attributes into "locale-stable identifiers" (id / class /
data-testid / data-* / role) vs "locale-dependent text" (aria-label /
title / placeholder / alt / textContent)
- Primary selectors must use locale-stable identifiers; locale-dependent
text is a last-resort tiebreaker
- When a site (e.g. ChatGPT web) only exposes aria-label, link the existing
`clis/chatgpt/utils.js` fallback-list pattern (en + zh-CN + stable
fallback at the front)
Explicitly document why we are NOT building a `find --i18n "zh:提交"` flag
(over-engineering: same indirection as a fallback list plus a translation
dictionary to maintain) and why we are NOT locking Chrome's locale at launch
(opencli doesn't launch Chrome — it connects to the user's running browser
via CDP, so forcing en-US would break users who intentionally run Chinese UI).
Adds pitfall #11 to success-rate-pitfalls.md for the agent-facing checklist.
Closes#1474
* docs(skill): tighten locale selector guidance
* fix(xiaohongshu+rednote): scroll until enough rows are rendered instead of fixed 2x autoScroll
Both search adapters previously called `page.autoScroll({ times: 2 })` which
hard-capped extraction at ~13 notes (xiaohongshu lazy-loads ~5-7 notes per
scroll round) regardless of `--limit`. Reported in #1471: `--limit 40` still
only returned 13 results.
Replace with a dynamic `buildScrollUntilJs(targetCount, maxScrolls=15)`
helper that:
- counts visible `section.note-item` rows (excluding `.query-note-item`
related-search rows)
- breaks early when count >= target
- breaks early after 2 consecutive scrolls add no new rows (DOM plateaued,
feed exhausted)
- hard caps at 15 iterations to bound runtime
Exported from xiaohongshu and reused by rednote (same DOM shape) instead of
duplicating the IIFE.
Fixes#1471
* fix(xiaohongshu): tighten search scroll boundary
* fix(doubao/ask): restore Assistant turn detection after 2026-05 DOM refactor
Doubao reworked message-item wrappers and dropped all `receive-message` /
`bg-g-receive-msg-bubble` markers from assistant turns. The legacy 6
`itemSelectors` (`item-kDun2N`, `union_message`, `message-block-container`,
`data-message-id`, `bg-g-send-msg-bubble`, `bg-g-receive-msg-bubble`) match 0
elements on the new DOM, so `getTurnsScript` returned [] and `getDoubaoTurns`
fell through to the whole-page transcript scraper. Assistant text came back as
sidebar labels + history titles + adjacent conversation snippets concatenated
with the real reply — silent SELECTOR failure (no thrown error).
Two minimal changes in `clis/doubao/utils.js` `getTurnsScript`:
1. `itemSelectors`: prepend `[class*="inner-item-"]` and `[class*="top-item-"]`
— the new 2026-05 wrappers. Outer wins via existing ancestor-keep dedup
below, so we get one root per turn (not one per nested chunk).
2. `getRole`: add a third fallback branch — if the root matches
`inner-item-*` / `top-item-*`, contains `.flow-markdown-body`, and has NO
`bg-g-send-msg-bubble` marker (User detection still works), treat it as
Assistant. `.flow-markdown-body` is already in `messageTextSelectors`, so
text extraction kicks in unchanged.
Test added asserting both new wrappers and the `.flow-markdown-body` assistant
fallback are present in the generated script.
Fixes#1478
* test(doubao): cover refactored assistant turns
* fix(youtube): request srv3 format for caption URLs (#1420)
YouTube may return empty responses when caption URLs lack an explicit format
parameter. This adds fmt=srv3 (standard YouTube XML caption format) to the
caption URL when no fmt parameter is already present, with a fallback to the
original URL if srv3 also returns empty.
Also adds HTTP status checking before reading the response body, preventing
silent failures on non-200 responses.
Fixes#1420
* fix(youtube): preserve caption fetch failures
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(reddit): add reply command for replying to comments
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(reddit/reply): replace silent-sentinel rows with typed errors
reply.js originally mirror-copied comment.js's failure pattern: returning
[{ status: 'failed', message: 'HTTP 403' }] on auth/HTTP/Reddit errors and
relying on the caller to inspect the row instead of throwing. That's the
'silent-sentinel' anti-pattern from typed-errors.md — failures should
surface as typed errors so an agent can actually branch on them.
Round 21 lesson (f) — "grandfathered-not-exempt + helper-refactor boundary
is new" — applies: comment.js / upvote.js / save.js can stay grandfathered,
but a brand-new file does not inherit that exemption.
Changes:
- Throw AuthRequiredError when /api/me.json or /api/comment returns 401/403,
or when /api/me.json returns 200 but data.name is missing (stale anon
session — empty modhash alone isn't a strong enough signal).
- Throw CommandExecutionError for non-2xx HTTP and for non-empty
data.json.errors (e.g. RATELIMIT, NO_TEXT, TOO_OLD).
- Drop the over-defensive `if (!page) throw ...` — registry guarantees a
page object when browser:true.
- Intermediate result object uses `kind` discriminator + `detail` /
`httpStatus` / `where` keys that don't overlap with columns
['status','message'], so the silent-column-drop audit stays quiet
(per PR #1329 sediment).
Verified:
- npx tsc --noEmit clean
- node scripts/check-typed-error-lint.mjs → 189/189, 0 new
- node scripts/check-silent-column-drop.mjs → 103/103, 0 new
- npx vitest run clis/reddit src/convention-audit → 11/11 pass
- node ./dist/src/main.js validate → 0 errors
Success path is unchanged: still returns
[{ status: 'success', message: 'Reply posted on t1_<id>' }].
* fix(reddit): harden reply command contract
* fix(reddit): reject suffixed reply urls
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(rednote): add rednote.com adapter mirroring xiaohongshu read commands (#1136)
Implements rednote.com support as discussed in issue #1136. The mainland
xiaohongshu adapter stays in place; international users redirected to
www.rednote.com now have a CLI without a copy-pasted adapter.
Issue #1136 documents that xiaohongshu and rednote share DOM selectors,
URL paths, API paths, response schema, cookies, and the xsec_token auth
mechanism. The only material differences:
Layer xiaohongshu rednote
Web host www.xiaohongshu.com www.rednote.com
API host edith.xiaohongshu.com webapi.rednote.com
Security host fe-static.xhscdn.com as.rednote.com
Cookie root .xiaohongshu.com .rednote.com
Search gate Inline text Full-screen modal + text
## Architecture (minimal)
`clis/xiaohongshu/*` keep all selector / regex / extraction logic. Each
command file is touched minimally to export the IIFE or pipeline so the
sibling adapter can reuse it:
search.js + export const buildSearchExtractJs(webHost)
+ export const command = cli({...})
note.js + export const NOTE_EXTRACT_JS
+ export const command = cli({...})
comments.js + export function buildCommentsExtractJs(withReplies)
+ export parseCommentLimit
+ export const command = cli({...})
download.js + export function buildDownloadExtractJs(noteId)
(CDN allowlist now includes rednote alongside xhscdn)
+ export const command = cli({...})
user.js + export const USER_SNAPSHOT_JS
+ export const command = cli({...})
feed.js + export function buildFeedPipeline(webHost)
+ export const command = cli({...})
notifications.js + export function buildNotificationsPipeline(webHost)
+ export const command = cli({...})
note-helpers.js buildNoteUrl now accepts `cookieRoot` + `signedUrlHint`
options (defaults preserved so xhs callers and tests
are unchanged)
user-helpers.js buildXhsNoteUrl / extractXhsUserNotes accept an
optional `webHost` argument (default xhs)
The `export const command = cli({...})` pattern matches twitter/lists.js
and clis/discord-app/*; without it the build-manifest scanner attributes
xhs's command to whichever rednote sibling triggered the transitive
import first.
## clis/rednote/ — thin shims
Each rednote command file imports the relevant builder / constant from
its xiaohongshu sibling and calls `cli()` with the rednote host triple.
No selectors, regexes, or extraction logic are duplicated.
search.js imports buildSearchExtractJs + noteIdToDate
declares its own WAIT_FOR_CONTENT_JS (modal + text
login-gate variants — the one xhs behaviour that
genuinely differs)
note.js imports NOTE_EXTRACT_JS + buildNoteUrl + parseNoteId
comments.js imports buildCommentsExtractJs + parseCommentLimit
+ buildNoteUrl + parseNoteId
download.js imports buildDownloadExtractJs + buildNoteUrl + parseNoteId
user.js imports USER_SNAPSHOT_JS + extractXhsUserNotes
+ normalizeXhsUserId
## Scope (initial)
Ships the five commands verified live against the user's logged-in
rednote.com session: search / note / comments / user / download.
`feed` and `notifications` are intentionally left out. Both rely on
intercepting the xiaohongshu Pinia store at the `homefeed` / `you`
capture pattern; live verification on rednote returns `tap → dict
(error)` for the feed step, so shipping them would surface a broken
contract. The mainland xiaohongshu commands continue to work. Adding
the rednote-side feed / notifications is straightforward follow-up
work once someone with rednote access maps the network surface.
Creator-center commands (publish, creator-*) have no rednote
counterpart and stay xiaohongshu-only, per the reporter's note in #1136.
## Verification
- clis/xiaohongshu/ + clis/rednote/: 103/103 tests green
- npx tsc --noEmit: clean
- npm run build: 807 manifest entries (xhs 13 + rednote 5 + everything
else preserved)
- silent-column-drop / typed-error-lint: 103 / 189 baseline entries,
no new violations
- Live verify against the user's rednote.com session:
rednote search "travel" --limit 1 → real note row
rednote note <signed-url> → 7 field/value rows
rednote comments <signed-url> --limit 3 → 3 top-level rows
rednote user 5b21f6564eacab3b38f05c39 --limit 2 → 2 profile notes
Spaced 15–30s between runs per the xhs/rednote rate-limit guidance;
no write commands invoked. Regression check: xiaohongshu/feed on
the existing mainland session still returns the standard 6-field
rows after the refactor.
Closes#1136
* fix(rednote): tighten adapter failure boundaries
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
Doctor's job is browser-bridge health diagnosis. The `--no-live` flag
let users skip the connectivity probe (= the core diagnostic), and
`--sessions` listed automation sessions (a separate concern not part of
health). Both flags accreted features that violated the command's
first-principles purpose.
Cleanup chain (removing dead code surfaced by the flag removal):
- `--no-live` / `--sessions` flags removed from `opencli doctor`
- `DoctorOptions.live` / `DoctorOptions.sessions` removed
- `DoctorReport.sessions` removed
- `[SKIP] Connectivity` render branch removed (always-live now)
- `listSessions()` removed (only consumer was doctor)
- `'sessions'` action removed from daemon-client protocol type
- `BrowserSessionInfo` type removed (no remaining consumers)
- extension `handleSessions` action handler removed (1.0.12)
- extension test "reports sessions per session" removed
- `OPENCLI_BROWSER_IDLE_TIMEOUT` test rewired to 'cookies' action
Verification:
- root typecheck + extension typecheck pass
- doctor.test.ts 17/17 pass
- extension/background.test.ts 49/49 pass
- typed-error-lint 189/189 baseline
- silent-column-drop 103/103 baseline
- build + extension build green
Follow-up to feat #1458 (registering tg-cli/discord-cli/wx-cli in
src/external-clis.yaml) — README and README.zh-CN had not been updated
to reflect the new entries.
Updates four spots in each README:
- intro paragraph that names example external CLIs
- "CLI Hub" highlight bullet
- "OpenCLI is not only for websites" bullet list
- the External CLI table itself
Add three local-first messaging CLIs to the External CLI registry so
agents can discover and install them via `opencli external install`:
- `tg-cli` (binary `tg`) — Telegram local sync/search/export via MTProto
- `discord-cli` (binary `discord`) — Discord local sync/search/export
- `wx-cli` (binary `wx`) — WeChat local data CLI
Refresh the External CLI list in skills/opencli-usage/SKILL.md so the
agent-facing skill names stay in sync.
Extends A0 (PR #1404) by dogfooding `installCommanderNamespaceStructuredHelp`
on the four remaining built-in Commander namespaces:
- `opencli daemon --help -f yaml|json`
- `opencli plugin --help -f yaml|json`
- `opencli adapter --help -f yaml|json`
- `opencli profile --help -f yaml|json`
Each emits the same payload shape as `browser`: namespace metadata, every
leaf command's positionals + command_options + description + usage,
namespace_options (empty for these), and program-level global_options.
Agents can fetch every leaf's contract in a single call — no per-leaf
`--help` follow-ups.
Each namespace snapshots its original description at declaration time
because `applyRootSubcommandSummaries(program)` later overwrites
`.description()` with a child-name listing; without the snapshot,
structured help would surface `"restart, status, stop"` instead of
`"Manage the opencli daemon"`. Tests lock the snapshot semantics for
`adapter` explicitly.
Tests: 138/138 (4 new — one per namespace, covering description
preservation, leaf names, positionals, command_options).
Typecheck + build clean.
* perf(reddit): opt 13 browser adapters into shared site-tab lease
Adds `browserSession: { reuse: 'site' }` to every reddit adapter that
already runs `browser: true` on `domain: 'reddit.com'`. Same metadata-only
follow-up to the twitter sweep merged in #1454 — the framework's
`shouldRunPreNav` short-circuit (src/execution.ts:190) skips the redundant
domain-root pre-nav when a sibling adapter already has the tab on
reddit.com, and idle-bound tabs are reused under the `site:reddit` bucket
until expiry.
Scope (13 files, all on `domain: 'reddit.com'` + `Strategy.COOKIE`):
- read (9): frontpage / popular / saved / search / subreddit / upvoted /
user / user-comments / user-posts
- write (4): comment / save / subscribe / upvote
Excluded:
- `hot.js` (no browser:true — public Reddit JSON API, no tab)
- `read.js` (Strategy.COOKIE but no browser:true — non-browser pipeline)
No logic changes; only metadata + manifest regeneration.
Verification:
- npm run check:typed-error-lint → 189/189 unchanged
- npm run check:silent-column-drop → 103/103 unchanged
- npm run test:adapter → 264/264 passed (2146 tests)
- npx vitest run --project unit → 72/72 passed (unrelated EADDRINUSE
flake on daemon.test.ts port 19825, also seen on #1454/#1452)
- tsc --noEmit clean
* fix(reddit): include read in site browser session reuse
The uploadImages function catches errors from page.setFileInput and only
falls back to the legacy base64 DataTransfer method when the message
contains 'Unknown action' or 'not supported'. However, Chrome can also
return 'Not allowed' (code -32000), which was not handled — causing the
publish command to fail instead of using the fallback.
Add 'Not allowed' to the fallback condition so image upload works even
when CDP file injection is blocked by Chrome's security policy.
Co-authored-by: together <together@togetherdeMac-mini.local>
* feat(openreview): add author command for ID-explicit publication lookup
Closes the missing leaf in the openreview adapter. Among the public-strategy
academic adapters, dblp and arxiv both already ship an `author` command for
ID-explicit publication lookup; openreview only had `search` (full-text),
`paper` (detail by note id), `reviews` (thread by forum id) and `venue`
(listing by invitation / venue text). There was no way to ask "give me every
submission this author put on OpenReview, newest first."
`openreview author <profile>`:
- takes a canonical profile id (`~First_LastN`); validated by
`requireProfileId` so a dblp PID or a bare name fails before any
network call,
- hits `/notes?content.authorids=~<id>&limit=<n>&sort=cdate:desc`,
- returns rank-ordered rows with the same shape as `openreview search`
(id / title / authors / venue / pdate / url),
- throws `EmptyResultError` when the profile has no public submissions
instead of returning an empty list,
- inherits the typed-error envelope from `openreviewFetch` so network
failure, non-200, malformed JSON, and in-band error envelopes all
surface as `CommandExecutionError`.
Tests: 6 new `it` blocks plus 1 updated registration test in
`clis/openreview/openreview.test.js`.
- `requireProfileId` (1 block, 9 assertions): accepts canonical
`~First_LastN`, `~Bo_Liu17`, and a multi-segment middle-name id;
rejects empty, whitespace, missing tilde, missing trailing number,
embedded space, and a dblp-style PID.
- 5 author runtime cases covering pre-network ArgumentError, empty
result, non-200, fetch network error, and the happy path with a
request-shape assertion (`content.authorids` filter + `cdate:desc`
sort).
- Registration test extended to expect five commands and lock the new
`columns` contract.
Manifest auto-regenerated to register the new command.
Live-verified end to end against `~Yoshua_Bengio1`: the most recent ICLR
2026 workshop submissions return with the expected fields. A malformed
profile is rejected before any HTTP call. A nonexistent profile yields
`EMPTY_RESULT`.
* fix(openreview): accept real profile id slugs
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
Convert 9 of 18 page.wait(N) calls in clis/claude/ from fixed-duration
sleeps to event-based readiness checks (page.wait({ selector, timeout }),
backed by MutationObserver). Mirrors the deepseek D1 template (PR #1449).
* Page-ready waits (5 converted): utils.js:29 (ensureOnClaude composer),
utils.js:114 (getConversationList recents links), new.js:19 (composer),
detail.js:24 (.font-claude-response message bubble), send.js:26 (composer).
Each resolves as soon as the selector matches, swallowing the timeout so
downstream typed-error helpers (ensureClaudeLogin / ensureClaudeComposer
/ EmptyResultError) still surface the right error when the selector
never mounts (login redirect, empty conversation, etc).
* Dropdown waits (2 converted): utils.js:150 (selectModel post-trigger),
utils.js:178 (setAdaptiveThinking post-trigger). Wait for menuitemradio
/ menuitem to mount instead of a fixed 0.6 s sleep.
* Resume conversation wait (1 converted): ask.js:51 — wait for the resumed
message bubble (MESSAGE_SELECTOR) instead of a fixed 2 s sleep.
* Settle/redundant waits removed (6): ask.js:55 standalone settle (next
ensureClaudeComposer queries composer presence directly via getPageState);
ask.js:83 / ask.js:90 post-toggle settles (next CDP eval flushes React
state between roundtrips); ask.js:103 pre-waitForResponse settle (the
polling loop's first 3 s tick already covers this); read.js:20 post-
ensureOnClaude sleep (ensureOnClaude now waits for the composer selector
itself); send.js:29 post-ensureOnClaude sleep (same).
Three remaining page.wait(N) calls are kept: utils.js:231 post-input
1.2 s React debounce inside sendMessage (the ProseMirror editor needs a
debounce window before the send button enables; reducing this risks
silent send-button-disabled drops), and the 3 s / 1 s polling ticks in
waitForResponse / waitForFilePreview (already polling patterns, out of
scope for D-track wait→event sweep).
Targeted tests: clis/claude + src/browser 389/389 pass; tsc clean;
build clean; typed-error 189/189 baseline (no new); silent-column-drop
103/103 baseline (no new).
D2 in the LLM-adapter wait→event sweep started by deepseek (D1, #1449).
Convert 10 of 18 `page.wait(N)` calls in clis/deepseek/ from fixed-duration
sleeps to event-based readiness checks (`page.wait({ selector, timeout })`,
backed by MutationObserver):
* Page-ready waits (5): utils.js:46, ask.js:38/52, detail.js:28, new.js:19
now wait for the composer textarea (TEXTAREA_SELECTOR) or message bubble
(MESSAGE_SELECTOR) to mount before continuing. Resolves as soon as the
selector matches instead of always sleeping the full duration.
* Settle/redundant waits removed (5): ask.js:56 standalone settle (already
covered by upstream selector waits); ask.js:79/105 post-toggle settles
(next CDP eval gives React time to flush aria-checked updates); ask.js:118
pre-waitForResponse settle (the polling loop's first 3 s tick already
covers this); read.js:19 post-ensureOnDeepSeek sleep (ensureOnDeepSeek
now waits for the textarea selector itself).
* `new.js` now throws CommandExecutionError when the composer fails to
mount within 8 s instead of silently returning "New chat started" on a
half-loaded or logged-out page.
Eight remaining `page.wait(N)` calls are kept: in-loop polling ticks in
waitForResponse / pickResumeUrl / getConversationList / waitForFilePreview
/ send-button-enable polling (these are already polling patterns and
out of scope for D1), and the native-input flush + textarea-mount poll in
send.js.
Targeted tests: clis/deepseek 49/49, src/browser 355/355 pass; build,
typecheck, typed-error and silent-column-drop audits clean.
Proof template for the LLM-adapter wait-cleanup follow-ups.
Read-only Twitter/X adapters now declare `browserSession: { reuse: 'site' }`,
matching the LLM-site adapters (claude/gemini/yuanbao/etc.) and unblocking
the perf wins WAWQAQ called out for the 35s→9s/3.4s thread.js progression
(#OpenCLI:3889b5cf):
- Tab lease shared across calls under `site:twitter` until idle expiry, so
the second-and-later command pays no cold-start tab cost.
- Framework's domain-root pre-nav (`https://x.com`) is skipped on subsequent
calls when the reused tab is already on x.com (`shouldRunPreNav` →
`isDomainRootPreNav` + `urlMatchesDomain` short-circuit at
`src/execution.ts:190`).
Files (17 read-only adapters):
- Strategy.COOKIE × 13: article, bookmark-folder, bookmark-folders,
bookmarks, download, following, likes, list-tweets, lists, profile,
thread, timeline, trending, tweets
- Strategy.UI × 1: followers
- Strategy.INTERCEPT × 2: notifications, search
Insertion point in each file: after `browser: true,` (or after `strategy:`
in download.js which omits the explicit `browser:` field), matching the
convention used by yuanbao/read.js, claude/read.js, etc.
Manifest regenerated (cli-manifest.json: +85/-17 — 17 entries gain the
`browserSession: { reuse: "site" }` block).
Verification:
- npx tsc --noEmit clean
- npx vitest run clis/twitter → 218/218 pass (25 files)
- npx vitest run src/convention-audit.test.ts → 8/8 pass
- typed-error-lint baseline 189/189 (no new violations)
- silent-column-drop baseline 103/103 (no new violations)
Scope notes (intentionally NOT in this PR):
- Write adapters (post/reply/quote/like/retweet/bookmark/follow/list-add/
list-remove/delete/hide-reply/block/accept/follow) are kept as one-shot
by default — `reuse: 'site'` for write paths is a separate decision
about action idempotency under tab reuse.
- The thread.js / timeline.js comments still say "Cookie context
auto-established by framework pre-nav"; the deeper truth (CDP
`getCookies({url})` is origin-independent) was a framing nit on PR C
(#1451) — left as a doc-only follow-up to keep this PR's diff focused
on the perf gain.
Refs: #OpenCLI:3889b5cf (WAWQAQ msg=fa209a2c, msg=35c90460, msg=838128ef
"你们继续做啊… 后面还有那么多其他的东西呢")
* perf(twitter): drop redundant goto+wait — framework auto pre-navs (PR C)
Twelve twitter read adapters did `await page.goto('https://x.com'); await
page.wait(2~3)` purely to establish cookie context for the subsequent
`document.cookie` read. After PR #1450 hoisted those reads to
`page.getCookies({url})` (which queries the CDP cookie store directly,
no navigation needed), the explicit goto+wait became dead.
The framework already pre-navigates to `https://${domain}` for any
adapter declaring `Strategy.COOKIE + domain` (`src/registry.ts:191`),
so the cookie store is populated before `func` runs. The 2-3s
`page.wait` was the slowest part of the redundant call.
Files (all read-only, all ct0/cookie-only):
- bookmark-folder / bookmark-folders / bookmarks
- following / likes / list-add / list-remove / list-tweets / lists
- thread / timeline / tweets
Out of scope (kept as-is): goto calls that navigate to a *specific*
URL needed for content/SPA shell — `trending` (`/explore/tabs/trending`),
`notifications` (`/home`), `article` (`/i/article/{id}`), `profile`
(`/${username}`), and `list-add` line 133 (`/${username}` for UI ops).
Verification:
- npx tsc --noEmit ✓
- npx vitest run clis/twitter → 216/216 ✓
- typed-error-lint 189/189, 0 new ✓
- silent-column-drop 103/103, 0 new ✓
* fix(twitter): keep list UI root navigation
* perf: replace document.cookie reads with page.getCookies({domain}) (Tier 1 cookie API sweep)
Prior pattern in 25 adapter files round-tripped through `page.evaluate(\`document.cookie.split…\`)` to extract a single cookie value (CSRF token, session ID, etc.). CDP's `page.getCookies({domain})` reads the cookie store directly with zero JS-execution overhead.
Files touched (sites: twitter / linkedin / maimai / youtube):
- twitter (15): thread, timeline, list-add, bookmark-folders, following, list-tweets, bookmarks, list-remove, tweets, bookmark-folder, likes, lists, trending — direct 4-line replacement (cookie was outside `page.evaluate`); article, profile — hoisted ct0 read OUT of `page.evaluate` and threw `AuthRequiredError` upfront so unreachable in-evaluate auth branches got cleaned up too.
- linkedin/search.js — JSESSIONID was read inside the per-batch fetch loop's `page.evaluate`; hoisted once before the loop and pass `csrf` value into the template via `JSON.stringify`.
- maimai/search-talents.js — csrftoken cookie hoisted via getCookies; meta-tag fallback preserved inside `page.evaluate` (reached only when no cookie). Also converted the `page.evaluate(async (body) => …, body)` Playwright-style call to OpenCLI's template-string form so the helper actually runs.
- youtube — `SAPISID_HASH_FN` (used by like / unlike / subscribe / unsubscribe) reworked: sapisid is now passed in as a parameter; new `readYoutubeSapisid(page)` helper reads it via CDP. The HMAC-SHA1 compute still happens browser-side (Web Crypto), only the cookie read is hoisted.
Tests updated where mocks specifically referenced `document.cookie` (twitter following / bookmark-folder / bookmark-folders) to mock `getCookies` instead.
Verification:
- `npx tsc --noEmit` clean
- `npx vitest run clis/twitter clis/linkedin clis/youtube` → 264/264 pass
- typed-error-lint 189/189 (no new violations)
- silent-column-drop 103/103 (no new violations)
Scope notes (not in this PR):
- `goto + wait` redundancy and `browserSession: { reuse: 'site' }` rollout are scoped to follow-up PRs B and C per the #OpenCLI:3889b5cf thread plan.
- `document.cookie.match(...)` patterns (instagram 8 / xiaoe / qwen / hupu / tiktok / 1point3acres — ~13 files) are outside the original \`document.cookie.split\` audit scope and will follow as a Tier 1 expansion sweep.
* fix(adapters): read auth cookies by url scope
- bump opencli to 1.7.15 (was 1.7.14)
- extension stays at 1.0.9 (already bumped during the release cycle)
- finalize CHANGELOG: move Unreleased to 1.7.15 with date
Major release: Browser Agent Runtime project (Phase 0/1/2) — alignment
with vercel-labs/agent-browser model. CDP-primary input, AX snapshot/refs
with stale recovery, semantic locators across all primitives, full form
toolbelt (hover/focus/dblclick/check/uncheck/upload/drag/wait-download),
annotated screenshots, and same-origin iframe AX routing.
PR #1399 added auto-restart of stale daemons in BrowserBridge
(daemonVersion ≠ PKG_VERSION → restart). The browser-tabs e2e fake
daemon hard-coded `daemonVersion: 'test'`, so every test reported as
stale and the bridge tried to /shutdown the fake daemon — which has no
shutdown endpoint — causing all 4 tests in the file to exit with code 1.
This has been the failing signal in `e2e-headed (ubuntu-latest)` and
`e2e-headed (macos-latest)` on every main push since #1399.
Read PKG_VERSION from package.json once at module load and feed that to
the fake /status response. The fake daemon now matches the running CLI
so the stale-daemon path is not triggered.
Verification:
- npx tsc --noEmit clean
- npm run build clean
- npx vitest run --project e2e tests/e2e/browser-tabs.test.ts → 4/4 pass
* feat(dianping): resolve unknown cities live from www.dianping.com
The static CITY_ID map in clis/dianping/utils.js only covers ~20 cities,
so passing --city 汕头 (or any other Chinese name / pinyin slug not on
that list) fails with ArgumentError. Adding the missing cityIds by hand
doesn't scale to dianping's full city list and silently goes stale when
the site renumbers cities.
This change adds an async resolver that falls back to dianping.com when
the static map misses:
- Numeric input → pass through unchanged.
- Static map hit → fast path, no network (utils.CITY_ID untouched).
- Pinyin slug (e.g. "shantou") → goto /<slug>, parse cityId out of
any /search/keyword/{id}/ link rendered on the per-city landing page.
- Chinese name (e.g. "汕头") → goto /citylist, walk anchors to build a
Chinese-name → pinyin map, then resolve the slug as above.
Resolved (input → cityId) pairs are memoized per-process so repeat
searches skip both navigations.
Implemented as a new module (clis/dianping/cityResolver.js) so utils.js
stays minimal and the existing synchronous resolveCityId / CITY_ID API
keeps working for direct callers and tests.
Tested:
- Unit tests cover null/numeric/static fast paths, pinyin fallback +
cache, Chinese-name fallback via /citylist + cache for both forms,
rejection of garbage input, rejection of Chinese names not on
/citylist, and CommandExecutionError when the per-city page lacks
a /search/keyword/{id}/ link.
- JSDOM tests cover the pure DOM extractors (buildCitylistMap and
extractCityIdFromPage) against curated HTML fixtures.
- npm test: 3196 passed, 1 skipped (no new failures).
- npx tsc --noEmit: clean.
- opencli validate: 0 errors.
* fix(dianping): require city resolver links to be authoritative
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(douyin): handle empty response body in browserFetch (#1405)
browserFetch calls res.json() directly, which throws SyntaxError when
the API returns an empty body (content-length: 0). This happens when
the Douyin hashtag search endpoint returns HTTP 200 with no content.
Fix: read response as text first, return null for empty bodies, then
throw a descriptive CommandExecutionError at the caller level.
Fixes#1405
* fix(douyin): wrap browser fetch parse failures
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
Why
- `opencli twitter followers --help` rendered:
Arguments:
user
with a blank trailing column. Both humans and agents could not
recover the parameter's purpose without reading source. WAWQAQ
surfaced this directly: "没有说明当后面的 followers [user] [options]
如果都没填的时候,获取的是什么?"
- This is metadata completeness, not stylistic taste. Failing closed
is the only way to keep the help surface trustworthy as adapters
land.
What
- src/build-manifest.ts: add `findManifestMetadataIssues()` that flags
any positional with empty / whitespace-only / missing `help`. Wired
into `main()` after the import-failures gate; build aborts non-zero
with a per-arg report (`site/cmd positional "name" (sourceFile)`).
- src/build-manifest.test.ts: cover the gate (positives + negatives,
scoped strictly to positionals — named flags are intentionally
out-of-scope).
- 18 adapter offenders (16 required + 2 optional) get explicit help
text:
twitter: followers/following/list-add/list-remove/list-tweets/
search/thread
reddit: search/subreddit/user/user-comments/user-posts
douyin: stats/update
bilibili: subtitle
jike: search
Optional positionals (`twitter followers/following [user]`) now
document the omit semantics — fetches the currently logged-in
account.
- CHANGELOG: document the build gate and the offender list.
Out of scope (planned follow-ups)
- Semantic-quality advisory: optional positional help should also
contain `default / omit / current / logged-in / required unless …`
keywords. That belongs to the planned Arg metadata v2 work
(`when_omitted / when_present / value_format` 3-field schema).
- Named-flag `help` quality. Named flags carry the flag name itself
in help, so a missing `help` is not as opaque; if we want to gate
those too, do it as a separate, intentional decision.
Validation
- `npm run build` → 799 entries, clean.
- `npm run typecheck` → clean.
- `npx vitest run --project unit --project adapter` → 257 + 4 files,
all green (build-manifest 13 tests, manifest gate added).
- Smoke: temporarily reverted `followers.js` help to empty → build
aborts with the exact `twitter/followers positional "user" (...)`
line; restored, build is clean again.
- `npm run check:silent-column-drop` and `check:typed-error-lint`
baselines unchanged.
- bump opencli to 1.7.14 (was 1.7.13)
- extension stays at 1.0.6 (no extension changes since v1.7.13)
- finalize CHANGELOG with the three landed PRs:
* #1399 daemon restart on stale ready state for npm -g upgrade
* #1400 twitter write-action symmetry (unlike/retweet/unretweet/quote)
* #1401 agent-friendly adapter help (drop globally-shared option noise)
Fixes#1376 — YouTube transcript command failed with `No captions available for this video` for all videos.
## Root cause
Transcript adapter used InnerTube `/youtubei/v1/player` API with Android client context (`clientName: 'ANDROID'`, version `20.10.38`) to retrieve caption track URLs. YouTube has restricted/deprecated this approach; the Android client no longer reliably returns captions data.
## Fix
Replace Step 1 (caption track retrieval) with watch page HTML bootstrap parsing — fetch `/watch?v=...` with cookies and extract `ytInitialPlayerResponse.captions.playerCaptionsTracklistRenderer`. This is the same approach used by sibling `clis/youtube/video.js`, so it's an alignment to existing site-local stable pattern, not a new invention.
## 2 head iteration
- `cf77f5e8` initial fix (Step 1 caption retrieval switch + 18/18 unit tests)
- `bb30788c` lead test hardening — source-contract regression test in `transcript.test.js`:
- **positive lock**: must fetch `/watch?v=...`, parse `ytInitialPlayerResponse`, read `playerCaptionsTracklistRenderer`
- **negative lock**: must NOT use `/youtubei/v1/player` or `clientName: 'ANDROID'` (prevents regression)
- stale Android-InnerTube file header comment also updated
## Better-solution evaluation
- Official YouTube Data API captions surface (`developers.google.com/youtube/v3/docs/captions/download`) is owner-authorized API, NOT a public transcript replacement
- yt-dlp also relies on watch-page bootstrap path
- Existing `youtube/video.js` already uses the same `ytInitialPlayerResponse` extraction → this PR aligns transcript with stable site-local pattern instead of inventing a new path
## Typed failure / no-silent-empty boundaries
- watch HTML HTTP failure / missing `ytInitialPlayerResponse` / no `captionTracks` → `CommandExecutionError` (typed fail)
- Empty parsed XML → `EmptyResultError` (existing path, preserved)
- `Strategy.COOKIE` matches YouTube adapter family + `video.js`; cookies/session/consent unavailable → typed fail not silent empty success illusion
## Diff containment
Runtime change limited to Step 1 caption track discovery. XML fetch, segment parsing, chapters, raw/grouped formatting all unchanged.
## Verification
Local: YouTube adapter tests `19/19` (+1 from new test), `npm run build`, typed-error-lint `192/192`, silent-column-drop `103/103`, doc coverage `140/140`, `docs:build`, listing-id advisory unchanged `13`, `git diff --check`, merge-tree clean.
GitHub: build × 3 OS, unit × 2 shards, bun-test, adapter-test, audit, doc-coverage, docs-build all SUCCESS. PR CLEAN/MERGEABLE.
Author: kagura-agent (fork). Lead: codex-mini0. Aux: First-principles-0. Coordination: pr-monitor.
Xiaohongshu image-note publishing reliability fixes for creator center UI (legacy raw-Error write command, not a typed-error migration).
## 3 changes (one publish-path repair)
1. **Open creator publish in image mode**: append `target=image` to the publish URL so it loads directly in image mode instead of default
2. **Exact `图文` tab priority**: prefer exact tab text matching before broad `startsWith/includes`, reducing parent-container misclicks while keeping fallback for UI wording variants
3. **DataTransfer fallback for `Chrome Not allowed`**: when CDP `setFileInput` returns the permission/bridge denial error, fall through to the existing DataTransfer upload path (CDP-first remains primary to avoid base64 bridge/payload limits)
## Lead hardening (`edf8107d`)
Added `clis/xiaohongshu/publish.test.js` regression coverage for all three claimed behaviors:
- `target=image` creator URL locked
- exact tab text matched before broad fallback
- `Chrome Not allowed` falling into DataTransfer path
## Better-solution evaluation (lead + aux 一致)
- **CDP-first kept**: CDP avoids base64 payload/bridge limits; `Not allowed` is a known permission failure class where fallback is appropriate. DataTransfer-first would weaken the common path and reintroduce large-payload fragility.
- **Exact tab text first**: XHS creator markup is private and volatile, selector-only alternative not clearly more stable. Exact text reduces misclicks while broader fallback + post-click `video_surface` check preserve resilience for wording shifts. If exact text disappears, command fails fast with screenshot instead of silent video-mode publish.
- **Scope boundary self-imposed**: not expanding to typed-error migration (publish.js is legacy raw-Error and typed-error-lint already accounts for it).
## Verification
Local: xiaohongshu publish tests `12/12`, typecheck, build/manifest, docs:build, typed-error-lint `189/189`, silent-column-drop `103/103`, doc coverage `140/140`, node --check, git diff --check.
GitHub: build × 3 OS, unit shards, bun-test, adapter-test, audit, docs-build, doc-coverage all SUCCESS. PR CLEAN/MERGEABLE.
Author: E2ern1ty (fork). Lead: codex-mini1. Aux: First-principles-1. Coordination: pr-monitor.
* chore(release): pre-release P0/P1 cleanup
P0 fixes:
- delete src/analysis.ts (179 lines, 0 importers across src/clis/extension)
- remove dead OPENCLI_DIAGNOSTIC negative test assertion
- rename OPENCLI_BROWSER_TIMEOUT to OPENCLI_BROWSER_IDLE_TIMEOUT — the env
controls workspace lease idle release, not command runtime; old name was
misleading and undocumented (no fallback needed)
- add 'fill' to validate.ts KNOWN_STEP_NAMES so adapters using PR #1222's
fill pipeline step do not trip "unknown step name" warnings during validate
P1 fixes:
- BrowserConnect daemon-not-running hint: replace stale "make sure port is
available" with actionable "run opencli doctor / opencli daemon restart"
- TimeoutError hint: lead with --timeout flag, demote env var to secondary
* fix(validate): derive step allowlist from pipeline registry
@pr-monitor flagged the prior "add 'fill' to KNOWN_STEP_NAMES" fix as
treating only the symptom — two parallel hand-maintained lists will keep
drifting whenever a new pipeline step is registered.
Address the root cause: pipeline/registry.ts now exports
`getRegisteredStepNames()` and validate.ts builds KNOWN_STEP_NAMES from
that. Adding a step via `registerStep()` automatically allowlists it.
* test(validate): regression guard for pipeline step allowlist linkage
@pr-monitor follow-up: lock the validate ↔ pipeline registry linkage at
the test layer so future drift is caught immediately.
Changes:
- recompute KNOWN_STEP_NAMES per-call (was const at module load) so
steps registered after validate.ts import (plugins, dynamic registration)
are honoured
- add src/validate.test.ts with 3 cases:
1. every step name from getRegisteredStepNames() exists
2. an adapter using every currently registered step does not warn
3. a step registered at runtime is automatically allowlisted by
validate without any source change to validate.ts
* fix(capabilityRouting): add fill to BROWSER_ONLY_STEPS
Same double-list drift pattern as validate.ts KNOWN_STEP_NAMES (audit
follow-up flagged in this PR's evolution thread). The fill step was
registered in pipeline/registry.ts (PR #1222) but never added to the
browser-only allowlist in capabilityRouting.ts.
Concrete impact:
- shouldUseBrowserSession() didn't recognize a `[{ fill: ... }]` pipeline
as needing a browser, so PUBLIC adapters using fill could end up
without a page and crash inside stepFill at `page!.fillText(...)`
- pipeline/executor.ts's per-step retry policy (BROWSER_ONLY_STEPS gets
2 retries on transient errors, others get 0) skipped fill — losing
retry coverage on a DOM-touching step
Fix:
- add 'fill' to BROWSER_ONLY_STEPS
- add a documenting comment explaining BROWSER_ONLY_STEPS is the
browser-touching subset of registered steps (not the full set)
- export _validateBrowserOnlyStepsAgainstRegistry() so the test layer
catches the inverse drift (browser-only step that no longer exists)
- 3 new tests in capabilityRouting.test.ts:
* pipeline with fill routes to browser session
* BROWSER_ONLY_STEPS subset of registered step names
* fill is in both lists
This addresses @pr-monitor follow-up #3 (audit similar double-list
patterns) for the obvious in-scope candidate. Other candidates outside
this PR's scope: build-manifest serialization vs registry shape, error
code unions vs lint baselines.
* test(validate): use Strategy.PUBLIC enum instead of string cast in regression test
Self-review nit: `strategy: 'public' as never` worked but bypassed the
typed CliOptions union. Use `Strategy.PUBLIC` so the test exercises the
real public API.
Wire up the standard browser-LLM command surface for Yuanbao, matching the
recently shipped chatgpt + claude + qwen baselines:
- status — login + current model + (agentId, convId) + URL
- read — render the visible conversation as User/Assistant rows
- detail — open `<agentId>/<convId>` and read its messages
- history — list sidebar conversations with stable IDs
- send — fire-and-forget, returns once the send button has been clicked
Refactor `ask.js` to share helpers (`sendYuanbaoMessage`, `normalizeBooleanFlag`)
with the new commands via `shared.js`, keeping the public ask behavior intact.
Notable bits:
- `parseYuanbaoSessionId` accepts only full chat URLs or `<agentId>/<convId>`
pairs — Yuanbao chat URLs encode both, and silently opening the wrong agent
on a bare UUID is a worse failure mode than throwing. URL regex anchored
with `(?:[/?#]|$)` so 37+ char tails reject rather than truncate.
- `sendYuanbaoMessage` polls the send button (up to 3s) for the React
re-render that drops `style__send-btn--disabled___*` after composer input —
a fixed wait raced the debounce and produced silent no-op clicks.
- `getYuanbaoMessageBubbles` uses `data-conv-id`/`data-conv-idx`/
`data-conv-speaker` attributes for stable per-turn identity (was relying
on innerHTML alone).
- Status surfaces both human label (`Yuanbao`) and `dt-model-id`
(`hunyuan_gpt_175B_0404`) — sentinel strings would silently look like a
real model name; null is the typed-unknown signal.
Verified: 25 unit tests pass; targeted live smoke for status/read/detail/
history/new/send + ask round-trip on yuanbao.tencent.com.
* feat(qwen): add detail command + fix stale message bubble selector
`getMessageBubbles` was matching `[data-msgid="<id>-question|answer"]` from an
older Qianwen frontend. The reshipped DOM no longer carries that attribute on
chat turns; `[data-message-id]` now lives on citation cards inside assistant
responses, so the old selector silently returned an empty list and `qwen read`
had been silently broken.
Rewire to walk `[data-chat-question-wrap]` and `[data-chat-answers-wrap]` in
DOM order (correct Q/A interleaving) and synthesize stable IDs from the
nearest sibling `data-req-id` so `waitForAnswer.seenAssistantId` and
read/ask/detail dedupe paths keep working. Verified live against an existing
conversation: 3 user turns + 3 assistant turns extracted; old selector
returned 0.
`qwen detail <id|url>`: open a specific conversation by ID or full chat URL,
poll up to 20s for the transcript to render, return Role/Text rows. Adds
`parseQianwenSessionId` (5 unit tests covering ID/URL parsing + ArgumentError
on malformed input). Reuses the same site-level browser session as `read`/
`ask` so consecutive calls continue in the same Qwen tab.
- clis/qwen/detail.js (new)
- clis/qwen/utils.js (parseQianwenSessionId + getMessageBubbles rewire)
- clis/qwen/utils.test.js (new)
- docs/adapters/browser/qwen.md (detail entry + options/columns)
- cli-manifest.json (regenerated)
* fix(qwen): anchor URL regex to reject 33+ hex tail truncation
codex-coder review on PR #1390 caught that
`https://www.qianwen.com/chat/<33+ hex>` would silently truncate to the
first 32 chars and open the wrong conversation. Adds end-of-input /
slash / query / fragment boundary to the URL match group and two new
unit-test cases (digit tail + letters tail) covering the truncation gap.
Add ChatGPT web ask/send/read/history/detail/new/status alongside existing image support. Tighten ChatGPT web helper selectors and typed error contracts, update docs/changelog, regenerate manifest, and seed local ChatGPT verify fixtures for ask/read.
* test(gov-policy): JSDOM-against-frozen-fixture tests for in-browser extractors
Applies the pattern documented in skills/opencli-adapter-author/references/jsdom-fixture-pattern.md
(introduced in #1319 alongside the dianping reference test in #1313) to the
gov-policy adapter.
Refactor: the inline IIFE inside `page.evaluate` template literal is hoisted
to a top-level `extractSearchRows` / `extractRecentRows` function using bare
`document` / `location`. Same code now runs identically in:
- the live browser (injected via `${extractor.toString()}`)
- JSDOM unit tests (with `globalThis.document` / `globalThis.location` swapped)
Tests:
- 6 new cases in clis/gov-policy/gov-policy.test.js (was commands.test.js).
- 3 representative search result cards (1 with real article snippet, 2 with
only publish-time in `.description`) and 5 recent listing rows in the
fixtures.
- ok:false fallback path covered for both extractors.
- Lock-in: `要闻` type-tag prefix fusion in title and empty-source contract
on recent listings (no `.source` / `.from` elements on that page) are
asserted explicitly so a future selector tweak can't silently change them.
Reverse-validated against two buggy variants per the reference doc:
breaking the title selector and stripping the `要闻` prefix both fail the
JSDOM assertions with helpful diffs.
Fixture sanitization follows the reference doc step-by-step: scripts /
styles / iframes / comments / preload links stripped, image srcs replaced
with `placeholder.png`, trimmed to the minimum subtree that exercises the
extractor (3 search items, 5 recent rows), all whitespace-only lines
removed.
* fix(gov-policy): use typed errors for touched commands
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* enrich(coupang): add product detail cmd + replace silent clamp/sentinel/Error with typed errors
Two enrichment changes plus three silent-failure fixes on top of existing
search / add-to-cart.
New cmd: coupang product
─────────────────────────
Pairs with search as the listing↔detail round-trip target. Reads a logged-in
product page and extracts a single canonical row with price, original_price,
discount_rate, rating, review_count, seller, brand, rocket, delivery_promise,
image_url, url. Three-source extractor (JSON-LD Product schema → bootstrap
globals → DOM) merged in priority order, mirroring the search.js pattern.
The columns use string|null typing — null means "upstream did not provide
this field on this product" (e.g. some items have no original_price).
Failures (login wall / page mismatch / page failed to render) raise typed
errors instead of silently returning empty rows, so callers can treat any
returned row as real data.
Search column shape: added product_id
─────────────────────────────────────
Listing must pair with detail by id. The data was already extracted by
normalizeSearchItem; only the columns array needed updating so the field
projects through to the rendered row. Per the listing-id-pairing convention
(PR #1297) the new column lets agents round-trip rows directly into
`coupang product` without re-scraping URLs.
Silent-failure fixes
────────────────────
1. search --limit silent clamp.
Old: `Math.min(Math.max(Number(kwargs.limit||20),1),50)` silently
rewrote `--limit 999` to 50 and `--limit 0` to 1.
New: `parseLimitArg(raw, 20, 50)` throws ArgumentError on out-of-range
/ non-integer / negative input. Same convention as the typed-fail-fast
memory & PR #1289.
2. search --page silent clamp.
Old: `Math.max(Number(kwargs.page||1),1)` silently lifted negative pages.
New: parsePageArg throws ArgumentError on non-positive input.
3. Generic `throw new Error(...)` → typed errors.
- Empty query, unsupported --filter, missing --product-id/--url
→ ArgumentError
- Login wall detection → AuthRequiredError('coupang.com', ...)
- Empty result / filter-not-rendered → EmptyResultError
- PRODUCT_MISMATCH / OPTION_REQUIRED / button-not-found / unknown
ack failure (add-to-cart) → CommandExecutionError
- The PRODUCT_MISMATCH and `actualProductId || 'unknown'` sentinel were
also fixed (silent-sentinel was the audit hit there).
Coverage
────────
- 21 contract assertions in clis/coupang/coupang.test.js covering
parseLimitArg / parsePageArg (no silent clamp), registry shape (search has
product_id, product is read-class with expected columns, add-to-cart is
write-class), and typed-error pre-flight rejections (empty query / bad
filter / out-of-range limit & page / missing detail args).
- Manifest 763 → 764 (+1 entry: coupang/product).
- Audits: typed-error-lint 196 → 194 (resolved 2 silent-clamp/sentinel
baseline entries; baseline updated). silent-column-drop 103/103 unchanged.
* fix(coupang): tighten product id and browser errors
* fix(coupang): require real product urls
* refactor(linux-do): remove deprecated hot/category/latest compat shims
The three shims have been pure backward-compat wrappers since linux-do/feed
became the unified entrypoint. With no stable release commitment to preserve,
they are pure surface cost: 3 manifest entries, 3 deprecated branches in help
output, and a `buildLinuxDoCompatFooter` helper that exists only to feed them.
- delete clis/linux-do/{hot,category,latest}.js
- drop now-orphaned `buildLinuxDoCompatFooter` from feed.js and unexport
`executeLinuxDoFeed` (no external consumers remain)
- remove the Compatibility section in docs/adapters/browser/linux-do.md
- regenerate cli-manifest.json (-125 lines)
BREAKING CHANGE: `opencli linux-do hot|category|latest` are removed. Use
`opencli linux-do feed --view top --period <period>`,
`opencli linux-do feed --category <id-or-name>`, and
`opencli linux-do feed --view latest` instead.
* fix(linux-do): finish compat shim removal
* refactor(runtime): unify command timeout into a single --timeout arg
Drop the cli-level `timeoutSeconds` build-time ceiling field. A command
now opts into runtime-enforced timeouts purely by declaring an arg named
`timeout`; the user-facing `--timeout` value (its default or override)
is the single authoritative knob, used both by the adapter polling loop
and by the runtime ceiling (with a 30s padding for return + closeWindow
+ trace export).
Behavior:
- Browser commands without a `--timeout` arg fall back to
OPENCLI_BROWSER_COMMAND_TIMEOUT (default 60s, unchanged).
- Non-browser commands without a `--timeout` arg now run unbounded
rather than against the previously implicit `timeoutSeconds` cap.
Affected commands keep their old caps via newly added `--timeout` args.
- LLM adapters (gemini/claude/deepseek/doubao/qwen/yuanbao ask) keep
their current `--timeout` defaults; the runtime ceiling is now strictly
more generous (userTimeout + 30s vs. the previous 180s cap), so
`--timeout 600` actually buys 600s of polling rather than dying at 180s.
Closes the design discussion that started from PR #1227, which proposed
a per-site `OPENCLI_GEMINI_ASK_TIMEOUT` env var to work around the same
underlying mismatch.
* fix(timeout): wire --timeout arg into chatgpt/gemini image adapter polling
codex-coder review on PR #1364 caught that the new --timeout arg I added
to chatgpt/image and gemini/image only drove the runtime ceiling — the
adapter still hardcoded `const timeout = 120`, so users passing
--timeout 240/600 saw runtime allow 270s/630s but the adapter stop
polling at 120s. That recreated the same single-knob mismatch this PR
was meant to delete.
Also add the browser-path runWithTimeout assertion codex-coder flagged
as missing: a browser command with --timeout default=5 must call
runWithTimeout with timeout: 35; a browser command without --timeout
arg must fall back to DEFAULT_BROWSER_COMMAND_TIMEOUT.
Image adapters now read kwargs.timeout and reject non-positive-integer
values with ArgumentError (no silent fallback). chatgpt/image.test.js
updated to pass an explicit timeout when calling .func directly (the
test bypasses arg coercion).
* fix(runtime): reject invalid timeout ceilings
* fix(timeout): normalize timeout args to integer values
* fix(timeout): preserve remaining command ceilings
* fix(runtime): validate timeout before browser setup
* enrich(toutiao): hot board + bug fixes (silent column drop, partial render)
Per WAWQAQ "丰富现有 adapter" pivot — Phase 2 site #3.
## New command
- `toutiao hot` (Strategy.PUBLIC, browser:false) — public homepage hot
board via the toutiao.com hot-event/hot-board endpoint. No login required.
Returns 8 stable columns (rank/id/title/query/hot_value/label/url/image).
## Bug fixes for `toutiao articles`
- **Silent column drop fixed**: `parseToutiaoArticlesText` previously
did `if (title && stats) push(...)`, silently dropping any row where
the stats span hadn't finished rendering by the time page.innerText
was read. Slow-render bugs were invisible — adapter looked "complete"
while writers saw extra rows in the dashboard. Partial rows now
surface with `null` stat columns.
- **Silent clamp on `--page` removed**: out-of-range / non-integer
values raise `ArgumentError` with explicit bounds [1, 4]. Same
validation reused by both `articles` and `hot` via `parseArticlesPage`
/ `parseHotLimit` in `utils.js`.
- **Empty result typed**: zero-row scrape now raises `EmptyResultError`
instead of returning `[]` silently (would otherwise look like a
legitimate "no articles" response).
## Refactor
- Parser logic extracted to `clis/toutiao/utils.js` (alongside hot-row
mapping, validators, and the hot-board URL constant).
- `articles.js` switches from declarative `pipeline:` to imperative
`func` form so `parseArticlesPage` validation can run before the
navigation step (declarative pipeline can't pre-validate args).
- Strategy is now explicit: `Strategy.COOKIE, browser: true` for
articles (creator dashboard is logged-in only).
## hot field map
`ClusterIdStr` (or numeric `ClusterId`) → id; `Title` → title;
`QueryWord` → query (falls back to title); `HotValue` → hot_value
(non-negative numeric, else null); `Label`, `Url`, `Image` →
respective columns. `pickImage` walks `Image.url` → first truthy
`Image.url_list[]`. Empty-title rows are dropped (returns null) before
ranks are densely re-assigned 1..N.
## Tests
29 contract assertions across `parseArticlesPage` / `parseHotLimit` /
`parseToutiaoArticlesText` / `mapHotRow` + registry-level shape checks
+ `hot` adapter func behaviour (typed errors / no silent clamp / fetch
failure paths / dense-rank).
## Audits
- typed-error-lint: 196 = 196 (unchanged baseline)
- silent-column-drop: 103 = 103 (unchanged baseline)
- listing-id-pairing: hot has `id` column (round-trippable when a
detail command lands later); advisory list unchanged.
## Manifest
757 → 758 entries (+1 for `hot`).
## Doc
- index.md: toutiao mode 🔐 → 🌐/🔐 (hot is public, articles is logged-in)
- toutiao.md: per-command mode/domain table + column docs + prerequisites
* fix(toutiao): tighten hot and articles contracts
* fix(linkedin): surface detail_error on --details (no silent catch / no silent empty)
The previous --details enrichment path had two indistinguishable failure modes
that both produced `description: '', apply_url: ''`:
1. `if (!job.url)` early return — row had no jobId, so we couldn't navigate.
2. `} catch {}` — page.goto / page.evaluate threw (network, timeout, parse error).
Callers couldn't tell "upstream had no description" from "we failed to fetch",
and the catch swallowed every error without logging. For an enrichment that
costs one page navigation per row, silent failure is especially harmful — users
just see an empty cell with no way to debug.
Fix: replace empty strings with `null` for missing/failed rows, add a new
`detail_error` column (string|null) carrying a short typed reason:
- 'no url' — row had no jobId
- 'fetch failed: <msg>' — page.goto / page.evaluate threw
- 'missing description' — page loaded but body was empty
- null — success
Every failure is also logged to stderr with the offending URL so debugging is
possible. Per-row failures still don't abort the batch (the original intent),
but they're now visible.
Tests: 13 new contract assertions in clis/linkedin/search.test.js covering
parseCsvArg, mapFilterValues (ArgumentError on unknown values), decodeLinkedinRedirect,
and 5 enrichJobDetails paths (no-url / goto-throw / empty-description / success /
multi-row-mixed). Added `export const __test__` for testability.
Audits clean: typed-error-lint 196/196, silent-column-drop 103/103.
* fix(linkedin): fail fast on auth walls
* enrich(ctrip): hotel-suggest + bug fixes (silent clamp, dropped columns, fake URL)
Per WAWQAQ "丰富现有 adapter" pivot — Phase 2 site #1.
## New command
- `ctrip hotel-suggest` — surfaces hotel-context suggestions (cities,
business areas, individual hotels) via the same backing endpoint with
searchType=H. Distinct from `ctrip search` (searchType=D) which returns
destinations / scenic spots / railway stations.
## Bug fixes for `ctrip search`
- **Silent clamp on `--limit` removed**: out-of-range values (≤0, ≥51,
non-integer) now raise `ArgumentError` with explicit bounds rather than
silently snapping to [1, 50].
- **Silent column drop fixed**: previously the adapter discarded `id`,
`cityId`, `cityName`, `provinceName`, `countryName`, `lat`, `lon`, `eName`
and `displayType` from upstream rows. Now all are surfaced as stable
columns.
- **Fake URL fixed**: previously `url` was always `''`. Now constructs
canonical Ctrip URLs by `type` (City / Markland / Hotel / Zone / RailwayStation)
and returns `null` (no silent fabrication) for unknown types.
- **In-band error envelope typed**: `Result: false` payloads now surface
as `COMMAND_EXEC` (was previously not handled — adapter returned empty
rows).
## Doc fix
- `Mode: 🔐 Browser` → `🌐 Public` (search uses public API, no login)
- Add `hotel-suggest` to commands table in both `docs/adapters/index.md`
and `docs/adapters/browser/ctrip.md`.
## Coords picker
Mainland China rows ship `gdLat`/`gdLon` (gaode); international rows ship
`gLat`/`gLon` (wgs84). Adapter picks the first non-zero pair (zero is the
upstream sentinel for "missing"); returns `null` if all variants are zero.
## Tests
25 contract assertions across `parseLimit` / `pickCoords` / `buildUrl` /
`mapSuggestRow` + registry-level checks for both commands (Strategy /
shape parity / typed errors / no silent clamp).
## Audits
- typed-error-lint: 196 = 196 (unchanged baseline)
- silent-column-drop: 103 = 103 (unchanged baseline)
- listing-id-pairing: advisory only (search has `id` round-trip column)
## Manifest
757 → 758 entries (+1 for `hotel-suggest`).
* fix(ctrip): wrap suggest fetch and json failures
Closes#1334.
Exposes viewport overrides for `opencli browser screenshot` so an adapter or
ad-hoc shell user can render a page at a fixed width and capture the full
scrollable height. The ljg-card HTML to PNG pipeline use case.
Behavior:
- `--width W` only overrides device-metrics width; height is left unchanged.
- `--height H` only overrides height (ignored under `--full-page`).
- `--full-page` keeps the existing `captureBeyondViewport` shortcut.
- `--full-page --width W` first reflows at W, then re-overrides to (W, contentH)
so the captured image reflects the layout at the requested width.
- Override is always cleared in `finally`, including on capture failure.
* feat(deepseek): add detail and send commands for explicit conversation control
doubao already ships `detail <id>` and `send` for ID-explicit conversation
read/write; deepseek had only `read` (current page only) plus the
implicit-resume `ask`. Adding both gives users a stable handle when they
know the conversation ID, without going through `ask`'s resume detection
or its full prompt-then-wait pipeline.
`deepseek detail <id>`:
- parses a bare UUID or any URL containing `/a/chat/s/<id>`,
- rejects malformed input via `ArgumentError` before any browser
navigation,
- navigates to `https://chat.deepseek.com/a/chat/s/<id>` and returns
the visible message list,
- throws `EmptyResultError` when the conversation has no rendered
messages.
`deepseek send <id> <prompt>`:
- takes the conversation id as a required positional, because the
framework runs each browser command in an ephemeral per-command
workspace (a fresh tab) and there is no shared "current conversation"
across commands; the navigation must be explicit,
- drives input through CDP `Input.insertText` via `page.nativeType`,
mirroring the doubao adapter (#1278); `execCommand('insertText')` plus
a synthesised input event leaves the React-controlled state desynced
on a freshly-opened tab and the resulting click silently no-ops,
- keeps the verification loop inside the same `page.evaluate` so the
framework cannot close the tab mid-flight; counts user-class bubbles
by text-match (DeepSeek virtualises the message list, so a numeric
bubble-count check is unreliable),
- throws `CommandExecutionError` with a specific reason when the
textarea did not populate, the send button stayed disabled, the
bubble never settled, or the optimistic render rolled back during
a 3s settle window,
- treats "Promise was collected" from the post-click eval as success,
matching the existing pattern in `ask --file`.
Helper `parseDeepSeekConversationId` is exported from utils.js so the
same parser feeds both commands and round-trips the canonical lower-case
ID.
Tests:
- utils.test.js: 5 cases covering bare UUID, upper-case
normalisation, URL extraction with and without query string, empty /
null / whitespace input, and non-UUID rejection.
- detail.test.js: 5 cases covering registration, navigation +
message return, URL normalisation, ArgumentError before browser
navigation, and EmptyResultError on no-messages.
- send.test.js: 7 cases covering registration, ArgumentError on bad
id, full happy-path through nativeType + IIFE verification, the
textarea-mount timeout, missing nativeType helper, focus failure,
IIFE-reason translation to CommandExecutionError, and the
"Promise was collected" success path.
Manifest auto-regenerated to register both commands.
Live-verified end-to-end against my own DeepSeek session:
- `detail` returns the canonical message list for a bare UUID, parses
a full chat URL, and rejects malformed IDs before any browser
navigation,
- `send` lands the prompt as the latest user message in the target
conversation and gets an AI response back; reload of the
conversation page in a separate tab confirms the message persisted
server-side.
* docs(deepseek): document detail and send commands
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
Keep the owned automation container window warm across lease release. Non-final owned leases close their tab; the final owned lease resets its tab to about:blank as a reusable placeholder. Update browser close wording to describe lease release rather than window closure.
Closes#1342.
`opencli deepseek ask` (without --new) used to resume the most recent
conversation by clicking the first `a[href*="/a/chat/s/"]` in DOM order
after a fixed 2-second wait.
Two bugs:
1. Pinned conversations sit in their own DOM section ("置顶") that
renders above "30 天内" and friends. Click-first-anchor lands on the
pinned thread, not the user's most recent. Reproduced live by
pinning a conversation through the sidebar context menu and
observing that the existing logic targets it instead of the most
recent non-pinned thread.
2. The 2s wait is fixed. On a slow network the sidebar has not
populated yet, the click is a no-op, and `ask` silently falls
through to the new-chat path. The user typed "follow up" and
a brand-new conversation gets created.
Replace the click-first-anchor + fixed wait with a new helper
`pickResumeUrl(page)` in utils.js that:
- polls the sidebar for up to 10s (5 attempts × 2s),
- identifies pinned anchors by a text-based check on the section
header (`/^\s*(置\s*顶|Pinned)\s*$/i`); DeepSeek's CSS-module
class names are randomized per build, so the text is the only
stable signal,
- returns the URL of the first non-pinned anchor (or falls back to
the first overall if every visible anchor is pinned),
- returns null if no anchor surfaces in time.
`ask.js` calls the helper and `page.goto`s the returned URL. When the
helper returns null, `ask` now throws a `CommandExecutionError`
instead of silently navigating to a fresh chat. The user gets a clear
"pass --new" hint and their prompt is never sent to a wrong target.
Tests:
- utils.test.js: 4 cases covering happy path, polling-then-success,
timeout returns null, and a structural assertion that the embedded
DOM walker uses text-based pinned detection.
- ask.test.js: replaced the prior "still selects model when no
conversation to resume" test (which exercised the silent
fall-through) with a fail-fast assertion. Updated the resume-success
test to mock the new helper.
* feat: 11 read adapters across 8 sites (dblp / steam / bbc / devto / lobsters / medium / coingecko / hf)
Round 2 of the adapter expansion sweep. All 11 commands hit public APIs (no
browser, no auth), follow the post-#1332 typed-error / no-silent-failure
discipline, and were live-verified against real endpoints.
New adapters:
- dblp/author : recent publications for one author (resolve PID by name, or pass --pid)
- steam/search : storefront name search (storesearch API)
- steam/app : single app detail (appdetails API; HTML entities decoded)
- bbc/topic : per-topic RSS (8 canonical BBC News feeds)
- devto/latest : /api/articles/latest with --page pagination
- lobsters/domain : stories from a specific source domain (/domains/<d>.json)
- medium/tag : tag RSS (description full-length, no silent truncation)
- coingecko/exchanges : trust score + 24h BTC volume leaderboard
- coingecko/categories : sector buckets with 6 sort options
- coingecko/global : aggregate market totals + BTC/ETH dominance
- hf/paper : single-paper detail by arXiv id (summary, ai_summary, ai_keywords, upvotes)
Also adds clis/steam/utils.js + clis/bbc/utils.js as shared helpers (HTML entity
decode, RSS parsing). All listings carry a round-trippable id where a detail
sibling exists; advise:listing-id-pairing reports zero new violations. typed-
error-lint and silent-column-drop gates both unchanged from baseline.
Manifest: 698 → 709 (+11 entries).
* fix: tighten adapter round2 contracts
* Add uisdc news adapter for CLI
Implements a CLI adapter for fetching the latest AI/design news from uisdc.com. Allows specifying the number of news items to return.
* feat(aibase): add aibase daily news adapter
This file implements a news adapter for AIbase that fetches the latest AI industry news and allows for configurable limits on the number of news items returned.
* fix(news): harden uisdc and aibase adapters
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
Document the pattern for running opencli on a remote machine while keeping
the daemon and Chrome on the local machine. Reverse-tunnel local 19825
back to the remote (via SSH -R or frp) so the remote opencli still talks
to its own loopback and the daemon never leaves localhost.
Captures the rationale we landed on after reviewing #636: native
extension-to-remote-daemon support is deferred until the daemon protocol
gains authentication; in the meantime this is the safe, zero-code path
that achieves the same outcome.
* feat: add tiktok creator-videos command
TikTok Studio creator content list with views/likes/comments/saves/shares.
Hits the Studio item_list endpoint
(https://www.tiktok.com/tiktok/creator/manage/item_list/v1/?aid=1988) from a
logged-in /tiktokstudio/content session and pages with cursor until limit is
satisfied (server caps size at 50). Username for the resulting video URL is
extracted from the user_text= query param on play_addr / download_info entries,
falling back to scraping a[href*="/video/<id>"] from the Studio page DOM.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tiktok): regen manifest + replace silent-clamp with ArgumentError
- Regenerate cli-manifest.json (CI gate: must match `npm run build` output)
- Replace `Math.max(1, Number(args.limit) || 20)` and
`Math.min(Math.max(limit, 1), 50)` with an explicit positive-integer
guard + a server-cap-only ternary, per the silent-clamp guidance in
references/typed-errors.md (typed-error-lint baseline is unchanged)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tiktok): tighten creator videos contract
---------
Co-authored-by: root <root@example.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
Per WAWQAQ feedback in #OpenCLI thread on the flat "Site adapters (112)" listing:
the bucket conflates real web sites (bilibili, dianping, ...) with desktop apps
(chatgpt-app, chatwise, codex, cursor, discord-app, doubao-app, antigravity, notion).
Group them so agents that fall back to --help can scan by category.
Three buckets, sourced from existing metadata only — no new adapter schema:
- External CLIs: passthrough binaries from loadExternalClis() (docker, gh, vercel, ...)
- App adapters: domain is `localhost` or any non-DNS string (no `.`)
- Site adapters: domain contains `.` (real DNS), or domain is unset (default)
The classifier is one line: `domain.includes('.') ? 'site' : 'app'`. Adapters
without a domain field default to site (most are public web scrapers like
arxiv / wikipedia / spotify / ...).
Verified against the live registry: 7 External CLIs, 8 App adapters
(antigravity, chatgpt-app, chatwise, codex, cursor, discord-app, doubao-app,
notion), 104 Site adapters.
Structured help (-f yaml/json) gains parallel `external_clis` / `app_adapters`
/ `site_adapters` keys; `commands` no longer leaks adapter names.
External CLIs are now hidden from the default Commands listing (mirrors how
site adapters were already filtered) and surfaced in their own section.
- Add clis/test-utils.js with standard createPageMock utility
- Migrate 11 test files to use shared utility (removes ~300 lines of duplication)
- Delete extension/src/cdp.test.ts dead skip test (chrome.scripting.executeScript removed from source)
- Remove clis/pixiv/test-utils.js (superseded by shared utility)
Codify the JSDOM-against-frozen-fixture pattern that PR #1313 introduced
for dianping (and that PR #1318 had to follow up to clean up). The skill
previously had no reference for this category of test, so authors of the
next adapter that hits silent-in-browser-DOM bugs would either reinvent
it or skip it.
Key conventions captured:
- **Mandatory awk 'NF>0' as the final step of fixture creation.** The
blank-line noise that PR #1318 removed (84.6% / 54.8% of file content
in dianping/{shop,search}.html) came from manually stripping
script/style content without collapsing the surrounding newlines.
Skipping this step is the silent quality regression that the next
fixture author would also hit.
- **Trim-to-minimum but never re-flow content.** Some bugs depend on
text-node adjacency without intervening whitespace
(dianping #1312 bug #2: rating "4.8" + reviews "21241条" fused as
"4.821241条"). Pretty-printing the meaningful mega-line would mask
the very condition the test is meant to catch.
- **Reverse-validate the regression guard.** "18/18 tests pass" only
proves agreement with the current implementation, not that the test
would have caught the original bug. Reintroducing the buggy variant
must make the test fail — otherwise the fixture is over-stripped or
the assertion is too loose.
- **__fixtures__/ is the documented exception** to the "no committed
HTML dumps" rule in the skill's "关键约定". Calling that out
explicitly because the rule otherwise reads as "all HTML in repo is
bad," which the dianping fixture pattern intentionally violates for
a real reason.
Background: WAWQAQ in #1313 follow-up thread (`#OpenCLI:36d2f65a`) asked
twice — first about the visible blank-line noise (→ PR #1318 cleanup),
then about the root cause and what should improve in the workflow itself.
This is the workflow improvement.
No new tooling / CI gate / lint introduced (B 1-week gate freeze still
applies). When a fifth fixture site adopts this pattern,
`opencli browser fixture-snapshot` automation can be revisited; until
then, runbook discipline + skill reference is the right scope.
dianping/__fixtures__/search.html and shop.html came out of page.content() with
hundreds of blank lines that JSDOM ignores during parsing — pure visual / disk
noise that bloats reviewer diff and obscures the meaningful DOM subtree the
fixture freezes.
search.html: 372 → 168 lines (-204 lines, -572 bytes)
shop.html: 39 → 6 lines (-33 lines, -64 bytes)
`awk 'NF>0'` keeps every line that has any non-whitespace character, so the
minified mega-line (where the rating-vs-reviews adjacency that triggers #1312
silent fusion lives) is preserved verbatim. dianping.test.js 18 tests still
pass, and reintroducing the buggy `headText.match(/(\d+)条/)` extractor still
makes the regression guard fail with the expected '821241条' (proving the
fusion-bug detection power is intact after the strip).
Per WAWQAQ feedback in #1313 thread; opus independently validated the same
approach before the cleanup.
PR #1312 fixed two silent in-browser DOM bugs that the existing mocked
`page.evaluate` tests could not catch:
1. shop title fallback split on ASCII `[]` while dianping renders
full-width `【】`, so `name` was always empty (or `"undefined"`).
2. headText `\s+` collapse fused rating "4.8" with reviews "21241条",
so a head-wide `/\d+条/` regex captured "4.821241" → 5.
Both bugs only surfaced on live verify; mocked-evaluate unit tests fed
pre-baked results to the func and the real DOM walk never ran.
Make the in-browser extractor logic testable in CI:
- clis/dianping/shop.js, clis/dianping/search.js: extract the IIFE
bodies into top-level `extractShopFields()` / `extractSearchRows()`
using bare `document` / `location`. The live adapters inject these
via `page.evaluate(\`(\${fn.toString()})()\`)` so behavior is
unchanged; both commands re-verified end-to-end against live
dianping (shop returns name=芈重山老火锅(五道口店), reviews=21241,
rating=4.8; search returns 3 result-shaped rows with correct ids).
- clis/dianping/__fixtures__/shop.html (3.4KB), search.html (8.4KB):
sanitized HTML snapshots — scripts/styles/iframes/comments stripped,
img src placeholdered, only structural attributes kept. Trimmed to
the minimum subtree needed to exercise the extractors (search keeps
3 of 15 li cards; shop keeps .shop-head + .desc-info + .review-title
plus full-width 【】 title and headText with the rating/reviews
fusion preserved).
- clis/dianping/dianping.test.js: add a fifth describe block —
"extractors against frozen HTML fixtures" — that loads the fixtures
via JSDOM, swaps `globalThis.document` / `globalThis.location`, and
asserts the post-fix behavior:
* shop: name=芈重山老火锅(五道口店), reviewsRaw=21241条, rating=4.8,
breakdown={口味:4.8,环境:4.8,服务:4.8,食材:4.9}, hours, rank, subway.
* search: 3 rows with correct shop_ids, names, reviewsRaw, priceRaw,
starClass; round-trip through parseReviewCount/parsePrice mappers
to lock in {rating:5.0,reviews:21231,price:109} et al.
* ok:false branches: shop fixture without `.shop-head`, search
fixture with empty `#shop-all-list`.
Manually verified the fixtures would catch the original bugs by running
buggy extractor variants against shop.html — ASCII-bracket fallback
returns `name="undefined"`, and head-wide `/\d+条/` returns `821241条`
(both fail the new assertions).
Test suite grows 14 → 18 passing tests; live verify of both commands
still produces correct output post-refactor.
Pattern intentionally limited to dianping as a reference point. If other
sites with in-browser DOM extraction encounter similar silent bugs, this
JSDOM-against-frozen-fixture pattern can be adopted per-site.
* docs(cases): add three researcher workflow examples
Add use cases under cases/ that exercise the recently-landed
researcher-friendly adapters:
- daily-rl-research-monitor.md uses arxiv recent + openreview venue
+ hf top to compress a morning paper-skim into one shell pipeline.
- find-paper-implementation.md chains arxiv search/paper + dblp
search + hf top + openreview search to map a paper's canonical
record, follow-ups, and community uptake.
- track-conference-papers.md walks openreview venue + reviews to
shortlist accepted papers and digest review threads in batch.
Each file is a real workflow built on commands from #1289 (arxiv
recent), #1294 (openreview), and #1299 (dblp).
* docs(cases): correct venue ids and forum example to ones that return data
The first revision used "ICLR.cc/2026/Conference" and "ICLR 2026 oral"
as venue strings. Both return EMPTY_RESULT today because the venue is
not open. Update each case to use natural-language venue text that
OpenReview currently exposes ("ICLR 2024 oral", "NeurIPS 2025 oral")
and a real forum id (KS8mIvetg2, "Proving Test Set Contamination in
Black-Box Language Models") in the reviews / paper drill-down. Note
the arxiv free-text-search ranking quirk so the worked DPO example
makes sense.
PR #1297 introduced a CI gate that fails when a site has both a listing
and a detail command but the listing rows don't carry an id-shaped column.
The gate came with a 10-entry EXEMPT map (topic-string trending,
profile-attribute rows, UI-only sessions, ...) where each exemption
recorded a "why this listing legitimately doesn't pair" reason.
By the same filter that closed PR #1311 (write-without-delete-pair gate):
Is "listing should pair with detail" a *permanent* anti-pattern, or
case-by-case business judgment?
It's case-by-case. Topic-string listings and profile-attribute rows
genuinely don't pair with a detail command. The fact that we needed an
EXEMPT map with 10 entries and individual reason strings is the smell —
it's not the rule winning, it's the rule failing. Forcing every adapter
PR to either add an id column or file an exemption was a higher cognitive
cost than the silent-loss bugs the rule actually catches.
Changes:
- .github/workflows/ci.yml — drop the "Check listing↔detail id pairing"
step. Other gates (silent-column-drop, typed-error-lint) stay in place.
- package.json — rename the script from `check:listing-id-pairing` to
`advise:listing-id-pairing` to make the advisory nature explicit.
- scripts/check-listing-id-pairing.mjs — drop the `--strict` flag and the
EXEMPT map. The script now always exits 0 and prints an advisory report
of listings that don't carry an id-shaped column. Reviewers/authors use
it as guidance, not a gate.
- docs/conventions/listing-detail-id-pairing.md — rewrite from "MUST" to
"soft convention". Adds an explicit "why advisory, not a gate" section
that lists the legitimate non-pairing categories so future readers know
the rule's boundary.
- docs/developer/ts-adapter.md — match the advisory tone in the
adapter-author guidance.
The doc, the script, and the column patterns table all stay — agents and
adapter authors can still consult them. What's gone is the CI failure and
the per-PR exempt-list maintenance burden.
Net diff: -34 lines (gate + EXEMPT map removed, advisory-tone doc adds
a small "why advisory" section).
Adds a baseline CI gate for convention-audit typed-error lint findings. Also refreshes the silent-column-drop baseline for dianping changes already on main.
The merged adapter had two silent in-browser bugs that the mocked-evaluate
unit tests don't catch — only live verify against www.dianping.com surfaces
them:
1. Shop name returned `undefined`. The fallback parsed `document.title` with
an ASCII-bracket split (`/[\\[\\]]/`) but dianping wraps the name in
full-width brackets `【芈重山老火锅(五道口店)】...`. Switch to a `【...】`
regex so the title fallback actually fires.
2. Reviews returned `5` instead of `21241`. The headText was whitespace-
collapsed to `★★★★★4.821241条...`, fusing the rating and review digits;
a head-wide `/\d+条/` then captured `4.821241` and rounded to `5`. Read
the dedicated `.reviews / .review-num` element ("21241条") instead, with
a `.review-title` "评价(<n>)" fallback.
* feat(dianping): browser adapter — search + shop on www.dianping.com
Adds two browser-mode adapters for the dianping (大众点评) PC site:
- `dianping search "<keyword>" --city <name|id> --limit <n>`: keyword
shop/restaurant search. Returns rank, shop_id, name, rating, reviews,
price, cuisine, district, url. shop_id round-trips into `dianping shop`.
- `dianping shop <shop_id>` (alias `detail`): shop detail sheet
(field/value rows: name, rating, breakdown 口味/环境/服务/食材, reviews,
price, rank, hours, address, subway, features, url).
Both use Strategy.COOKIE on www.dianping.com (the PC site renders search
SSR and does not require JS hydration). m.dianping.com is intentionally
crippled for non-mobile UAs, so it's not used.
Auth detection (utils.detectAuthOrEmpty) inspects both response text and
final URL for the Meituan Yoda captcha redirect (verify.meituan.com) and
the dianping login redirect; raises AuthRequiredError with the captcha
URL embedded so the user can clear it manually in the same profile.
Listing↔detail id pairing: search.shop_id → shop.<id>. Adds 'shop' to
DETAIL_NAMES in scripts/check-listing-id-pairing.mjs so the convention
gate scans this site (35 sites / 78 listings now covered).
* fix(dianping): harden browser failure classification
* fix(dianping): fail on partial missing shop ids
Adds opencli convention-audit for batch convention scanning, with structured output, strict mode, docs, and startup isolation from local user/plugin discovery.
* feat(youtube/xiaohongshu/xiaoe): surface dropped ids/url on listings (sweep)
Round 8 same silent-column-drop class as #1300/#1301/#1302 — row already
emits the id/url field but `columns` array forgot to project it, so table
view drops it and agent loses the chain into detail commands.
- youtube/feed: rename row.videoId → video_id (snake_case convention),
add to columns. youtube/video accepts both URL and id, so url-based
round-trip already worked, but exposing the canonical id removes the
url-parse step for chained calls.
- xiaohongshu/feed: pipeline map already extracts `id` from the homefeed
payload, columns now lists it.
- xiaoe/catalog: pipeline map already projects `url`, columns now lists
it. xiaoe/detail takes a positional url, so this completes the
round-trip explicitly.
Also fixes one camelCase column violation on youtube/feed (videoId vs the
project's snake_case convention as in twitter `is_retweet`/`created_at`,
douban `subject_id`/`photo_id`, hupu `thread_title`).
CI gate `check:listing-id-pairing` ✓ (34 sites, 77 listings, 10 exempt).
typecheck clean. 114 tests pass for youtube + xiaohongshu.
* fix(youtube): keep feed continuation ids after rename
Round 7 — silent-drop sweep. Continues the listing→detail id-pairing
work from #1297. Each row was already extracting these ids/urls
internally; only the `columns` projection was missing, so they showed
up in `-f json` but never on the table view.
| Adapter | Added columns |
|--------------------|-------------------------------------|
| `1688 search` | `item_url`, `member_id` |
| `hupu mentions` | `tid`, `pid`, `url` |
| `douban photos` | `photo_id`, `subject_id` |
| `linux-do tags` | `slug` |
Round-trip wins:
- `1688 search` → `1688 item <item_url>` (item_url is the canonical
detail.1688.com URL); `1688 search` → `1688 store <member_id>`
- `hupu mentions` → `hupu detail <tid>` (and `pid` for the deep link)
- `douban photos` → tied back to the parent movie via `subject_id`
- `linux-do tags` → `linux-do feed --tag <slug>` (slug is the URL form)
No logic change — only the column array. JSON output unchanged.
Tests: 45/45 pass for the four affected sites.
Round 6 — silent-drop audit follow-up. Sibling twitter listings have
been inconsistent about the canonical tweet `id` (rest_id):
- timeline ✓ exposes id
- search ✓ exposes id
- list-tweets ✓ exposes id
- notifications ✓ exposes id
- bookmarks ✗ extracts but drops it from columns
- likes ✗ extracts but drops it from columns
- tweets ✗ extracts but drops it from columns
The `id` is already in the row object — only the `columns` projection
was missing. With the listing↔detail id-pairing CI gate from #1297 now
on main, surfacing `id` makes round-trip into `twitter thread <id>` /
`twitter delete <id>` / `twitter like <id>` work from the table view too
(previously only via `-f json`).
Other field-presentation drift (`name`, `created_at`, `retweets`)
aligned with sibling adapters where those values are already emitted.
Tests: tweets.test.js asserts `toEqual` on the columns array — updated
that assertion. Other twitter tests use `toMatchObject` and pass
unchanged. 81/81 in `clis/twitter/`.
While auditing instagram/facebook/pixiv coverage gaps, found that pixiv
listings already extract `user_id` and construct `url` per row but drop
both fields from the table view (`columns` doesn't list them). The data
is in the row object — only the column projection was missing.
Per the listing↔detail id pairing convention (#1297), surface them so:
- `user_id` round-trips from `ranking` / `search` → `user` / `illusts`
- `url` is the canonical share link for every illust / user record
Changes:
- `ranking`: + user_id, + url
- `search`: + user_id, + url
- `illusts`: + url (user_id is the arg, no need to repeat per row)
- `user`: + url
No behavior change beyond the table view — JSON output already had these
fields, so existing scripts that consume `-f json` keep working.
* feat(dblp): public bibliography adapter — search + paper
Wraps the dblp.org public API:
- `dblp search <query>` → /search/publ/api JSON, projected into one row per hit
- `dblp paper <key>` → /rec/<key>.xml, parsed into a one-row record
Why dblp on top of arxiv/openreview: dblp is the largest, oldest CS
bibliography (7M+ entries) and the only one of the three that consistently
indexes pre-arXiv literature, journal articles, books, and theses. The
canonical record key (e.g. `conf/nips/VaswaniSPUJGKP17`) round-trips
cleanly between the two commands per the listing↔detail convention.
Implementation notes:
- No deps beyond the registry — XML parsed with conservative regexes,
same approach as the arxiv adapter.
- Polite User-Agent per dblp's API guidance; HTTP 429 mapped to a
CommandExecutionError with a "lower --limit" hint.
- Author homonym suffixes (`"Smith 0001"`) trimmed for clean output.
- 39 unit tests cover validators, XML extraction, both commands.
* fix(dblp): fail fast on API status envelopes
* feat(convention): listing↔detail id pairing rule + CI gate
Adds a hard convention: when a site exposes both a listing-class command
(search / hot / top / recent / ...) and a detail-class command (read /
paper / article / view / ...), every listing row MUST surface an id-shaped
column whose value round-trips into the detail command. Without that, an
agent has no way to follow up on a listing row except re-searching by
title or scraping URLs out of band — both of which break the agent-native
contract.
What's in this PR
- docs/conventions/listing-detail-id-pairing.md — full rule, examples
table, why-it-matters, what counts as id-shaped, exemption taxonomy,
how to add an id column to a listing.
- scripts/check-listing-id-pairing.mjs — validator that reads
cli-manifest.json, classifies each entry as listing / detail / other,
and fails when a listing on a site that also has a read-detail command
is missing an id-shaped column. Exemption allowlist records WHY each
pair is exempt so future maintainers know what to verify.
- npm run check:listing-id-pairing — strict-mode wrapper.
- CI: new step in build job runs the validator after the manifest
freshness check on Linux.
- docs/developer/ts-adapter.md — cross-link from the adapter authoring
guide.
- docs/.vitepress/config.mts — sidebar entries for the new conventions
section.
Fixes brought to zero violations
- 1688/search: add offer_id (already extracted, just surfaced)
- bluesky/user: add uri (AT URI round-trips into bluesky/thread)
- tieba/search: add id + url (thread_id already extracted)
- tieba/hot: add url (rows are topics, not threads — url is the
best-effort round-trip handle, doc'd as such)
Exemptions (intentional, doc'd in EXEMPT map with rationale)
- nowcoder/hot, bluesky/trending, twitter/trending — listing rows are
topic strings, not posts.
- lesswrong/user, reddit/user — rows are profile-attribute key/value
pairs, addressed by the username arg.
- discord-app/search — desktop UI session, message ids not extractable.
- notion/search — Strategy.UI Quick Find, page ids not exposed in DOM.
Validator output after this PR: 32 sites scanned, 75 listings checked,
7 exempted, 0 violations.
* fix(convention): tighten listing id gate
* fix(convention): close url-derived id loophole
* feat(indeed): add `search` and `job` adapters (US site)
Adds an Indeed adapter that fills the US job-search gap (alongside
existing 51job / boss-zhipin / linkedin coverage). Both commands run
through a real browser session because Indeed sits behind Cloudflare
and answers bare HTTP fetches with `403` + `cf-mitigated: challenge`.
## Commands
- `indeed search <query>` — keyword job search
- args: `query`, `--location`, `--fromage`, `--sort`, `--start`, `--limit`
- columns: `rank, id, title, company, location, salary, tags, url`
- `indeed job <jk>` (alias `detail`, `view`) — full job posting
- args: `id` (positional, the 16-char hex `jk` from `search`)
- columns: `id, title, company, location, salary, job_type, description, url`
## Listing↔detail id pairing
`search.id` is the Indeed `jk` (job key, 16-char lowercase hex). It feeds
directly into `indeed job <jk>`. Conforms to the listing↔detail id
pairing convention proposed in #1297.
## CF challenge handling
The adapter polls the result selectors for up to 15s after navigation,
giving the browser time to clear the Cloudflare interstitial. If the
challenge is still up after the wait, the adapter throws a
`CommandExecutionError` with a hint pointing the user at the connected
browser to clear it once. Subsequent calls reuse the warmed cookies via
`Strategy.COOKIE`, mirroring the v2ex / boss / linkedin patterns.
## Validation
`utils.js` keeps argument validation pure and unit-testable:
- `requireJobKey` rejects anything that isn't a 16-char lowercase hex
- `requireFromage` only accepts `1` / `3` / `7` / `14` (Indeed's enum)
- `requireSort` only accepts `relevance` / `date`
- `requireBoundedInt(limit, default=15, max=25)` — Indeed serves at most
one page (10 jobs/page); ArgumentError on out-of-range, no silent
clamping, per the typed-error feedback in #1289.
## Tests
18 unit tests in `clis/indeed/indeed.test.js` cover registration,
validators, URL builders, and DOM-card normalizers. Browser-driven
verification stays out of CI by design (CF challenge is interactive).
## Docs
- `docs/adapters/browser/indeed.md` — full adapter doc with prerequisite
CF-challenge notes and listing↔detail id pairing callout.
- Sidebar entry + adapter index row.
* fix(indeed): tighten timeout fail-fast and runtime tests
* fix(indeed): align readiness with search parser
* feat(openreview): add public adapter — search/venue/paper/reviews
OpenReview is the open peer-review platform used by ICLR / TMLR / COLM
and ML workshops. Its v2 API exposes everyone-readable submissions,
reviews, and decisions without auth, so all four commands run with
`browser: false`.
Commands:
- `openreview search <query>` — full-text search
- `openreview venue <venue>` — list submissions; accepts either a venue
display name (matched against `content.venue`, e.g. "ICLR 2024 oral")
or a full invitation id (e.g. "ICLR.cc/2025/Conference/-/Submission")
via `/-/` heuristic; supports offset pagination
- `openreview paper <id>` — single-paper detail with full abstract
- `openreview reviews <forum>` — paper + threaded reviews/decisions/
comments, ordered chronologically with paper lifted to row 0;
classifies notes via invitation tail (REVIEW / DECISION / REBUTTAL /
COMMENT / META_REVIEW / WITHDRAWAL); per-row truncation via
`--max-length` (min 200)
Listing IDs round-trip into `paper`/`reviews`. PDF URLs normalized to
absolute `https://openreview.net/pdf/...`. `pdate` falls back to
`cdate` when missing, formatted as `YYYY-MM-DD`.
All limits/offsets/ids fail-fast with typed errors (`ArgumentError`,
`EmptyResultError`, `CommandExecutionError`) — no silent clamping, no
empty-array fallbacks. fetch + json + non-2xx + 404 are wrapped so
network/API failures never look like empty results.
Tests: 23 unit tests covering the column contract, content extraction,
date/PDF normalization, invitation-vs-venue dispatch, error paths
(network/JSON/HTTP), pagination offset accounting, and the reviews
classifier + section joiner + truncation.
Live-verified against api2.openreview.net for search ("diffusion
model"), venue ("ICLR 2024 oral"), paper (KS8mIvetg2), and reviews on
that paper's full thread.
* fix(openreview): tighten error and review typing
* fix(openreview): stabilize review contracts
Follow-up from PR #1293 review: 'all answers' was misleading because
the implementation is limit-bounded (default 10, max 100) rather than
unbounded pagination. Spell out the actual contract — including the
accepted-answer-outside-page fallback path — so users don't expect
infinite-scroll behaviour.
Non-blocking docs-only change flagged by codex-mini1 + First-principles-1
during #1293 review.
* feat(lobsters): surface short_id + created_at on listings, add `read <short_id>`
Same agent-native gap as the just-merged hackernews PR (#1288):
1. The 4 listings (hot / newest / active / tag) didn't surface each story's
`short_id`. Agents could see the title and a comments URL but couldn't
pass the id back into a follow-up command. Add `id` (= `short_id`) and
`created_at` columns; `created_at` is cheap signal for "how stale is this".
2. There was no way to read a story + comment tree from the CLI. Lobsters
makes this nicer than HN: `https://lobste.rs/s/<short_id>.json` returns
the story plus a flat `comments[]` array where each entry already carries
`parent_comment` and `depth`, so we get the full thread in one HTTP call
and just DFS using the parent map.
`read` mirrors the `hackernews read` shape (POST row + L0/L1/… indented
comments, `[+N more replies]` stubs at depth/limit cutoffs) so the two
adapters feel the same to agents that already learned one. Same typed
fail-fast envelope: `ArgumentError` on bad short_id / non-positive limit /
depth / replies, `EmptyResultError` on 404 or empty body, `CommandExecutionError`
on other HTTP failures.
Tests cover all 4 listings (column shape + map step), `read` registration,
positional arg shape, ArgumentError fail-fast (no fetch on bad input),
EmptyResultError on 404, threaded-tree assembly from a flat `comments[]`,
and the `+N more replies` depth-cutoff path.
* test(lobsters): lock read fail-fast coverage
* docs(lobsters): list read command
* feat(devto): surface article id + published_at on listings, add `read <id>`
Agent-native gap: devto listings (`top`/`tag`/`user`) didn't include the
article `id`, so an agent couldn't round-trip from a listing into a body
read. They also dropped `reading_time` and `published_at`, which are cheap
signals the API gives you for free.
Changes:
- `top` / `tag` / `user`: add `id`, `reading_time`, `published_at` columns
alongside existing rank/title/etc. `user` keeps its no-author shape since
it's already user-scoped.
- New `devto read <id>`: hits `dev.to/api/articles/<id>` and returns one
row with the article body (truncated by `--max-length`, default 20000,
min 100). DEV.to's public API does not expose comments yet, so this is
intentionally a single-row reader rather than a HN/lobsters-style
threaded tree — if/when comments become public we can extend to
POST + L0/L1.
- Typed fail-fast: `ArgumentError` for non-numeric id and for `--max-length`
below 100; `EmptyResultError` on 404; `CommandExecutionError` for other
non-2xx HTTP statuses. No silent clamps.
- Defensive tag normalization: the `/api/articles/<id>` endpoint returns
`tag_list` as a comma-string and `tags` as an array (the opposite shape
from listing endpoints). Caught this on live verification — both shapes
now collapse to a comma-joined string.
Tests: 12 vitest assertions covering listing column shape (all 3) +
register/args/strategy + typed-error fail-fast paths + happy-path body
extraction + truncation marker + alternate tag_list shape.
Live verification: `devto top --limit 3` and `devto read 3602287` both
return the expected agent-native shape.
* fix(devto): harden article read contract
X removed the post-count caption from each cell on `/explore/tabs/trending`.
The adapter still iterated `divs[2..]` looking for a numeric text node and
fell back to the literal string "N/A" when it found none — which was every
row, on every call. We were emitting a silent-wrong column for every result.
Drop the column and the no-longer-relevant scan loop. Add a regression test
on the columns shape so the column doesn't slip back in.
Live runs of `opencli twitter trending` previously returned rows like
`{rank: 1, topic: "...", tweets: "N/A", category: "..."}` — the `N/A` was
not a transient outage, it was structural.
* feat(arxiv): full abstract/authors, surface pdf+categories+comment, add `recent <category>`
`paper` was silently truncating the abstract to 200 chars and dropping all but
the first 3 authors — agents calling it for a paper summary lost data. Stop
truncating, return all authors, and surface the rest of what the Atom feed
already gives us: pdf url (`<link rel="related">`), all `categories`,
`primary_category`, and the author `comment` (page count, conference, etc.).
`search` keeps a compact list shape (no abstract column, but adds
`primary_category`).
New `arxiv recent <category>` lists newest submissions in a category sorted by
`submittedDate desc` — fills a gap (previously you had to know a search term
to surface anything). Validates the category string and rejects malformed
input via `ArgumentError`.
`search` also switches its no-results path from `CliError('NOT_FOUND', ...)`
to `EmptyResultError` to match the convention other public-API adapters use.
Tests cover: command registration, full-abstract / all-authors parsing, XML
entity decoding in titles, pdf/categories/comment extraction, and category
validation.
* fix(arxiv): harden category and limit validation
* feat(hackernews): add `read <id>` and surface item id on every listing
Two related agent-flow gaps in the HN adapters:
1. `top`/`best`/`ask`/`new`/`show`/`jobs`/`search` all carry the HN item
id internally (firebase items are fetched by id; algolia hits include
`objectID`) but drop it before output. Without an id column the agent
can see the title but has no handle to follow up with.
2. There was no way to read a story's discussion. The whole reason an
agent looks at HN is the comments — and that capability was missing.
This PR adds:
- `id` column on every listing adapter (firebase items: numeric id;
algolia search hits: `objectID` string). Existing column order is
preserved otherwise.
- `hackernews read <id>` — public/non-browser adapter that fetches the
story plus a tree of top-level comments + inline replies via
`https://hacker-news.firebaseio.com/v0/item/<id>.json`. Mirrors the
`reddit read` shape (`type/author/score/text`) so agents can use both
with one mental model. HTML-only fields (comment text) are converted
to plain text with anchor URLs preserved.
- Column-contract tests covering all listings + the new read adapter.
- Doc entry under `docs/adapters/browser/hackernews.md`.
Tested locally via `~/.opencli/clis/hackernews/` overrides:
opencli hackernews top --limit 3 # id present
opencli hackernews search rust --limit 2 # id (objectID) present
opencli hackernews read 47999636 --limit 5 # threaded output
* fix(hackernews): typed fail-fast for read
* fix(douban): drop unparseable fields from movie-hot, add id/votes
The chart page (movie.douban.com/chart) only exposes a single comma-joined
text dump in `.pl2 p`, of the shape:
<release_dates...> / <actors...> / <regions...> / <director_zh> /
<runtime>分钟 / <other_titles> / <genres> / <director with English> /
<languages>
The previous `loadDoubanMovieHot` tried to anchor on the release-date
regex and take `parts[releaseIndex - 1]` as director and
`parts[releaseIndex - 2]` as region. That breaks in two ways:
1. Most entries have multiple release dates back-to-back, so the
"anchor minus one" position is itself a date. Director output becomes
`'2025-09-07(多伦多电影节)'` and region is empty — silent wrong data.
2. For entries with a single release date, the offsets land on actor
names, not director / region.
The page does not actually carry a clean director or region per row —
that's only available on the subject detail page. Trying to reconstruct
either from the chart string is the canonical "verify passes but data is
wrong" failure (success-rate-pitfalls §2 sibling DOM contamination).
Fix: drop `director`, `region`, `quote` from the listing. Surface what
the chart page actually provides reliably:
- `id` — extracted from the subject URL, ready for `douban subject`
- `votes` — from `.star .pl` (`(62484人评价)`), useful as popularity signal
- existing `rank`, `title`, `rating`, `year`, `url`
Agents that need director / region should follow up with
`opencli douban subject <id>`, which is already wired for that data.
* fix(douban): fail fast on empty movie hot
* fix(bilibili,reddit): add identifier and url columns to hot lists
Both `bilibili hot` and `reddit hot` previously dropped their per-row
identifier and URL on the way out, breaking the typical agent flow where
the next call needs a `bvid` / `postId` to fetch detail or comments.
- bilibili/hot: add `bvid` and `url` columns (constructed from bvid)
- reddit/hot: surface `postId`, `author`, `url` (already in evaluate but
dropped in map)
Tested via local `~/.opencli/clis/<site>/hot.js` overrides.
* test(bilibili,reddit): lock hot list identifier columns
Move the Browser Bridge daemon WebSocket out of the MV3 service worker and into an offscreen document. Remove the popup/action UI and obsolete extension log forwarding now that doctor is the diagnostic surface.
Pairs with the new collection-create adapter so users (and future
fixture-teardown logic) can clean up saved-post collections from CLI.
- POST /api/v1/collections/{id}/delete/ with multipart module_name=collection_settings
- Accepts collection name (case-insensitive) or numeric collection_id; resolves
via /collections/list/ first so unknown / duplicate names error explicitly
instead of bubbling up a 404 or silently deleting the wrong one.
The previous implementation silently skipped any adapter whose import
failed (catch + warn-to-stderr + return []), then printed a successful
"✅ Manifest compiled: N entries". When dist/ was stale (e.g. after
renaming an export the JS adapters re-import) every adapter using that
export would fail to load, get skipped, and the script still exited 0.
An agent reading exit codes to gate work would commit the resulting
manifest and silently delete dozens of unrelated adapter entries.
Three layers of defense:
1. Distinguish skip kinds. Files that don't call `cli(...)` are still
silently dropped (helpers / type modules). Files that look like CLI
modules but fail to import now throw `ManifestImportError`. The
batch scanner aggregates failures and `main()` exits 1 with an
explicit list, leaving the existing manifest on disk untouched.
2. Net-deletion safety net. `main()` diffs the new entries against the
committed manifest and refuses to overwrite when entries would be
removed. `--allow-removals=N` (or bare `--allow-removals` for any)
is the explicit opt-in; the error message tells the caller exactly
what value to pass.
3. Runtime dist guard. `node dist/src/build-manifest.js` now refuses
to run with a clear pointer at `npm run build-manifest` (which uses
tsx). The npm script itself is migrated to `tsx src/build-manifest.ts`
so no project-level command points at the compiled copy anymore.
Release CI gains a manifest-drift gate (build-manifest + git diff
--exit-code) so a tag push can never publish stale or silently-shrunk
manifests. The existing CI check on PRs is preserved.
`ManifestEntry` is split into `src/manifest-types.ts` so runtime code
(discovery.ts) imports the type without pulling the build-time
compiler module.
Tests:
- `loadManifestEntries` throws ManifestImportError on import failure
- helper modules without cli() are still silently skipped
- `scanClisDir` aggregates per-adapter failures
- `diffRemovedEntries` returns expected site/name diff
- `parseBuildManifestArgs` reads --allow-removals[=N]
WAWQAQ feedback: the green left border on the status row looked
disconnected — only on the top half of the card, creating an awkward
stub. Connection state is already conveyed clearly by the colored dot
and the "Connected to daemon" / "Disconnected" text, so the border was
redundant decoration.
Drop the .card.connected/.disconnected/.connecting border-left rules.
No JS or layout changes; cleaner surface, fewer visual variants.
- Merge status row and profile row into a single rounded card with a
brand-colored left border accent indicating connection state
- Render contextId inline next to a "Profile" label with a Copy button,
letting users paste it into `opencli profile rename` without manual
selection (replaces the old full-width code block treatment)
- Show daemon version inline in the status row when connected, and
render the extension version as a tag in the popup header — both
surface version information that helps diagnose stale-daemon issues
- Forward both versions through the existing `getStatus` background
message: extension reads its own version from the manifest, daemon
version is fetched best-effort from `/status` with a 1.5s timeout so
popup never hangs when the daemon is unreachable
Closes#1192. Two changes:
1. New `instagram collection-create <name>` adapter wraps
`POST /api/v1/collections/create/` (multipart `name` +
`module_name=collection_create`, X-IG-App-ID + X-CSRFToken).
2. `instagram saved` gains an optional `--collection <name>` flag.
When set, the adapter resolves the name to a collection id via
`/api/v1/collections/list/` (case-insensitive trim match) and then
fetches `/api/v1/feed/collection/{id}/posts/`. Unknown names throw
with the available list so callers can self-correct.
Both verified end-to-end against a live IG account. Verify fixtures
under ~/.opencli/sites/instagram/verify/ ship the
patterns/notEmpty/mustBeTruthy guards from the latest adapter-author
skill (success-rate-pitfalls §1, §4, §8).
* feat(weibo): add favorites + publish CLI commands
Consolidates #1253 (favorites) and #1254 (publish) into a single PR per maintainer request.
- clis/weibo/favorites.ts: cookie-mode fetch of authenticated user's favorites via weibo.com/u/page/fav/{uid}
- clis/weibo/publish.js: UI-automation post (text up to 2000 chars, up to 9 images jpg/png/gif/webp)
- cli-manifest.json regenerated to include the new commands
Note: favorites.ts uses TypeScript syntax but build-manifest.js scans only *.js — favorites is currently NOT registered in the manifest. Reviewers please check whether to rename to .js or whether the manifest scanner should learn .ts.
Authored-by: hszhsz <heshaoz1990@gmail.com>
* fix(weibo): harden favorites and publish commands
* fix(weibo): publish without execute gate
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(claude): add Claude adapter
Adds a Claude (claude.ai) browser adapter family with seven commands
modeled on the existing clis/deepseek/ pattern: ask, send, new, status,
read, history, detail.
Closes#1251
* feat(claude): align send command columns with doubao
Match the established Status / SubmittedBy / InjectedText shape used by
doubao send so agent loops can rely on a consistent fire-and-forget
output across AI chat adapters.
* fix(claude): preserve DOM order in getVisibleMessages
The previous implementation queried user-message and assistant-message
nodes in two passes, which serialized as [u1, u2, u3, a1, a2, a3] for
multi-turn chats instead of the correct conversation order. Single
combined query preserves DOM order so claude read / detail return
turns in the order the user reads them on the page.
* docs(claude): note --live requirement for read across invocations
* fix(claude): fail fast on auth and empty states
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(update-check): show extension update notice on exit
The CLI exit hook already prints "Update available" when a newer @jackwener/opencli is on npm. Extension updates were only surfaced inside `opencli doctor`, so users running normal browser commands had no signal that the Chrome extension was out of date.
Solution piggybacks on the existing 24h background fetch:
- Daemon writes the live extensionVersion + lastSeenAt into the shared cache on every hello handshake (rare event, one fs.writeFileSync).
- CLI exit hook reads the cache it already loads and prints an extra extension notice when a newer release is available and the cache is fresh (<7d).
- writeCache becomes a read-merge-write so the daemon's currentExtensionVersion and the CLI's npm latestVersion don't clobber each other.
Net cost on the CLI hot path: zero new I/O, zero new daemon contact. The notice formatter is split into a pure helper (buildUpdateNotices) so the staleness window, equality, and combined-notice cases are unit-tested without touching disk or stderr.
* fix(update-check): tolerate partial cache when daemon writes first
Self-review caught a TypeError path: if the daemon's hello handler runs `recordExtensionVersion` before the CLI's npm fetch ever populated the cache, the resulting cache file has only `currentExtensionVersion` + `extensionLastSeenAt` and no `latestVersion`. The next CLI run then fed `undefined` into `isNewer`, which calls `.replace(...)` on it.
- Mark `lastCheck` and `latestVersion` optional in the cache schema (the merge pattern means either side may write first).
- Guard the CLI notice on `cache.latestVersion` being defined before comparing.
- Guard `checkForUpdateBackground`'s 24h short-circuit on `lastCheck` being defined.
- Add a test for the daemon-only cache case.
* feat(zhihu): add collection command to list favorite items
Add new 'opencli zhihu collection' command that:
- Lists items from a Zhihu collection (requires login)
- Supports pagination with --offset and --limit parameters
- Handles multiple content types: answer, article, pin
- Shows collection statistics: total count, total pages, current page
* feat(zhihu): split collection into collection and collections commands
- Rename zhihu collection list functionality to zhihu collections
- Keep zhihu collection for viewing specific collection contents by ID
- Convert collection.ts to collection.js so build-manifest picks it up
- Add tests for both commands
- Update cli-manifest.json
* fix(zhihu): harden collection read commands
---------
Co-authored-by: Developer <developer@example.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(twitter/following): switch from INTERCEPT+autoScroll to COOKIE+cursor pagination
The previous INTERCEPT strategy relied on autoScroll to trigger Twitter's
pagination by scrolling document.body. Twitter's virtual list doesn't grow
document.body.scrollHeight, so scrolls stopped triggering API calls after
the first few pages, capping results at ~50 regardless of limit.
Now uses Strategy.COOKIE with explicit cursor-based GraphQL pagination
(same pattern as twitter/likes), which correctly fetches all pages.
Fixes#1230
* fix(twitter): harden following pagination
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(boss): add jobType filter and bossOnline output
Add --jobType param (全职/兼职/实习 = 1901/1902/1903) so callers can
exclude internships at the API layer instead of post-filtering by name
keywords. Without this, --experience 应届 returns a mix of 校招 and 实习
because BOSS bundles both under code 108.
Also surface bossOnline (Y/empty) in results so callers can prioritize
HRs currently online — this is the only activity signal exposed by the
web API; 'recently active' / 'newly posted' filters are mobile-only and
not accepted by /wapi/zpgeek/search/joblist.json.
* fix(boss): correct experience codes (应届=102, not 108)
The previous EXP_MAP was off by ~2 across the board. Verified each
code by clicking BOSS web's filter UI and reading the URL:
108 = 在校生 (interns) was: '在校/应届','应届' → 108 (wrong)
102 = 应届生 (校招 full-time) was: '1-3年' → 102 (wrong)
101 = 经验不限 was: '1年以内' → 101 (wrong)
103 = 1年以内 was missing
104 = 1-3年 was: '3-5年' → 103 (wrong)
105 = 3-5年 was: '5-10年' → 104 (wrong)
106 = 5-10年 was: '10年以上' → 105 (wrong)
107 = 10年以上 was missing
This is why --experience 应届 had been returning mostly 实习生 jobs:
it was secretly querying 在校生 (108). The fix makes 应届 actually
mean 应届生 (102 = 校招), and lets users pick 在校生 (108) explicitly
when they do want internships.
* fix(boss): validate job type filter
* fix(boss): keep legacy campus experience alias
---------
Co-authored-by: youhh <youhh@1051233107@qq.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(deepseek): add vision mode support
DeepSeek added a third model "识图模式" (Vision Mode) that accepts
image uploads for visual understanding. Add vision to the --model
choices, update selectModel to use explicit index mapping for all
three models, skip the search toggle in vision mode (not available),
and extend waitForFilePreview to detect image thumbnails via send
button state since vision mode shows a preview image instead of a
filename label.
Also catch "Not allowed" errors from setFileInput (Cloudflare may
block CDP file operations) so the DataTransfer fallback can run.
Closes#1215
* fix(deepseek): harden vision upload mode
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(chatgpt): fix image generation detection and output path
Three fixes for chatgpt image command:
1. Page navigation: ChatGPT redirects away from the conversation
after sending. Poll for the /c/ URL after send, then periodically
reload the conversation page during image wait to pick up
asynchronously rendered images.
2. Composer selector: add fallback selectors for the chat input
since ChatGPT uses different aria-labels across UI versions.
3. Output path: the default '~/Pictures/chatgpt' was passed as a
literal string without tilde expansion, creating a directory
named '~' in the working directory. Removed the string default
and use os.homedir() fallback instead.
Fixes#1206
* fix(chatgpt): fail fast on image export failures
* fix(chatgpt): avoid reloads during image generation
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(chatgpt-app): support Traditional Chinese UI labels
The send button and Options button matchers only included Simplified
Chinese ("发送", "选项"). On macOS systems with Traditional Chinese as
the system language, the ChatGPT desktop app exposes "傳送" and "選項"
via the Accessibility API, causing `chatgpt-app send` to fail with
"Could not find send button" and `chatgpt-app model` to fail with
"Could not find Options button" for zh-TW / zh-HK users.
Verified via AXUIElement walk on ChatGPT 1.2026.104 / macOS 26 with
system language set to Traditional Chinese.
The "Stop generating" detection at line 314 already handles Traditional
Chinese because 停止生成 uses identical glyphs in both writing systems.
"Legacy models" at line 261 still lacks any Chinese variant but is not
addressed here since the Traditional Chinese translation has not been
verified on a live UI.
* test(chatgpt-app): cover traditional chinese ax labels
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(zhihu): fix identity detection, comment, answer, and search
Identity detection: Zhihu removed __INITIAL_STATE__ and moved the
user avatar from a profile link into a button. Added fallback that
extracts the user slug from the header avatar alt text.
Comment and answer: Zhihu moved the comment editor into a Modal
and changed the submit button behavior, breaking the UI-based
write flow. Replaced with direct API calls (POST /api/v4/answers/
{id}/comments and POST /api/v4/questions/{id}/answers) which are
reliable and much simpler.
Search: Zhihu's search API now returns mixed result types (ads,
education, hot_timing) alongside search_result. Updated the filter
to select by object.type (answer/article/question) and increased
fetch size to compensate for non-content results.
Fixes#1198
* fix(zhihu): rewrite like, follow, favorite to use API
Same DOM breakage as comment/answer. Replaced UI-based click
flows with direct Zhihu API calls for all write commands.
* fix(zhihu): harden api write regressions
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(zlibrary): add search and info commands
Add Z-Library adapter with two browser-based commands:
- `search` — Search books by title, author, or ISBN.
Navigates to /s/<query> and extracts results from
<z-bookcard> shadow DOM custom elements.
- `info` — Get book details and available download formats
from a book page URL.
Uses Strategy.COOKIE with browser automation to bypass
Cloudflare protection. The adapter reuses the user's existing
Z-Library login cookies from system Chrome.
Known limitation: actual file downloading requires Playwright's
download event handling (page.on('download')). OpenCLI's browser
automation does not currently intercept file downloads. Users
needing to download files should use Playwright to navigate to
the book URLs discovered by this adapter.
* fix(zlibrary): harden input and empty extraction
---------
Co-authored-by: jean <jean@jeandeMacBook-Pro.local>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(deepseek): fix send button detection in sendMessage
The previous selector `btn.closest('div')?.querySelector('textarea')`
always returned null because the button itself is a div, so
closest('div') returns the button, which has no textarea inside.
This caused every send to fall through to the Enter key fallback.
Walk up from the textarea to find the input container, then select
the last enabled non-toggle button with an SVG icon (the send
button). Excludes `.ds-toggle-button` elements (DeepThink / Search
toggles) so only the actual send button is clicked.
* fix(deepseek): fail closed when upload never enables send
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(browser): bind current tab to bound workspace
* docs(browser): document bound session idle semantics
* test(extension): cover bind-current owned-overwrite refusal
Adds regression for the second guard in handleBindCurrent that refuses
binding when the bound:* workspace already has an owned automation
window. Previously only the non-bound prefix path was tested.
* refactor(browser): rename bind command
* fix(browser): bind only current window tabs
* fix(browser): fail unbind when detach command fails
* feat(google-scholar): add cite and profile commands, fix search dedup
- cite: get BibTeX/EndNote/RefMan/RefWorks citation for a paper.
Clicks the cite button in search results and fetches the citation
content from Google's citation endpoint.
- profile: view an author's Google Scholar profile (h-index,
i10-index, citation count, top papers). Accepts author name
or Scholar user ID.
- search: fix duplicate results caused by CSS selector matching
both outer container (.gs_r.gs_or.gs_scl) and inner child
(.gs_ri) for each paper.
Closes#1174, closes#1175
* fix(google-scholar): fail fast on cite and profile misses
* fix(google-scholar): document new commands and lock dedup test
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
Pre-navigate Uiverse commands and retry detached browser bridge failures so code and preview flows stop falling back to about:blank. Broaden preview element matching for input-root components and cover the new navigation contract in tests.
* separate author name from date text in search results
* fix(xiaohongshu): constrain author date stripping
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(youtube): channel videos-tab fallback reads wrong tab from InnerTube response
After PR #1109, `opencli youtube channel <id>` still returns empty
`recent_videos` for channels whose Home tab is empty AND whose InnerTube
`/youtubei/v1/browse` response includes multiple tabs.
Root cause: the fallback fetch sends a browse request with the Videos
tab's `params`. The response, however, includes ALL tabs (Home, Videos,
Shorts, ...), with only the requested tab marked `selected: true`. The
existing code reads `tabs?.[0]?.tabRenderer?.content?.richGridRenderer?.contents`
— for multi-tab responses `tabs[0]` is Home (empty), so `richGrid` ends
up `[]` and `recentVideos` stays empty. PR #1109's test channels happened
to return single-tab lists with Videos at index 0, masking the bug.
Fix: find the tab with `selected: true` instead of assuming `tabs[0]`.
Reproducer: `opencli youtube channel UC44DSuDgw7_qccvZzIK3Jpg`
(杀鱼伟-Vi, ~3.1K subs, posts daily). Returns 0 videos pre-patch, 30+
videos post-patch.
`npm run typecheck` clean, `npm test` passes (1952/1952).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(youtube): preserve videos tab fallback
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(doubao): use ID selector for send button
The clickSendButtonScript was searching for the send button by walking up
the DOM tree only 2 levels from the textarea, but the actual send button
#flow-end-msg-send is at level 5. This caused message sending to fail.
Fix by directly selecting the button via its ID.
* test(doubao): update send button selector assertions
* fix(doubao): keep send-button fallback contract
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(toutiao): move NON_TITLE_LINES inside function scope
NON_TITLE_LINES was defined outside parseToutiaoArticlesText() as a
module-level const. When the function is serialized via .toString()
and injected into browser evaluate context, outer scope variables
are not available, causing 'NON_TITLE_LINES is not defined' error.
Fix: move NON_TITLE_LINES inside the function so it's included in
the serialized string.
* test(toutiao): cover serialized articles parser
---------
Co-authored-by: sontjer <sontjer@github.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* docs: update extension install to Chrome Web Store link
Extension is now published on Chrome Web Store. Replace manual
download/unpack instructions with the store link across READMEs
and skill docs.
* docs: restore manual install as Option B alongside Chrome Web Store
The shared article-download pipeline strips all <button> elements
via STRIPPED_TAGS, which is correct for article adapters (zhihu,
weixin) but causes web read to silently lose meaningful button
content like "Download All" on generic pages.
Override the button stripping in web read's configureTurndown
callback so button text is preserved as inline content.
Fixes#1184
Restore the original icons (commit b2fa7da) that were replaced by the
v1.6.8 "refresh icons" change in e9867dc. Per user feedback, the original
neon `>_` design read more clearly and was preferred over the abstract
arrow + dash variant.
Reverts only the four icon PNGs (16/32/48/128); manifest, popup, and
extension version stay where they are.
* fix(chatgpt-app): use AX send flow and support zh-CN generating state
* fix(chatgpt-app): fail fast on stale AX send path
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(weixin): add publish (create draft with cover) and drafts (list drafts)
Closes#441
* fix(weixin): rename publish to create-draft to match issue #441 proposal
* fix(weixin): fail fast on draft auth and empty states
* test(weixin): align adapter imports with repo style
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(deepseek): fix history titles and resume conversation on ask
- history: use link.innerText instead of link.querySelector('div') for
title extraction. DeepSeek changed sidebar DOM; the first child div
is now an empty ds-focus-ring element, causing all titles to show as
(untitled).
- ask: when workspace is recycled (idle timeout) and --new is false
(default), click the most recent sidebar conversation link to resume
it instead of staying on the blank new-chat page. Skip model
selection when inside an existing conversation since the selector is
only rendered on the new-chat page.
- ensureOnDeepSeek: return boolean indicating whether navigation
occurred, so callers can react to workspace recycling.
Closes#1149
* fix(deepseek): fail fast on explicit model resume
* fix(cli): expose only explicit option sources
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(sinafinance): match stock symbol in addition to name
The scoring function only compared user input against the Chinese
display name (p[4] from suggest API), so searching "AAPL" matched
"AAPLU" (score 0.8) over Apple Inc. whose name field is "苹果"
(score 0). Check the symbol field first for exact and partial matches.
Fixes#1157
* docs(sinafinance): add missing commands to adapter index
The index table only listed `news` for sinafinance. Added the other
three commands (`rolling-news`, `stock`, `stock-rank`) and updated
the mode from Public to hybrid since rolling-news and stock-rank
require a browser.
Fixes#1156
* test(sinafinance): lock stock symbol matching
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(web,download): absorb #1048 media + --stdout into web read
Distill the useful pieces of the abandoned PR #1048 (`web md`) into the
existing shared pipeline instead of introducing a parallel command:
- Turndown rules for <video> / <audio> / <iframe>. Video and audio are
emitted as inline HTML so renderers that support it keep playback,
and iframes degrade to markdown links (title + src) so embedded
content (YouTube, CodePen, …) stays reachable. `iframe` moves out of
STRIPPED_TAGS since it's now handled explicitly.
- `stdout` option on ArticleDownloadOptions: writes the full markdown
to process.stdout, skips image download + mkdir + file write, and
reports saved='-'. Remote image URLs stay intact so piped output is
self-contained.
- `web read --stdout` wires the above through.
- Lazy-load src rewrite: the extractor now promotes data-src /
data-original / data-lazy-src / data-srcset onto `src` before the
HTML is frozen, so the markdown body and the image-download list
reference the same URL (previously a page with placeholder.gif +
data-src produced broken image links in the output).
Nothing in #1048 that overlapped with the already-merged #1143
hardening was kept — no new Readability wiring, no duplicate Turndown
config, no new command.
* fix(web): keep stdout streaming output clean
* fix(tests): update iframe e2e assertion and drop relative src import
- article-extract e2e fixture test: iframe now converts to a markdown
link instead of being stripped, so assert the YouTube embed link
survives rather than asserting its absence.
- clis/web/read.test.js: replace vi.importActual('../../src/registry.js')
with a direct __test__.command export from read.js; the relative
import into src/ tripped the package-exports adapter guardrail.
* fix(deepseek): separate thinking process from response in --think mode (#1124)
When --think is enabled, the response now includes separate fields:
- response: clean final answer only
- thinking: chain-of-thought reasoning content
- thinking_time: time spent thinking (e.g. '1')
Supports both English ('Thought for X seconds') and Chinese
('已思考(用时 X 秒)') thinking header patterns.
Fixes#1124
* chore: regenerate cli-manifest.json
* fix(deepseek): DOM-level think/response separation, dynamic columns
Blocker 1: Replace fragile split(/\n\n+/) heuristic in parseThinkingResponse()
with DOM-level extraction in waitForResponse(). The page evaluate now queries
distinct DOM nodes (.ds-markdown--think vs .ds-markdown) for thinking and
response content. The text-level parser falls back to treating everything
after the header as thinking (no split), avoiding silent corruption of
multi-paragraph content.
Blocker 2: Remove static columns declaration from askCommand. The renderer
infers columns from row keys, so non-think output only shows 'response'
while think output shows all three columns.
Tests added for multi-paragraph thinking, multi-paragraph answer, and
non-think column regression guard.
* chore: regenerate cli-manifest.json
* feat(download): harden HTML→Markdown pipeline
Inspired by the MD-This-Page / markdown-viewer-extension analysis, tighten
the shared article→Markdown converter used by zhihu/weixin/web adapters:
- enable turndown-plugin-gfm (tables, strikethrough, task lists)
- strip script/style/noscript/iframe/canvas/form/button/dialog unconditionally
- strip SVG via a dedicated rule (not in HTMLElementTagNameMap)
- drop base64 data-URI images so they don't bloat .md output
- post-process: collapse NBSP, lone bullet/middle-dot residue,
trailing whitespace, and 3+ blank lines
- frontmatter shape guarantees ≤2 consecutive newlines even when
some metadata fields are absent
Adds a minimal local .d.ts for turndown-plugin-gfm and 6 new tests
covering GFM conversion, tag stripping, base64 drop, and whitespace cleanup.
* fix(download): emit canonical markdown strikethrough
* feat(download,browser): finish article pipeline polish
Per the follow-up from the MD-This-Page / markdown-viewer-extension
analysis, land the remaining items in the same PR instead of splitting:
article-download.ts
- extend STRIPPED_TAGS with header/footer/nav/aside (page chrome; the
article's title/author/publishTime are supplied as separate fields on
ArticleData, so duplicated DOM is redundant)
- new option ArticleDownloadOptions.cleanSelectors — per-adapter CSS
selector list removed before conversion, applied as a Turndown rule
via node.matches so invalid selectors fail silently
browser/article-extract.ts (new)
- generic Readability-based extraction that runs in-page via CDP
evaluate (no jsdom in Node)
- short-circuits non-HTML documents (text/plain, JSON, XML) and the
single-<pre> "browser rendering a plain text file" case
- clones the document before any mutation (preserves live page state
for subsequent snapshot / click)
- isProbablyReaderable gate, Readability.parse on the clone, then a
fallback chain main → [role="main"] → #main-content → … → body
- library sources are JSON-embedded and eval'd inside a Function scope
so their backticks / module.exports guards don't collide with the
surrounding IIFE
Tests
- article-download: page-chrome strip, cleanSelectors match + invalid
selector silently ignored (2 new)
- article-extract: JS generation contents, default fallback chain,
response normalization, null / malformed handling, and a Function()
parse check to catch any template-literal break-out in the embedded
Readability sources (8 new)
* fix(download): honor selector cleanup in fallback paths
* test(e2e): real-site regression for hardened article pipeline
Adds tests/e2e/article-download-pipeline.test.ts driving `opencli web read`
through 6 representative pages (example.com baseline, Wikipedia GFM tables,
MDN metadata, GitHub fenced code, Vercel SSR blog, Ruan Yifeng CJK+images)
and asserting the post-processing invariants: no base64/script/style leaks,
no blank-line runs, no residue, no trailing whitespace, no NBSP.
Graceful skip on bot detection / transient CDP errors, with a single retry.
All 6 sites pass locally (37s total).
* test(browser): add article extraction e2e fixtures
* feat(51job): add comprehensive 51job adapter (search / hot / detail / company)
Four adapters covering the main 51job surface:
- `51job search <keyword>` — keyword job search via we.51job.com/api/job/search-pc.
Rich filters: --area (40+ city name/alias → 6-digit code), --salary, --experience,
--degree, --companyType, --companySize, --sort, --page, --limit. Response already
carries full jobDescribe + HR + company + encCoId, so most callers won't need detail.
- `51job hot` — same endpoint with empty keyword, returns 51job's recommendation feed.
- `51job detail <jobId>` — scrapes jobs.51job.com/x/<jobId>.html. Returns description,
welfare tags, category, address, age requirement, company meta.
- `51job company <encCoId>` — scrapes jobs.51job.com/all/co<encCoId>.html. Job cards
carry a `sensorsdata` JSON attribute, so we parse that instead of fragile DOM text.
Company meta from `.c-info.ellipsis`, intro from `#companyIntroRef`.
All four are Strategy.COOKIE + browser:true + navigateBefore:false. 51job sits
behind Aliyun WAF — bare curl / Node-side fetch always hits the slider challenge
(tried copying acw_sc__v2 + ssxmod_itna cookies to Node, WAF also checks TLS
fingerprint and JS execution). Only reliable path is browser-context fetch via
`page.evaluate(fetch(url, {credentials:'include'}))`, so utils.js exports
`pageFetchJson` that wraps this pattern + detects WAF-served HTML.
Verify fixtures included (~/.opencli/sites/51job/verify/*.json) — four adapters
pass `opencli browser verify 51job/<cmd>` with rowCount / columns / types /
patterns / notEmpty checks. Eyeballed jobId 171699769 on jobs.51job.com/suzhou
matches adapter output.
* fix(51job): tighten city handling and docs
* chore: regenerate cli-manifest.json after 51job column cleanup
* feat(weread): add ai-outline command for AI-generated book outlines
Two-step API flow: fetch chapter UIDs via authenticated chapterInfos,
then retrieve hierarchical AI outline from public outline endpoint.
Supports --depth to control detail level (2=topics, 3=key points,
4=full details) and --raw for structured output (chapter/idx/level/text)
suitable for programmatic consumption.
Closes#1140
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(weread): tighten ai-outline auth contract
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(weread/book): add fallback selectors for reader page without cover
When the private API session expires, `loadReaderFallbackResult` navigates
to the reader URL. The page now sometimes skips the cover/flyleaf and
renders reading content directly, causing the wait for cover/flyleaf title
selectors to time out.
- Add `.readerTopBar_title_link` to `page.wait` selector (always present)
- Use cascading `firstText()` for title: cover → flyleaf → outline → top bar
- Use cascading `firstText()` for author: cover → flyleaf → outline → document.title
Fixes#1137
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(weread/book): parse author from trailing title segments
* fix(weread): avoid author guess from document title
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(xiaoyuzhou): correct podcast-episodes API endpoint
The endpoint `/v1/podcast/listEpisode` returns 404. The correct
endpoint is `/v1/episode/list` (verified against Xiaoyuzhou iOS app
traffic; also matches the `episode-list` implementation in
ultrazg/xyz, a widely-used Xiaoyuzhou API wrapper).
Additionally, the server requires an `order` field in the request
body (returns 400 if omitted). Add `order: 'desc'` so callers get
the latest episodes first, matching typical UX for a podcast feed.
Before: podcast-episodes -> HTTP 404 for every podcast
After: podcast-episodes returns the N most recent episodes
Tested against real podcast 626b46ea9cbbf0451cf5a962
(张小珺|商业访谈录) — now returns 140 episodes correctly.
* test(xiaoyuzhou): lock podcast episodes endpoint
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat: agent-native retrospective — analyze / verify guards / fixture content checks
Post-mortem on slow 1point3acres + 51job adapter sessions, consolidated
into one PR. Scope is "reduce uncertainty and catch silent failures"
— the two things that sink agent success rate on first-time adapters.
Changes:
- `browser analyze <url>` — one command returns pattern (A/B/C/D),
anti-bot vendor (Aliyun/Cloudflare/Akamai/Geetest), nearest adapter,
and a single-sentence recommended_next_step. Replaces the three-step
open/wait/network recon loop when it can reach a confident verdict.
- `browser wait xhr <regex>` — poll for a specific XHR URL instead of
blind `wait time N`, so SPA data-arrival barriers are deterministic.
- Fixture `mustNotContain` / `mustBeTruthy` — catch two silent-failure
modes `notEmpty` misses: content contamination (sibling DOM bleed)
and `|| 0` / `|| false` fallbacks.
- `browser verify` post-success site-memory check + `--strict-memory`
— verify-green no longer hides the case where `~/.opencli/sites/`
was never written back. Memory only materializes if authors write it.
- CI: guard that committed `cli-manifest.json` matches a fresh build.
Main was already drifted (#1118 left stale ordering + a missing arg);
this PR regenerates the manifest and will catch the next drift.
Docs (opencli-adapter-author + opencli-autofix skills):
- `success-rate-pitfalls.md` — 10 concrete silent-failure scenarios
seen in real adapter sessions, each with defense via fixture /
adapter patterns.
- `autofix` gains discipline rule #6: verify pattern failure means
tighten the adapter, never loosen the fixture.
- `site-recon.md` leads with `browser analyze`; `api-discovery.md`
adds a §0 covering WAF vendor detection and cross-subdomain CORS
(the two gotchas that burned the 51job session).
- `wait time 3` → `wait time 2`, with `wait xhr` as the robust choice.
* fix: make output-dir defaults host-independent in manifest
Three adapters (chatgpt/image, gemini/image, instagram/download) baked
`path.join(os.homedir(), ...)` into the `default` field of their args.
The committed manifest therefore carried my personal `/Users/jakevin/...`
paths — which agents running on a different host saw as surprising
defaults. The drift guard I just added to CI caught it on the first run.
Runtime behavior is unchanged: each adapter still falls back to
`path.join(os.homedir(), …)` inside `func` when the kwarg is absent.
Only the displayed / registered default becomes a tilde-path.
* fix(cli): enforce strict-memory without fixture
* fix(browser): harden analyze and xhr guards
* fix(browser): fallback to interceptor buffer
* feat(verify): fixture-based value validation + skill docs for COOKIE pitfalls
`opencli browser verify` now loads `~/.opencli/sites/<site>/verify/<cmd>.json`
when present and validates row count / columns / types / patterns / notEmpty
against the live adapter output. Without a fixture, behavior is unchanged
(just runs the adapter and prints). New flags `--write-fixture`,
`--update-fixture`, `--no-fixture` seed / refresh / bypass the spec.
Motivation: previous verify only checked that the adapter exited 0 and
produced *something* — shape regressions (author name bleeding across rows,
a column silently becoming null after a site refresh, duplicated thread-level
time on every post) all passed "✓ Adapter works!" and shipped broken.
Skill doc updates (opencli-adapter-author):
- adapter-template.md: new "COOKIE adapter 骨架" section — HttpOnly +
dual-domain cookie read via `page.getCookies`, Node-side fetch for HTML
(explaining why `page.evaluate(fetch(...))` is the wrong tool when
`navigateBefore: false` or the response is non-UTF-8), and empty-state
sentinel row over `[]`
- api-discovery.md §4: note that BBS engines (Discuz/phpBB/vBulletin) set
auth cookies on the root domain + HttpOnly, so single-domain `getCookies`
calls silently miss them
- SKILL.md Step 10/12: make `--write-fixture` part of the runbook, forbid
debug dumps outside `~/.opencli/sites/<site>/fixtures/` or `/tmp/`
* fix(verify): support positional argv in fixture args + site-memory docs
Reviewer feedback blocker: fixture.args was Record<string, unknown>,
expanded as --key value only, so positional-subject adapters
(<tid>/<url>/<query>) couldn't be verified. Repo convention is
"主语优先 positional".
- verify-fixture.ts: args now accepts Record<string, unknown> | unknown[].
Object → --k v pairs; array → verbatim passthrough. New helper
expandFixtureArgs() centralizes the branching.
- cli.ts verify action: swap inline expansion for expandFixtureArgs().
- verify-fixture.test.ts: 6 new cases covering array form, mixed
positional+flag, empty shapes, passthrough stringification.
- site-memory.md: Layer 2 tree now lists verify/<cmd>.json; new schema
block distinguishes it from fixtures/<cmd>-<ts>.json; runbook timing
section gets a Step 10 verify-write row. Repo-tree debug-dump ban
clarified.
- adapter-template.md: new "Verify fixture" section with named-flag and
positional recipes, honest about --write-fixture only seeding named.
Smoke-tested 1point3acres/thread (positional <tid>): fixture round-trip
green (args=["1173710","--limit","2"]).
Two issues surfaced post-merge of #1110 by the Copilot reviewer:
1. Help text and docs advertise `video URL` as a valid input for
`opencli bilibili video <bvid>`, but the original implementation
delegated the whole input to `resolveBvid()` — which only recognises
bare `BV...` IDs and `b23.tv` short codes. A canonical bilibili URL
like `https://www.bilibili.com/video/BV.../` therefore got rewritten
to `https://b23.tv/www.bilibili.com/video/BV.../` and failed before
ever calling the view API.
Fix: pre-extract the BV ID from `bilibili.com/video/<BV>...` and
`bilibili.com/bangumi/play/<BV>...` URLs (www / m. / with or without
query string) in `video.js`, and fall through to `resolveBvid()`
only for bare BV IDs and `b23.tv` links.
2. `description` was being truncated to 200 chars with whitespace
collapsed before being returned. JSON/YAML consumers silently lost
the full `desc` value. Other bilibili adapters return raw fields.
Fix: return the full `d.desc` verbatim and let consumers/display
layers handle formatting.
Adds four regression tests for the URL paths (full URL, URL with query
string, m.bilibili.com mobile URL) and one for description integrity
(> 200 chars, preserved verbatim).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(deepseek): use position-based model selection instead of text matching
Fixes#1111
* fix(deepseek): preserve explicit instant model contract
* fix(deepseek): guard expert selector arity
---------
Co-authored-by: Benjamin Liu <beneecs@Benjamins-Mac-mini.local>
Co-authored-by: jackwener <jakevingoo@gmail.com>
Adds two additive columns to the Twitter read commands (search, timeline,
tweets, thread, likes):
- has_media: boolean — true if the tweet contains any photo, video, or GIF
- media_urls: string[] — photo URLs and mp4 variant URLs for videos/GIFs,
extracted from legacy.extended_entities.media (falls back to entities.media)
The INTERCEPT/COOKIE payloads already carry this data; this change only
extends the row-mapping layer, so no new network work is needed. Pattern
mirrors #465 (time column).
Shared extraction helper lives in clis/twitter/shared.js so all five
adapters stay consistent, with unit coverage for photo, video (mp4 variant
selection), animated_gif, entities.media fallback, and the empty case.
Closes#1107
* docs(skills): restore and rewrite opencli-usage as orientation skill
The original opencli-usage skill was deleted in PR #1094 as part of the
skill consolidation, but its role (top-level orientation to what opencli
is, how to discover adapters, what flags/env/formats are universal, and
which specialized skill to load next) was not covered elsewhere. Restore
it, but deliberately NOT as a verbatim copy:
- Drop the hand-maintained 100-adapter table. There are 100+ adapters
and the list moves every week — `opencli list -f json` is the source
of truth agents should call at the start of a task.
- Replace it with the meta-structure agents actually need: the three
pillars (adapters / browser driving / external CLI passthrough), the
strategy tags (PUBLIC | COOKIE | HEADER | INTERCEPT | UI | LOCAL)
and what each implies for prerequisites, universal flags (-f, -v),
output formats, env vars, self-repair hook, adapter authoring paths,
plugins, external CLI passthrough.
- Explicitly list the commands PR #1094 removed (`explore`, `record`,
`web` / `desktop` top-level groups) so agents don't attempt them.
- Cross-link to the four post-consolidation skills: opencli-browser
(ad-hoc driving), opencli-adapter-author (writing adapters),
opencli-autofix (repair flow), smart-search (search routing).
Adapter-author description updated to stop claiming it replaces
opencli-usage.
* docs(skills): tighten opencli-usage validate + doctor scope per review
- validate: describe as registry-level semantic check (description, domain,
pipeline step names, func|pipeline|_lazy presence, arg duplicates), not
YAML/TS syntax check — matches src/validate.ts
- doctor: narrow to browser-bridge diagnostic; PUBLIC/LOCAL adapters, list,
validate, verify, plugins, and external-CLI passthrough do not need it
Backfill the 1.7.6 section that was missing from the release PR.
Covers window lifecycle flags, selector-first browser interactions,
agent-native payload, compound form fields, three new adapter commands,
four fixes, skill doc updates, and extension 1.0.2 body-truncation
contract unification.
--live (OPENCLI_LIVE=1) keeps the automation window open after an adapter
command finishes, so agents or humans can inspect the page state. Default
behavior (immediate closeWindow) is unchanged.
--focus (OPENCLI_WINDOW_FOCUSED=1) surfaces the existing env-var toggle as a
CLI flag so users don't need to shell-export to see the window in foreground.
Both flags are parsed early in main.ts and stripped from argv, so they can be
placed anywhere on the command line and work on any subcommand (adapter or
browser).
Restore `skills/opencli-browser/SKILL.md`, deleted in #1094, rewritten for
the post-#1116 browser CLI surface: selector-first target contract,
`match_level { exact | stable | reidentified }`, compound fields for
date/time/select/file, structured error codes with `available` vs
`candidates`, new `find` / `extract` / `network --filter` commands,
html tree budgets, tabs/frames, cost guide, recipes, pitfalls.
Review tightened two contract-drift bugs before merge:
- `browser tab list` envelope field is `page`, not `targetId`
- `network --ttl` default is `24h`, not `~5min`
2 reviewers green (codex-mini1, First-principles-1); CI all-green.
* feat(browser): compound expansion + cascading stale-ref + bbox 0.99 dedup
Three agent-native upgrades inspired by browser-use, landed as one PR
because they share the same target / snapshot / find surface.
1. Compound expansion (compound.ts)
Date/time/datetime-local/month/week, select, and file inputs now
emit a `compound` JSON field on `browser find --css` entries with
format, current value, min/max (date family), full options list
+ selected (select), accept / multiple / files[] (file). Kills
the three biggest form-page failure modes (wrong date format,
guessed options, re-uploaded files) without extra round-trips.
2. Cascading stale-ref (target-resolver.ts)
Numeric ref resolution now walks three tiers before giving up:
exact → stable (tag + strong id match, soft signals drifted) →
reidentified (original ref lost, fingerprint uniquely found a
live element; re-tag + refresh identity). Every success envelope
carries `match_level` so callers can tell which tier matched.
SPA re-renders / i18n label swaps no longer stall agents.
3. BBox 0.99 containment for interactive descendants (dom-snapshot.ts)
Adds a second dedup tier on top of the existing 0.95 non-interactive
one. When a parent is a propagator (tag a/button OR role button/
link/menuitem/tab/option) and a child is interactive but
undistinctive (no aria-label/id/testid/name/form-control), fold
it into the parent — removes `[1]<button> [2]<svg> [3]<span>`
noise on icon buttons.
Tests: 287/287 pass (src/browser + src/cli.test.ts). Typecheck clean.
* fix(browser): address reviewer blockers on PR #1116
- compound select: walk ALL options to collect selected labels, not just
the first 50 we serialize. Fixes dropdowns where the selected entry
sits past COMPOUND_SELECT_OPTIONS_CAP (e.g. country lists, timezones)
reporting current: "" even though the user picked a valid option.
- match_level: propagate the cascading match tier
(exact / stable / reidentified) through IPage.click/typeText/scrollTo,
BasePage, and the cli command envelopes (click / type / select /
get text|value|html|attributes). Agents now see in JSON that the
resolver had to fall back, instead of the tier being swallowed.
- compound contract is now also emitted by `browser state`
(per-ref compounds: sidecar) and by `browser get html --as json`
(compound field on each node), not only by `browser find --css`.
Closes the gap where agents using the default snapshot still
round-tripped `find` for every date / select / file control.
Adds targeted regression tests for each blocker + updates cli.test.ts
mocks to the new envelope shape.
The resolveTwitterQueryId() function in shared.js fetches an external JSON
file from GitHub without a timeout. If the network request stalls, the
function never resolves and the twitter article command hangs indefinitely.
Add a 5-second AbortController timeout so the fetch fails fast and falls
back to the local script-scanning strategy. This fixes the reported hang
when opencli twitter article loses network connectivity.
* feat(bilibili): add video command
Add `opencli bilibili video <bvid|url|short-link>` to fetch one
video's metadata via the public /x/web-interface/view endpoint.
Returns title, author, category, publish time, duration, view /
danmaku / reply / like / coin / favorite / share counts, parts,
thumbnail, and description as a key/value table.
Reuses `resolveBvid` and `apiGet` from clis/bilibili/utils.js to
stay consistent with the existing bilibili adapters
(subtitle/search/etc. all follow the same navigate + apiGet
pattern). Non-zero API codes surface as CommandExecutionError.
Fills a visible gap: existing bilibili commands cover search,
hot, subtitle, ranking, user-videos etc., but nothing returned
metadata for a single video — `web read` only gets a DOM shell.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(manifest): register bilibili video command
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* WIP: deepseek file upload (blocked by 30s idle timeout)
* feat(deepseek): add file upload support via --file flag
Closes#1092
* fix(deepseek): use native file input path for --file
---------
Co-authored-by: Benjamin Liu <beneecs@Benjamins-Mac-mini.local>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(youtube): fall back to Videos tab when Home tab has no videos (#1108)
Some channels have no video shelves on their Home tab, causing
`opencli youtube channel <id>` to return an empty `recent_videos` list
even though the channel has videos visible in the browser.
When the Home tab extraction finds zero videos, the command now makes
a second InnerTube browse request to the Videos tab and extracts from
its richGridRenderer format.
* fix(youtube): make Videos tab fallback locale-safe
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(browser): selector-first find + get/click/type/select with JSON envelope
A2: new `browser find --css <sel>` — structured JSON (matches_n + entries[]) so
agents can go from semantic selector directly to a list of candidates without
parsing free-text snapshot output. Per-entry shape: nth/ref/tag/role/text/attrs/
visible. Attr whitelist kept small (11 high-signal fields), invisible elements
still returned so agents can reason about offscreen vs missing.
A3: get text/value/attributes now accept a selector-first <target> (numeric ref
OR CSS) and emit `{value, matches_n}`. Bonus scope (approved by reviewers):
click/type/select share the same contract with `--nth <n>`, emitting
`{clicked|typed|selected, target, matches_n, ...}` on success.
Unified structured error envelope across all selector-first commands:
{ error: { code, message, hint?, candidates?, matches_n? } }
with codes invalid_selector / selector_not_found / selector_ambiguous /
selector_nth_out_of_range (CSS) plus not_found / stale_ref (numeric ref).
Write commands reject multi-match CSS without `--nth` as selector_ambiguous;
reads default to "first match wins" but always expose matches_n so agents
notice ambiguity. `resolveTargetJs` is the single source of truth; click /
typeText / scrollTo share a `runResolve` helper in BasePage.
No back-compat shims per design directive.
125 targeted tests green; tsc clean.
* fix(browser): unify selector surface + allocate fresh refs in find
Two blockers from PR #1112 review:
1. First-principles-1 (blocker): `browser find --css` now allocates fresh
numeric refs for untagged matches. It scans `window.__opencli_ref_identity`
(and any stray `data-opencli-ref` attrs) for the current max, allocates
`max+1` upward, writes `data-opencli-ref` on the element, and populates
the identity map with the same fingerprint shape snapshot uses (tag,
role, text, ariaLabel, id, testId). `find -> click <ref>` now works on
fresh pages without requiring `browser state` first. Type changed from
`ref: number | null` to `ref: number`.
2. codex-mini1 (blocker): removed the `isCssLike` regex
(`^[a-zA-Z#.\[]`) in `resolveTargetJs`. Valid selectors like `:root`,
`:has(...)`, `*` used to short-circuit to "Cannot parse target" before
reaching `querySelectorAll`, so `find --css` accepted them but
`get/click/type/select` did not. Now: numeric → ref path, everything
else → querySelectorAll, and the browser parser decides. Same selector
surface across all selector-first commands.
Tests added:
- target-resolver: pseudo-selectors flow into CSS branch (not rejection)
- find: ref allocation writes attribute + identity map; fingerprint shape matches resolver
- cli: find envelope now expects numeric refs
127 targeted tests green; tsc clean.
* feat(browser): agent-native payload — network bodies, html tree budgets, extract command
Three fixes/additions driven by agent-usage gaps, as one complete change:
- network (P0 fix): lift silent 4000-char body truncation in CDP + extension
paths to an 8MB memory-guard cap, and surface body_truncated / body_full_size
/ body_truncation_reason in the --detail envelope so the agent sees when a
body was cut. List view also exposes body_truncated_count and per-entry flag.
Adds --max-body flag for explicit caller-side capping.
- get html --as json (P1): add --depth / --children-max / --text-max budget
knobs on the tree serializer, plus a truncated={depth,children_dropped,
text_truncated} envelope that only appears when a budget is hit. Lets the
agent narrow DOM output without walking away empty-handed.
- extract (P2 new command): agent-native article/content channel. Scope →
denoise (strip nav/header/footer/scripts/forms/etc.) → HTML→markdown via
existing htmlToMarkdown → paragraph-boundary-aware chunk with stateless
next_start_char resume cursor. Agents no longer misuse `get html` to read.
* fix(browser): unify body-truncation signal contract across raw/detail/fallback
Addresses review blockers on #1104:
- NETWORK_INTERCEPTOR_JS fallback no longer silently drops bodies above the
per-entry cap. Raised cap to 1 MiB (ring stays at 200 entries), and on
overflow keeps the string prefix + sets `bodyTruncated` / `bodyFullSize`
so `browser network` propagates the same agent-visible signal the CDP /
extension paths emit.
- `CachedNetworkEntry` schema switches from internal camelCase
`bodyTruncated` to the user-facing `body_truncated` / `body_full_size`
fields. `--raw` emits cache entries verbatim, so this removes the
snake_case/camelCase split across list / --detail / --raw.
- Adds a `--raw` truncation-contract test that also asserts the camelCase
fields do not leak through.
Agents often know what fields a target request's body should contain
but not which captured request carries it. --filter lets them declare
the field set and get back only matching entries.
Matching is "any-segment": a field matches when it equals any segment
name of any inferShape() path (ignoring root $, array indices, and
bracket-quoted key syntax). Multiple fields AND together. Case-sensitive.
- invalid_filter for empty / commas-only values
- invalid_args when combined with --detail (mutually exclusive)
- 0 matches is a valid empty result, not an error
- persisted cache stays unfiltered so later --detail lookups still resolve
Envelope gains `filter` (echo) and `filter_dropped` (count of entries
passing the static-resource filter but not --filter). Existing --raw
and --all compose normally.
* feat(browser): remove silent html truncation, add --as json tree output
`browser get html` had two agent-hostile defaults:
1. A silent 50000-char cap on the returned HTML — agents that got a
truncated page had no signal they were looking at half the DOM.
2. Only raw HTML string output, forcing agents to re-parse for
structured extraction.
Changes:
- Default output is now the full outerHTML, no truncation
- `--max <n>` opts in to a character cap; when the cap actually
trips, the HTML is prepended with
`<!-- opencli: truncated N of M chars; re-run without --max ... -->`
so agents always see the signal
- `--as json` returns `{selector, matched, tree}` where `tree` is
`{tag, attrs, text, children}` recursively. `matched` is the full
count of selector matches so agents know when more elements exist
beyond the first. `text` is the node's own direct text children,
whitespace-collapsed; child elements live in `children`.
- `--selector` not matching any element now emits structured
`{error:{code:"selector_not_found", ...}}` with a non-zero exit
code, in both raw and json modes (was `(empty)` stdout previously,
indistinguishable from empty element)
- Invalid `--as` / negative `--max` emit structured
`invalid_format` / `invalid_max` error codes
Extracted the tree serializer as `src/browser/html-tree.ts` so the
JS expression can be unit-tested against a DOM stub.
* fix(browser get html): structured errors for invalid selector & strict --max
Both edges previously bypassed the structured-error contract introduced in
#1102, which agents rely on for branching:
- Invalid CSS selector: querySelector(All) would throw SyntaxError through
page.evaluate into the generic exception path. Wrap the lookup in try/catch
inside page context for both raw and --as json paths; surface as
{error:{code:"invalid_selector", message}} + non-zero exit.
- --max validation: parseInt silently accepted "1.5" -> 1 and "10abc" -> 10.
Switch to a strict /^\\d+$/ check so fractional, negative, and non-numeric
values all return {error:{code:"invalid_max"}}; validation runs up front so
bad values never reach the page.
Covered by new unit tests in cli.test.ts (fractional, non-numeric, invalid
selector on raw + json) and html-tree.test.ts (SyntaxError -> invalidSelector
envelope).
Co-authored-by: freemandealer <freeman.zhang1992@gmail.com>
---------
Co-authored-by: freemandealer <freeman.zhang1992@gmail.com>
* feat(browser): rewrite network command for agent-native discovery
Replace the index-based list + pretty-printed --detail flow with a
structured JSON interface built around stable keys, body-shape previews,
and a persistent capture cache. Agents can now reference captured
requests by operationName (GraphQL) or `METHOD host+pathname` (REST)
instead of array indexes that shift on every rerun.
- `browser network` now emits JSON: `{workspace, captured_at, count,
filtered_out, entries: [{key, method, status, url, ct, size, shape}],
detail_hint}` — no body payloads by default
- Shape inference (src/browser/shape.ts) walks response JSON into a
flat path -> descriptor map with depth cap 6 and a 2KB budget per
entry, so agents see structure without paying body tokens
- Stable key generator (src/browser/network-key.ts) derives
`operationName` from graphql URLs and `METHOD host+pathname`
elsewhere, disambiguating collisions with `#N` suffixes
- Persistent cache (src/browser/network-cache.ts) snapshots every
capture to `~/.opencli/cache/browser-network/<workspace>.json` with
a 24h TTL, so `--detail <key>` survives later commands
- `--detail <key>` returns `{key, url, method, status, ct, size, shape,
body}` with structured error codes (cache_missing / cache_expired /
cache_corrupt / key_not_found, the latter including available_keys)
- Add `--raw` for agents that want every full body inline, `--ttl` for
cache lookups
- Update opencli-adapter-author + opencli-autofix skill docs to
reference `--detail <key>` and the shape-first discovery flow
Supersedes the cache prototype in #1051.
Co-authored-by: freemandealer <freeman.zhang1992@gmail.com>
* fix(browser): structured errors for capture/save, shape budget guard
Self-review findings on the network refactor:
- captureNetworkItems throwing (browser crashed / CDP dropped) now emits
`error.code: capture_failed` on stdout rather than leaking a bare
stderr line from browserAction's generic handler — agents get a
parseable JSON blob on every failure path, matching the design goal.
- saveNetworkCache throwing (disk full, read-only path) is a soft
failure: the captured data is already in hand, so surface a
`cache_warning` field in the envelope and keep going instead of
aborting. `--detail` lookups on that run will miss the cache but the
listing still reaches the agent.
- shape.ts: guard the sub-walk on `add()`'s return value so the
"budget hits on the array/object descriptor itself" path can never
emit a stray child without its parent marker.
- network-key.ts: document that `#N` suffixes start at `#2` — the first
occurrence stays bare, there is no `#1`. Matches test + code.
Added regression tests: `capture_failed` on readNetworkCapture throw,
`cache_warning` on persistence failure, shape budget hit on array descriptor.
Co-authored-by: freemandealer <freeman.zhang1992@gmail.com>
---------
Co-authored-by: freemandealer <freeman.zhang1992@gmail.com>
* feat(twitter): add tweets command for fetching a user's recent posts
Adds `opencli twitter tweets <username> [--limit N]` to pull a user's
most recent chronological tweets via the UserTweets GraphQL endpoint.
Long posts resolve via note_tweet, pinned entries are skipped, and
retweets are flagged. QueryIds resolve dynamically through
`resolveTwitterQueryId` with hardcoded fallbacks.
* fix(twitter): expose retweet flag in tweets output
* refactor: consolidate 6 skills into 3, remove mechanical commands
Replaces opencli-oneshot / opencli-explorer / opencli-browser /
opencli-usage with a single opencli-adapter-author skill that takes
the AI agent end-to-end: site recon, API discovery, field decoding,
adapter coding, and `opencli browser verify`.
Removes the mechanical commands (`explore`, `synthesize`, `generate`,
`cascade`, `record`) and their src/tests — they were codegen scaffolding
meant for agents, which the new skill handles more flexibly via
`opencli browser` primitives.
Skill highlights:
- Top-level decision tree + 12-step runbook
- 5 site patterns (SPA / SSR / JSONP / Token / Streaming)
- 5-layer API discovery (network → initial state → bundle → token → interceptor)
- Field decode playbook (self-explanatory → codes → sort-key comparison)
- Output design guide (columns, types, order, ≤15 per adapter)
- Two-layer site memory: in-repo seeds for eastmoney/xueqiu/bilibili/tonghuashun
plus local `~/.opencli/sites/<site>/` runtime workspace
Kept skills: opencli-autofix (now points to adapter-author for rewrites),
smart-search. Kept primitives: `browser *`, `doctor`, `list`, `validate`,
`verify`, `<site> <cmd>`, `plugin *`, `completion`.
No backward compatibility shims. Full test suite (1605 tests) passes.
* review fixes: honest coverage, hard memory-hit path, typo, stale docs
- site-memory hit path no longer jumps to writing adapter; forces Step 5
endpoint re-verification + Step 7 field check, and 30-day expiry
- site-memory.md now specifies exact schemas for endpoints.json /
field-map.json / notes.md / fixtures + write-back timing rules
- coverage-matrix.md marks unverified patterns as 🟡 with an evidence
section citing coingecko dry run + PR #1091 eastmoney + bilibili
- eastmoney seed typo: resolveSecids -> resolveSecid (and splitSymbols)
- docs/developer/ai-workflow.md rewritten to teach the adapter-author
skill + opencli browser * primitives (dropped generate/synthesize/
cascade/explore references)
- ts-adapter.md, getting-started.md, CHANGELOG.md:87 updated to point
at opencli-adapter-author
* fix(ci): resync package-lock + drop stale built-in list reference
- Regenerate package-lock.json to restore @emnapi/core + @emnapi/runtime
entries that got dropped during the rebase — `npm ci` was failing on all
CI jobs (build / audit / docs-build / bun-test / unit-test)
- docs/guide/getting-started.md: built-in list dropped `explore`, now
reads (list, validate, verify, browser, doctor, plugin...)
* fix(ci): restore package-lock.json from main (unrelated lockfile churn)
Mirror the remaining 13 read-oriented adapters and the shared _secid.js
helper from the author's local workspace into the repo, so that
clis/eastmoney/ becomes the full Phase A codegen regression oracle
described in OpenCLI Improvement Spec v1.1 §B.10.
Total repo oracle after this PR: 14 adapters under clis/eastmoney/
(hot-rank.js already exists; this PR adds the other 13) plus the
_secid.js normalize helper.
Covers the two schema-expressiveness gaps discovered during prep:
- CSV row_format: kline.js decodes "YYYYMMDD,open,close,..." strings
- :row_index source: convertible.js derives rank = i + 1
_secid.js is the canonical example of the v1.1 §B.7 helper contract
(pure normalize/derive function, serializable I/O, no env/fs/net/session
access, does not drive pagination/retry/fallback).
This PR is oracle-only, carries no framework changes. Phase A framework
PR depends on this merging first so the codegen diff target is stable.
Refs: task #177 / spec v1.1 §B.10
* feat(download): show saved file path in web read and weixin download output
Closes#1038
* test(download): cover saved article path
---------
Co-authored-by: Benjamin Liu <beneecs@Benjamins-Mac-mini.local>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(browser): add cross-origin iframe support via CDP execution contexts
Enable interaction with cross-origin iframes through CDP's execution
context mechanism, without requiring content scripts or all_frames.
- Track frame execution contexts via Runtime.executionContextCreated events
- Add 'frames' action to list all child frames (including cross-origin)
- Support frameIndex in 'exec' action to evaluate JS in specific frames
- Add Page.frames() and Page.evaluateInFrame() APIs for CLI consumers
- Tag cross-origin iframes with [F0]/[F1] indices in DOM snapshots
- Add Page.getFrameTree to CDP allowlist
Closes#1077
Change-Id: Id03361ddb616912dff3bfa8e59e8b68716de590b
* fix(browser): align cross-origin iframe routing contract
* fix(browser): unify iframe frame-index routing
---------
Co-authored-by: xuezhangying <xuezhangying@bytedance.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
From first principles, `opencli browser` commands exist for AI Agents to
operate websites through the browser skill. Reframe both READMEs to reflect
this: show users how to install the skill into their AI agent and describe
tasks in natural language, rather than listing raw CLI commands.
* feat(twitter): rewrite lists via GraphQL + add list-tweets
The DOM-scraping / detail-click approach in PR #1053 remained fragile
against X's frequent overview-page rendering changes and slow (N+1 page
loads per list). Rewrite `twitter lists` to call
`ListsManagementPageTimeline` GraphQL directly — one request returns all
owned + subscribed lists with id/name/member_count/subscriber_count/mode.
Also add `twitter list-tweets <listId>` for pulling the tweet stream from
a list, completing the read-side chain (lists → pick an id → list-tweets).
- lists: drop positional `user` arg (GraphQL returns only logged-in
user's lists), add `id` column, change followers to exact integer from
subscriber_count.
- list-tweets: same GraphQL pattern as bookmarks/likes (BEARER + ct0 +
dynamic queryId with static fallback + cursor pagination).
- Delete obsolete lists-parser.js and lists.d.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(twitter): add list-add / list-remove with Save-button commit
Two new commands to toggle list membership. X's list dialog uses a
"click-to-stage, Save-to-commit" pattern — clicking a row only updates
optimistic UI; the actual POST fires when the user clicks the top-right
"Save" button. Pressing ESC or the close-X silently cancels the change.
Implementation:
- Resolve listId → name via ListsManagementPageTimeline GraphQL, so we
match the dialog row by name (dialog rows have no data-testid listId).
- Open profile page → DOM click "…" menu → "Add/remove from Lists".
- Scroll dialog to locate target row (virtualized list).
- page.nativeClick on row — trusted CDP Input.dispatchMouseEvent fires
React's onclick, flips aria-checked (.click() alone does not suffice;
X ignores non-trusted events for list mutations).
- page.nativeClick on the Save button — commits to server.
- Verify by re-fetching ListsManagementPageTimeline and diffing
member_count: success only if N→N±1. No silent successes.
This fixes the pattern where batch `list-add` calls returned success for
every user but committed zero to the server (optimistic UI lied).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: stabilize twitter list manifest and query ids
* docs: add twitter list command discoverability
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(zsxq): separate content field from title, remove title truncation
- Split getTopicText to return only title, add getTopicContent for body text
- Remove .slice(0, 120) that was truncating titles
- content field now contains full body text instead of duplicating title
* fix(zsxq): preserve title fallback for body-only topics
---------
Co-authored-by: huzekang <huzekang@opencode.ai>
Co-authored-by: jackwener <jakevingoo@gmail.com>
PR #1059 migrated xiaoyuzhou from SSR scraping to authenticated API.
The E2E tests run without credentials, producing exit code 78
(CONFIG_ERROR). The existing `isExpectedChineseSiteRestriction` guard
only caught FETCH_ERROR, PARSE_ERROR, and NOT_FOUND — not config
errors from missing auth credentials.
Three improvements from the design debt audit:
1. Remove deprecated `tabId` field and `getActiveTabId()` method
- Delete `tabId` from DaemonCommand (daemon-client.ts) and Command (protocol.ts)
- Delete `getActiveTabId()` from IPage interface (types.ts) and Page class (page.ts)
- Update extension resolveCommandTabId() to remove legacy fallback
- Update handleTabs select case to remove tabId check
- The tab→page migration is now complete
2. Unify argument validation into single code path
- Remove `normalizeArgValue()` from commanderAdapter.ts
- Commander adapter now passes raw values to prepareCommandArgs()
- All coercion (bool, int, number) and validation (required, choices)
happens once in coerceAndValidateArgs() in execution.ts
- Eliminates duplicated boolean normalization
3. Remove dead plugin filesystem wrappers
- Delete `promoteDir()` — never called in production code
- Delete `replaceDir()` — thin wrapper over beginReplaceDir, never called
- Remove corresponding test-only exports and tests
- Rename PromoteDirFsOps → ReplaceDirFsOps to match remaining usage
- Transaction infrastructure (runTransaction, beginReplaceDir,
beginReplaceSymlink) retained — used by publishStandalonePlugin
and publishMonorepoPlugins for atomic multi-step operations
* fix(extension): per-workspace idle timeout for browser sessions (#1058)
The global 30s WINDOW_IDLE_TIMEOUT was too aggressive for interactive
`opencli browser` commands where users type manually between invocations.
- browser:*/operate:* workspaces now default to 10 min idle timeout
- Adapter workspaces keep the existing 30s timeout
- Support custom timeout via OPENCLI_BROWSER_TIMEOUT env var (seconds)
or command-level idleTimeout parameter
- Surface sessionExpired warning when a new window is created after
the previous session timed out
- Fix stale comment (said 120s, actual was 30s)
Closes#1058
* fix: resolve sessionExpired double-delete race and timeout override lifecycle
Addresses @codex-coder review blockers:
1. sessionExpired flag was never set because getAutomationWindow()
consumed expiredWorkspaces before handleCommand() could check it.
Fix: use .has() in getAutomationWindow, only .delete() in handleCommand.
2. workspaceTimeoutOverrides was never cleaned up — once set, it
persisted until extension restart. Fix: clear override on idle
timeout expiry, explicit close-window, and borrowed-session detach.
Adds 5 tests covering:
- browser:* uses 10min timeout (not 30s)
- sessionExpired flag is set and consumed correctly
- workspaceTimeoutOverrides cleared on idle expiry
- workspaceTimeoutOverrides cleared on explicit close
- idleTimeout from command applies to workspace override
* refactor: remove sessionExpired warning per product decision
@WAWQAQ decided session-expired warning is not needed.
Remove expiredWorkspaces tracking, sessionExpired flag from protocol,
and related CLI-side warning code. Keep per-workspace timeout and
override lifecycle cleanup.
* fix: clean up workspaceTimeoutOverrides on user-initiated window close
The windows.onRemoved listener was missing workspaceTimeoutOverrides
cleanup, causing stale overrides to persist across sessions when users
manually close the automation window.
* fix(xiaoyuzhou): migrate from broken SSR scraping to authenticated API (fixes#1023)
Xiaoyuzhou removed SSR rendering — /podcast/<id> and /episode/<id> pages
now return 404, breaking fetchPageProps() which scraped __NEXT_DATA__.
Migrate podcast, podcast-episodes, episode, and download commands to use
the existing authenticated API client (requestXiaoyuzhouJson) that
transcript.js already uses successfully.
Changes:
- podcast.js: use /v1/podcast/get API endpoint
- podcast-episodes.js: use /v1/podcast/listEpisode API endpoint
- episode.js: use /v1/episode/get API endpoint
- download.js: use /v1/episode/get API endpoint
- utils.js: remove unused fetchPageProps, keep format helpers
- Update all affected tests (download.test.js, utils.test.js)
- Change strategy from PUBLIC to LOCAL (requires credentials)
* fix(xiaoyuzhou): align local strategy contract
* fix(xiaoyuzhou): align local api metadata
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
#1025 merged hot-rank adapters for eastmoney/tdx/ths but the
accompanying docs were missing. This breaks the Doc Check CI
workflow on every PR ('--strict' mode, exits non-zero when
`scripts/check-doc-coverage.sh` finds adapters without docs),
blocking merges across the board.
Adds a doc page per adapter, registers them in the adapters
index table, and adds sidebar entries in the VitePress config.
* feat: add hot stock ranking adapters for eastmoney, tdx, ths
Add three new site adapters for Chinese stock hot rankings:
- eastmoney/hot-rank: 东方财富热股榜
- tdx/hot-rank: 通达信热搜榜
- ths/hot-rank: 同花顺热股榜
All use Strategy.COOKIE browser mode with page.evaluate() DOM scraping.
Each includes co-located tests (13 tests total, all passing).
* fix(tdx,ths): add symbol validation and deduplication in evaluate()
Add seen Set for deduplication and skip entries with empty symbol/name,
matching the pattern already used in eastmoney/hot-rank.js.
* fix: refine hot-rank selectors based on browser inspection
- eastmoney: use table.rank_table tbody tr with td index-based extraction,
fix name from a[title] to avoid post content contamination
- tdx: use div.top-cell[data-code] data attributes for reliable extraction,
add tags column from div.tips-item.gnbk
- ths: use card-based layout selectors, remove price column (not in UI),
extract tags from div.tag.PFSC-R
* fix(hot-rank): align tdx and ths columns with actual output
* fix: register hot stock ranking adapters
---------
Co-authored-by: dengjingren <dengjingren@cn.wilmar-intl.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix: preserve network capture and surface extension mismatch diagnostics
Older Browser Bridge installs can still connect to the daemon while
missing two capabilities we now rely on: the network-capture actions
and the extension version handshake. That created three user-facing
failure modes with real impact:
1. `opencli explore ...` crashed with `Unknown action: network-capture-start`
against an old extension, so exploration stopped before any site
analysis finished.
2. `opencli doctor` and `opencli daemon status` could show a healthy
connection even when the extension never reported a version, which
hid the compatibility problem and sent users toward the wrong fix.
3. After reloading a new extension, `explore` could still report
`Endpoints: 0 total, 0 API` because `handleNavigate()` detached the
debugger before top-level navigation and cleared the active network
capture state right before the page load we needed to observe.
Fix this in two layers:
- Teach `Page` to treat unsupported `network-capture-*` actions as an
old-extension compatibility case. It now warns once, memoizes the
unsupported state, and returns empty capture data instead of throwing.
- Teach `doctor` and `daemon status` to treat "connected but version
unknown" as a warning instead of a healthy state, so version-handshake
failures are visible immediately.
- Preserve the debugger attachment while network capture is armed, so
the initial navigation keeps the capture state alive and the extension
can record requests from the first page load.
Before:
- `opencli explore ...` -> `Error: Unknown action: network-capture-start`
- `opencli doctor` -> `[OK] Extension: connected` / `Everything looks good!`
- `opencli daemon status` -> `Extension: connected` even when the
extension version was missing
- `opencli explore ...` after reloading the extension -> `Endpoints: 0 total, 0 API`
After:
- `opencli explore ...` on an old extension -> warns once and continues
- `opencli doctor` -> `[WARN] Extension: connected (version unknown)`
- `opencli daemon status` -> `Extension: connected (version unknown)`
- `opencli explore ...` on the reloaded extension keeps network capture
armed across navigation instead of clearing it before the page load
* fix: reset network capture flags on closeWindow()
Prevents stale _networkCaptureUnsupported flag from persisting across
sessions when the user reinstalls or reloads the extension mid-session.
* fix: startNetworkCapture returns boolean to prevent false-positive on old extensions
When the extension doesn't support network-capture-*, startNetworkCapture()
now returns false instead of silently resolving. This ensures browser open/
network correctly falls back to the JS interceptor on old extensions.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix: auto-restart stale daemon and improve connection error messages
When daemon is running but extension never connected (stale daemon started
before extension was installed), the CLI now auto-restarts the daemon to
give the extension a fresh WebSocket endpoint, instead of just waiting
and then telling the user to install the extension.
Also improves error messages across cli.ts, bridge.ts, and doctor.ts to
suggest "opencli daemon stop && opencli doctor" as the quick fix, since
that's what actually resolves the issue.
* fix: version-aware stale daemon detection and improved error messages
- Daemon /status now includes `daemonVersion` field
- bridge.ts: when daemon is running but extension not connected, checks
daemonVersion vs CLI version. Only auto-restarts if version mismatch
(stale daemon from older CLI). Same-version daemon shows improved error
message with "opencli daemon stop && opencli doctor" hint.
- doctor.ts: explicitly identifies stale daemon (version mismatch) in
diagnostics report, shows daemon version in status line
- cli.ts: error message changed to suggest "opencli daemon stop && opencli doctor"
* fix: treat missing daemonVersion as stale, verify shutdown before respawn
- Missing daemonVersion (pre-version daemon) is now treated as stale,
covering the most common user scenario (old daemon without version field)
- After requestDaemonShutdown(), poll until daemon actually stops (port
released) before spawning new one, with 3s timeout
- If shutdown request fails, log warning instead of silently proceeding
- doctor.ts also treats missing daemonVersion as stale with clear message
* fix: fail explicitly when stale daemon replacement fails
- If shutdown request fails or port isn't released within 3s, throw
'Stale daemon could not be replaced' instead of blindly spawning on
an occupied port
- Add tests for all three stale-daemon branches: same-version (no
restart), missing daemonVersion (stale), mismatched version (stale)
* fix: use type-based error dispatch in browserAction instead of string matching
browserAction() now checks `instanceof BrowserConnectError` first and
renders both message and hint, instead of string-matching on message
content. This ensures stale daemon errors ("Stale daemon could not be
replaced") surface the actionable hint to the user.
* feat(grok): add image command for grok.com image generation
Add `opencli grok image <prompt>` which submits a prompt via the existing
grok.com browser session and returns the generated image URLs from the
latest assistant bubble.
Because assets.grok.com URLs are gated by Cloudflare and cannot be
downloaded with a plain HTTP client, the --out flag triggers an in-page
fetch(credentials: 'include') so the browser session's cookies and
referer are attached, then writes the decoded blob to disk.
Flags:
- --new start a fresh chat before sending
- --timeout max seconds to wait for the image (default 240)
- --count minimum number of images to wait for before returning
- --out directory to save downloaded images
Ships with unit tests for the helpers (isOnGrok, normalizeBooleanFlag,
dedupeBySrc, imagesSignature, extFromContentType, buildFilename).
* fix(grok): harden image composer and bubble detection
* fix(grok): harden image flow and docs
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
When ChatGPT macOS app is set to Chinese language, the "Options"
button label becomes "选项". This change checks for both English
and Chinese labels to find the button.
Co-authored-by: mad <mademing@maddeMac-mini.local>
Co-authored-by: Claude <noreply@anthropic.com>
* feat: implement Ref-Backed Locator for browser actions
Introduces a unified target resolution system with fingerprint
verification and structured error diagnostics.
Snapshot phase:
- Each interactive element now gets a fingerprint (tag, role, text,
ariaLabel, id, testId) stored in window.__opencli_ref_identity
- Zero overhead: metadata is already available during DOM walk
Resolution phase (new target-resolver.ts):
- Numeric input → ref path with fingerprint verification
- CSS-like input → querySelectorAll with uniqueness check
- No more silent first-match: ambiguous selectors are rejected
Error model (new target-errors.ts):
- stale_ref: element identity changed since snapshot
- ambiguous: CSS selector matched multiple elements (with candidates)
- not_found: element not in DOM or invalid input
- All errors include actionable hints for AI agents
base-page.ts:
- click() and typeText() now use two-phase resolve-then-act
- Existing CDP fallback for click preserved
* feat: migrate scrollTo to unified resolver pipeline
scrollTo now uses the same two-phase resolve-then-act pattern as
click and typeText, getting fingerprint verification and structured
error diagnostics (stale_ref/ambiguous/not_found) for free.
* fix: address review — stronger fingerprint verification & surface TargetError in CLI
1. Fingerprint verification now uses the full identity vector (tag, id,
testId, ariaLabel, role, text) instead of just tag/role/text. Strong
identifiers (id, testId) are decisive; remaining signals use majority
voting. Fixes false negatives where same-tag elements swapped.
2. browserAction() now renders TargetError with code, hint, and
candidates list instead of just the message string.
* fix: migrate get/select/type-autocomplete to unified resolver
- browser get text/value/attributes now resolve via resolveTargetJs
instead of raw querySelector, getting fingerprint verification and
structured errors for free
- browser select uses selectResolvedJs on __resolved element
- type command's autocomplete detection uses isAutocompleteResolvedJs
on the already-resolved element
- Fix empty-string text prefix match: fp.text="Login" + text="" no
longer falsely passes fingerprint check
- docs/index.md: update feature cards to match README Highlights
- docs/zh/index.md: sync Chinese feature cards
- docs/guide/getting-started.md: align Highlights section
- README.zh-CN.md: rename "为什么是 OpenCLI" to "亮点", align with EN
* fix: remove duplicate extension zip from releases
The release and build-extension workflows were creating both
opencli-extension.zip and opencli-extension-v{version}.zip (identical
content), causing both to be uploaded. Keep only the versioned filename.
* docs: update extension zip filename to versioned format
Update all references from opencli-extension.zip to
opencli-extension-v{version}.zip to match the workflow change.
* feat(mubu): add mubu (mubu.com) adapter with 5 commands
Commands: doc, docs, notes, recent, search.
- Uses COOKIE strategy; API calls via in-page XHR with Jwt-Token
from localStorage (matches the web app's own mechanism).
- Renders node trees to Markdown (default) or plain text;
supports tables, tasks, images, emoji, mentions, strikethrough,
underline, and nested structures.
- notes supports flexible time ranges: single day, month, year,
or custom --from/--to spans, plus a --list overview mode.
- search returns full-text matches with hit count and snippets
for both folders and documents.
* fix(manifest): register mubu commands in runtime manifest
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* refactor: unify OPENCLI_VERBOSE and DEBUG=opencli into one mechanism
Three debug output levels (verbose/debug/diagnostic) was redundant.
Merge DEBUG=opencli into OPENCLI_VERBOSE so `-v` flag controls all
verbose/debug output through a single mechanism.
- log.verbose() now checks both OPENCLI_VERBOSE and DEBUG=opencli
- log.debug() becomes an alias for log.verbose() (backward compat)
- boss/utils.js verbose helper simplified to check OPENCLI_VERBOSE only
- DEBUG=opencli still works as fallback (no breaking change)
* fix(boss): preserve debug fallback for verbose logs
The error path in executeCommand did not call page.closeWindow(),
leaving the automation window open until the extension's idle timer
fires. On Windows, MV3 service worker suspension makes this timer
unreliable, causing windows to linger indefinitely.
Now closeWindow is called after diagnostic collection but before
rethrowing, ensuring the window is closed on both success and failure.
Add missing env vars to both README and README.zh-CN:
- OPENCLI_SKIP_FETCH: skip adapter sync on global install
- OUTPUT: override output format (json/yaml/table)
- DEBUG=opencli: internal debug logging
- DEBUG_SNAPSHOT: DOM snapshot debug output
* fix: clean up stale .yaml adapter files from older versions (#953)
Users upgrading from v1.6.x retain .yaml adapter files in
~/.opencli/clis/ that trigger "Ignoring YAML adapter" warnings on
every run. The hash-based sync only tracks .js files, so these
legacy .yaml files are never cleaned up.
Add a cleanup step (3b) that removes .yaml/.yml files from user
adapter directories when the corresponding site exists in the
official package (i.e., the site has been migrated to .js).
* fix(fetch-adapters): narrow stale yaml cleanup
* feat: decouple extension version from CLI version
Extension and CLI had tightly coupled version numbers (both 1.7.2),
requiring manual sync across 3 files on every release. This decouples
them so each can release independently.
Changes:
- Extension version reset to 1.0.0 with independent versioning
- Extension sends compatRange (e.g. ">=1.7.0") in hello message
so doctor can check CLI/extension compatibility
- Daemon stores and exposes extensionCompatRange via /status
- Doctor uses compatRange for compatibility checks (falls back to
major-version check for older extensions without compatRange)
- Doctor shows extension update availability from cached GitHub
Releases data
- release.yml always builds and attaches extension zip to every
CLI release, so users always find both in the same release page
- build-extension.yml triggers on ext-v* tags (not v*) to avoid
duplicate builds
* fix: version extension release assets
* fix: code audit round 2 — pruneEmptyDirs, evaluateWithArgs, hot-reload, error cause chain
1. pruneEmptyDirs: use path.relative() instead of startsWith() to prevent
false boundary matches on overlapping directory names
2. evaluateWithArgs: add safe evaluate method that auto-serializes args via
JSON.stringify, preventing injection by design
3. Hot-reload: detect mtime changes on user adapter files in daemon mode,
invalidate module cache so edits take effect without restart
4. toEnvelope: preserve error cause chain in verbose mode for better
production debugging
* fix: address review feedback on code audit round 2
- pruneEmptyDirs: resolve() paths before relative() check
- evaluateWithArgs: validate keys are valid JS identifiers
- hot-reload: only bust ESM cache on reload, not first load
- toEnvelope: move cause serialization into toEnvelope itself
so all consumers (AI agents, MCP tools) get cause chain
* fix: address code audit findings (C1-C4, I1, I4, I6)
Security:
- C1: Fix page.evaluate injection in browser type/select commands and
6 adapter files by using JSON.stringify for user input interpolation
- C2: Close WebSocket on CDP connect timeout to prevent resource leak
- C3: Reject CDP connect promise on Page.enable failure instead of
silently swallowing the error
Reliability:
- C4: Guard against corrupted adapter-manifest.json hashes to prevent
false-positive override deletion
- I1: Throw on pre-navigation failure instead of warn-and-continue
- I4: Use Map<string, Promise<void>> for lazy module loading to prevent
concurrent double-imports of the same adapter
Performance:
- I6: Replace O(n) registry alias cleanup with O(k) direct deletion
* fix: address self-review findings on PR #981
- C1: add quotes around CSS selector attribute values in browser
type/select to match other commands (get text/value/attributes)
- C2: clear this._ws in timeout handler to prevent race with open event
- C4: refine corruption guard — treat null/undefined hashes as empty,
only skip sync for truly invalid types (string, number, array)
* feat(clis/chatgptweb): add ChatGPT web image generation command
Add `opencli chatgptweb image` command that generates images using
ChatGPT web (GPT-4o image generation) and saves them locally.
Features:
- Navigates to chatgpt.com/new with full page reload to ensure clean state
- Uses Playwright's page.type() for reliable text input in TipTap editor
- Closes sidebar if open (covers the chat composer on some layouts)
- Polls for response completion (handles thinking/throttling states)
- Extracts generated images from DOM (backend-api/estuary/content URLs)
- Downloads and saves as PNG/JPEG files to user-specified directory
- Supports --op for output directory and --sd to skip download
Files:
- clis/chatgptweb/image.js: CLI command definition
- clis/chatgptweb/utils.js: DOM helpers, send/wait/export functions
Works cross-platform (Linux/macOS/Windows) via OpenCLI browser automation.
* fix(chatgptweb): stabilize image generation flow
* docs(chatgptweb): add browser adapter guide
---------
Co-authored-by: Tony Simons <tony@tonysimons.dev>
Co-authored-by: jackwener <jakevingoo@gmail.com>
Support browsing second-hand houses, neighborhoods, rentals, and
transaction records on ke.com with city/district/price filtering.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(maimai): add talent search with multi-dimensional filters
Add maimai.cn talent search adapter with support for:
- Keyword search (query)
- Company filtering (multiple companies supported)
- School filtering (with 985/211 options)
- Location filtering (province/city)
- Work experience and education level filters
- Industry and position filters
- Direct chat availability
- Sort by relevance, activity, work years, or education
Features:
- Reuses Chrome login session for authentication
- Extracts candidate info: name, job title, company, work history
- Shows work years, education, age, active status
- Displays skill tags and mutual friends count
* fix docs and strategy for maimai adapter
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(discord-app): add delete command to remove a message by ID
Adds a new `delete` command for the discord-app CLI that deletes a
message in the active channel by its snowflake ID. Uses the UI strategy
to hover the message, open the "More" menu, click "Delete Message", and
confirm the deletion dialog.
* docs: add binance adapter doc and update discord doc with delete command
* feat(twitter): add lists command to retrieve user lists
Add twitter/lists command that fetches Twitter/X lists for a user.
Supports:
- Lists with member and follower counts
- Private/public mode detection
- Default to current user if no user specified
- Works for any Twitter user
* docs: add lists command to twitter commands in README
Add twitter lists command to Built-in Commands table in both
English and Chinese README files
* fix(twitter): parse lists from card DOM instead of locale-specific page text
---------
Co-authored-by: isanwenyu <isanwenyu@users.noreply.github.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(zsxq): accept topic_id as string in getTopicFromResponse
The ZSXQ API returns topic_id as a string, but getTopicFromResponse()
only checked for typeof === 'number', causing it to fall through and
return null. This made 'opencli zsxq topic <id>' fail with NOT_FOUND
for all valid topic IDs.
* fix(zsxq): use group-scoped topic endpoint instead of bare /v2/topics/{id}
The ZSXQ API requires topics to be fetched within their group context.
Change /v2/topics/{id} -> /v2/groups/{groupId}/topics/{id} for both
the detail and comments endpoints. Also adds optional --group_id arg.
* fix: include adapter tests in default npm test
`npm test` only ran unit + extension projects, so adapter tests
(clis/**/*.test.js) were never exercised by the default test command.
Add --project adapter so they run alongside unit and extension tests.
* test: include adapter project in default npm test
* refactor: smart sync adapters instead of full copy (#sparse-override)
Replace unconditional full-copy of all adapters to ~/.opencli/clis/ with
hash-based smart sync that only copies files whose content has changed.
Changes:
- fetch-adapters.js: use SHA-256 content hashes to skip unchanged files;
store per-file hashes in adapter-manifest.json
- discovery.ts: simplify ensureUserAdapters() to only create the directory
(no longer triggers full copy on first run)
- main.ts: fix fast completion to check manifest file existence instead of
directory existence (sparse override may have empty user dir)
- cli.ts: add `opencli adapter eject/reset/status` commands for managing
local adapter overrides
- engine.test.ts: add tests for empty user dir and ensureUserAdapters
* fix: address review blockers — site-level sync + reset --all
1. Fix `adapter reset --all`: change <site> from required to optional
argument so --all can be used without specifying a site name.
2. Change smart sync from file-level to site-level granularity:
if any file in a site has changed upstream, overwrite the entire
site directory. This matches the agreed product semantics — local
modifications to any file in a site are replaced when upstream
updates that site.
* fix: delete old site dir before writing updated adapter files
When a site has upstream changes, delete the entire site directory
first, then write the new version. This prevents stale files from
older versions lingering in the user directory.
* fix: reset --all preserves custom sites, only removes official overrides
Blocker 3 fix: reset --all now checks BUILTIN_CLIS to identify official
sites and only deletes those, preserving user-created custom sites.
* refactor: sparse sync deletes local overrides instead of copying new versions
Changed fetch-adapters.js semantics per team agreement:
- When an official site has upstream changes, DELETE the local override
instead of copying the new version into ~/.opencli/clis/
- Runtime automatically falls back to package baseline
- ~/.opencli/clis/ becomes a true sparse override layer
* fix: reset <site> rejects custom sites, only allows official overrides
Single-site reset now checks BUILTIN_CLIS before deleting, matching
the same protection that reset --all already has.
* fix: reset <site> allows custom sites per product decision
Per @WAWQAQ: explicit single-site reset should work on custom sites too.
Differentiate messaging: official sites say "using official baseline",
custom sites say "removed custom site".
reset --all still only removes official overrides (bulk safety).
* fix: reset --all deletes all local sites including custom per product decision
Per @WAWQAQ: --all should clear the entire local working cache,
including custom sites. Single-site reset already handles both types.
Binance was the only adapter left in src/clis/ after the TS→JS
migration (PR #928). Move all 11 adapters and the test file to
clis/binance/, strip TypeScript syntax from the test, and switch
the test import to the @jackwener/opencli/pipeline package export.
log.debug() requires DEBUG=opencli to output, which means
DEBUG_SNAPSHOT=1 alone no longer shows snapshot fallback diagnostics.
Use process.stderr.write directly since the DEBUG_SNAPSHOT guard
already controls when this diagnostic fires.
Users who created custom .ts adapters in ~/.opencli/clis/ will see
their commands silently disappear after upgrading to the JS-only
version. Add an explicit warning so they know to convert to .js.
The alias resolution logic checked `!registry.has(target)` before
calling `registry.get(target)`, which always returned undefined.
Moreover, aliases registered as `site/alias` keys meant `registry.has`
returned true, skipping the block entirely. The canonical name was
never resolved, so `validate site/alias` silently checked 0 commands.
Simplify to always resolve via `registry.get(target)` which handles
both canonical keys and alias keys correctly.
Older versions (pre-1.7.1) shipped adapters as .ts files. When users
upgrade to a .js-only version, the old .ts files are left orphaned in
~/.opencli/clis/. Add a cleanup step that removes .ts files when a
corresponding .js official adapter exists.
* perf: P0 performance optimizations — VM context reuse, startup parallelization, stealth caching
1. Reuse VM sandbox context in pipeline template engine instead of creating
a new vm.createContext() on every expression evaluation. This eliminates
~0.3ms per call in map/filter loops over large arrays.
2. Cache sanitizeContext() results via WeakMap keyed by object reference.
In pipeline loops, `args` and `data` are the same object across all
iterations — the expensive JSON round-trip now runs only once per step.
3. Parallelize independent startup I/O: built-in CLI discovery now runs
concurrently with ensureUserCliCompatShims and ensureUserAdapters,
saving ~30-50ms on cold start.
4. Cache the stealth JS string (350 lines, pure static) after first
generation — every subsequent goto() reuses the cached string.
* fix: address review feedback on P0 perf optimizations
1. sanitizeContext: cache JSON string instead of parsed object to prevent
sandbox mutation from polluting subsequent calls
2. VM sandbox: clean non-whitelisted properties before each execution to
prevent cross-expression state leakage
3. Startup parallelization: document registry overwrite semantics and
confirm no shared-state race between parallel tasks
* refactor(validate): switch from YAML scanning to registry-based validation
The validate/verify commands only scanned YAML files, which are no
longer supported. Rewrite to validate commands from the in-memory
registry populated by discoverClis(), aligning with the JS-first
adapter architecture.
New checks: missing description, browser commands without domain,
pipeline step name typos, commands without func/pipeline, duplicate
arg names, and positional arg ordering.
* fix(validate): treat lazy-loaded commands as valid
Manifest-registered commands have _lazy=true and no func/pipeline
until execution time. Recognize this as a valid execution form.
* fix(validate): warn on empty registry, support alias targets
- Emit warning when registry is empty instead of silent PASS
- Resolve alias targets to canonical key before filtering
* fix: project hygiene — docs, lint, daemon restart, code fence
- Update Node version requirement from >= 20 to >= 21 in 7 doc files
(README, README.zh-CN, installation guides, troubleshooting)
- Update adapter count from 79+ to 87+ in READMEs
- Remove duplicate `lint` script (identical to `typecheck`)
- Fix TESTING.md CI matrix: Node ['22'] instead of ['20', '22']
- Fix autofix SKILL.md code fence escaping (\``` → ~~~)
- Add daemon restart to postinstall so updated adapters are picked up
- Fix preuninstall to respect OPENCLI_DAEMON_PORT env var
* fix: align docs and skills with JS-first adapter contract
Adapters are now .js files (not .ts). Update all references across:
- README.md, README.zh-CN.md, CONTRIBUTING.md
- docs/guide/getting-started.md, docs/index.md
- skills/opencli-browser/SKILL.md, skills/opencli-explorer/SKILL.md
The runtime (discovery.ts) only loads .js from user clis/ directories,
and `opencli browser init` generates .js scaffolds. Documentation was
still teaching users to create .ts files.
* fix: update CI matrix to Node 22 only (drop Node 20)
package.json requires Node >= 21 (styleText dependency). The CI matrix
was still testing Node 20 which doesn't meet this requirement.
* fix: revert incorrect daemon restart from postinstall
The daemon (browser bridge) only handles CDP communication — it has no
knowledge of adapters. Adapter discovery, loading, and execution all
happen in the CLI process, which is fresh each invocation. The
_loadedModules cache in execution.ts is process-local and not a real
staleness concern. Remove the unnecessary restartDaemon() call.
Strategy is a 5-value enum (PUBLIC/COOKIE/HEADER/INTERCEPT/UI) that
the execution path was reading at two points — resolvePreNav() and
shouldUseBrowserSession() — to make decisions that are already fully
expressible by the existing `browser` and `navigateBefore` fields.
This commit introduces normalizeCommand() inside registerCommand(),
which expands strategy into concrete runtime fields at registration
time. After normalization, execution code never reads cmd.strategy.
normalizeCommand expansion rules:
- strategy → browser: PUBLIC defaults to false, others to true.
Explicit browser value always wins.
- strategy + domain → navigateBefore:
· COOKIE/HEADER + domain → 'https://{domain}' (pre-navigate)
· Non-PUBLIC without domain → true (needs auth context, no URL)
· PUBLIC → undefined (no auth needed)
Explicit navigateBefore (false or string) always wins.
This matters because commands enter the registry from 4 sources
(cli(), manifest, generate-verified, tests), and previously only
cli() did strategy derivation. The other 3 constructed CliCommand
directly, leaving strategy as a runtime dependency. Now all sources
converge through registerCommand → normalizeCommand.
Changes:
- registry.ts: add normalizeCommand(); simplify cli() to delegate
all derivation to normalizeCommand via registerCommand()
- execution.ts: resolvePreNav() no longer reads strategy; just
reads the already-expanded navigateBefore field. Strategy import
removed.
- capabilityRouting.ts: shouldUseBrowserSession() checks
cmd.navigateBefore (truthy = needs browser session) instead of
cmd.strategy !== PUBLIC. Strategy import removed.
- discovery.ts: manifest path no longer hardcodes browser default;
delegates to normalizeCommand.
- capabilityRouting.test.ts: test now reflects normalized command
shape (navigateBefore: true for COOKIE without domain).
strategy is preserved as metadata on CliCommand — opencli list,
cascade probe, adapter generation, and documentation continue to
read it. Only the execution path stops consuming it.
Add Step 6 to the autofix skill: after a verified local fix, prepare a
GitHub issue draft and file it (with user confirmation) via `gh issue
create`. Pure skill/documentation approach — no new runtime code.
Closes the need addressed by #936 with zero code, zero tests to maintain.
* fix: sync package-lock.json with package.json dependencies
package-lock.json was missing @emnapi/core@1.9.2 and
@emnapi/runtime@1.9.2 (transitive deps of @emnapi/wasi-threads),
causing `npm ci` to fail on all CI jobs.
* fix: resolve remaining CI failures after TS-to-JS adapter migration
- vitest.config.ts: update adapter project include/exclude from .test.ts
to .test.{ts,js} to match converted adapter test files
- check-doc-coverage.sh: skip adapter directories containing only utility
files (prefixed with _), fixing false positive for clis/slock/
- linux-do/topic-content.test.js: fix hardcoded reference to topic.ts
(now topic.js after PR #928 migration)
* fix: clean up legacy shim files and stale tmp files on upgrade
Add cleanup steps to fetch-adapters.js that run on every version upgrade:
1. Remove legacy compat shim files from ~/.opencli/ (registry.js,
errors.js, utils.js, etc.) that were created by an older approach
using file:// re-exports. Current approach uses node_modules symlink.
Only deletes files containing "export * from 'file://" to avoid
removing user-created files.
2. Remove legacy compat shim directories (browser/, download/, errors/,
etc.) using the same safety check.
3. Clean up stale .plugins.lock.json.tmp-* files left behind by
crashed processes. These accumulate over time (108 found on one
machine) and clutter ~/.opencli/.
* fix: check every file in legacy shim directories before deleting
Instead of checking only the first file and deleting the entire
directory, now checks each file individually and only deletes files
matching the shim pattern. Directory is removed only if empty after
individual file cleanup.
- Remove mapDistToSource() from diagnostic.ts — mapped dist/clis/
paths back to clis/ but dist/clis/ no longer exists after JS-first
migration. The function always returned null.
- Simplify resolveAdapterSourcePath() to check candidates directly
without the dead dist→source mapping detour.
- Delete scripts/clean-yaml.cjs — walked dist/clis/ to delete YAML
files, but dist/clis/ no longer exists.
- Remove clean-yaml script entry from package.json.
1. candidateToJs: escape single quotes in site, name, domain, and arg
name/type fields to prevent syntax errors in generated JS adapters.
Previously only description and help fields were escaped.
2. diagnostic: pass network request body through redactText() to
prevent sensitive data (JWT, bearer tokens) from leaking into
repair context. responseBody/responsePreview already used
sanitizeCapturedValue which calls redactText, but the body field
only had truncation.
* refactor(adapters): convert adapter layer from TypeScript to JavaScript
Core framework stays TypeScript; adapter layer moves to JS-first.
Adapters are essentially "executable config + browser scripts" that
barely use TS features — this simplifies the build/distribution pipeline
by removing the dist/clis/ intermediate compilation step.
Changes:
- Convert all 753 adapter files in clis/ from .ts to .js
- Update tsconfig to exclude clis/ from compilation
- Simplify build-manifest to scan clis/*.js directly (no dist/clis/)
- Update discovery, main, fetch-adapters to load JS adapters from clis/
- Update generate-verified to output .js artifacts
- Update package.json files field: dist/clis/ → clis/
- Fix all test files for the .ts → .js transition
* fix(main): use findPackageRoot for BUILTIN_CLIS path
The previous relative path (../../clis from __dirname) only worked for
dist/src/main.js but broke dev mode (tsx src/main.ts) where __dirname
is <repo>/src — resolving to /clis instead of <repo>/clis.
Use findPackageRoot() which works for both dev and prod paths.
* fix(build-manifest): import compiled JS from dist/clis/ instead of raw TS
Node's type stripping does not rewrite '.js' → '.ts' in import
specifiers, so dynamically importing .ts source files fails whenever
they contain relative imports like './utils.js'.
Switch to scanning dist/clis/ for compiled .js files after tsc runs.
This eliminates all 268 "Cannot find module" warnings and increases
manifest entries from 254 to 532 (previously half were silently skipped).
* fix: write manifest to dist/cli-manifest.json where runtime expects it
The runtime resolves BUILTIN_CLIS to dist/clis/ (relative to
dist/src/main.js), so discoverClis() looks for manifest at
dist/cli-manifest.json. Previously it was written to the package root
where the runtime never found it — manifest was effectively unused,
always falling through to filesystem scanning.
* refactor(errors): unify error output as YAML envelope to stderr
Replace the 100+ line chalk renderError() switch-case with a single
YAML envelope output path. All errors now output a structured
{ok, error: {code, message, help, exitCode}} envelope to stderr,
regardless of TTY status.
This simplifies the error system from 5 mechanisms to 3:
1. Error Envelope (YAML → stderr) — unified error output
2. Exit codes (sysexits.h) — process exit semantics
3. Diagnostic (OPENCLI_DIAGNOSTIC=1) — autofix repair context
Removed: chalk error rendering, ERROR_ICONS map, classifyGenericError
regex classifier, BrowserConnectError-specific bridge status display.
Added: toEnvelope() utility, ErrorEnvelope type.
* refactor(errors): migrate adapters to throw CliError, update docs
- Migrate xueqiu adapters from return [{error,help}] to throw CliError
- xueqiu/utils.ts: fetchXueqiuJson now throws AuthRequiredError/
CommandExecutionError instead of returning {error, help} objects
- Remove resolveColumns error fallback from output.ts (no longer needed)
- Add verbose stack trace support to error envelope
- Add ADAPTER_LOAD to AutoFix hint trigger codes
- Update skill docs (adapter-templates, explorer, oneshot, advanced-patterns)
to recommend throw CliError pattern instead of return [{error, help}]
* fix: remove remaining dead error-forwarding in 4 xueqiu adapters + review fixes
- Remove `if ('error' in d) return [d]` from feed, hot, search, kline
(fetchXueqiuJson now throws, so these were dead code)
- Add `stack?: string` to ErrorEnvelope interface (removes type cast hack)
- Fix adapter-templates.md: use AuthRequiredError instead of plain Error
* fix: migrate barchart/quote and yahoo-finance/quote to throw CliError
Last two adapters that silently returned [] on error instead of
throwing CommandExecutionError.
* fix: self-review fixes — doc evaluate crash, error messages, kline consistency
- adapter-templates.md: getServerContext was throwing AuthRequiredError
inside a function serialized into page.evaluate() (browser has no
CliError). Reverted to return {error} sentinel + func() body throw.
- yahoo-finance/quote, barchart/quote: include symbol in fallback error msg
- xueqiu/kline: throw EmptyResultError instead of returning [] for
consistency with other xueqiu adapters
* docs(skills): add Tier 2.5 localStorage Bearer, SPA discovery, and test standards
From real-world experience building slock.ai CLI adapters:
- oneshot: add network-empty diagnosis, SPA baseURL bundle search, Tier 2.5
localStorage Bearer template (with multi-tenant X-Server-Id pattern),
updated auth quick-reference, file path note, opencli browser verify test flow
- explorer: add Tier 2.5 to decision tree and strategy table, update test section
with opencli browser verify + Done standard, fix Step 5 path to ~/.opencli/clis/,
add 4 new pitfall rows (SPA HTML, 400 context header, empty network, wrong dir)
* docs(skills): fix path conflict + add anti-change patterns from real adapters
Fix reviewer blocking issue:
- Remove the contradictory "~/.opencli/clis/" note that mixed user-local and
repo-contributor workflows; replace with explicit two-scenario callout in
Step 4, Step 5, pitfall table, and oneshot test section
- Template comments in oneshot restored to clis/<site>/<name>.ts (repo path)
Add "抗变更模式" section to explorer, based on opencli's own production code:
- Pattern 1: dynamic queryId discovery (twitter/shared.ts resolveTwitterQueryId)
— scan loaded JS bundle by operationName (stable) to find queryId (unstable)
- Pattern 2: semantic DOM priority fallback (web/read.ts)
— article > [role=main] > main > class-hint > body, pick largest text block
- Pattern 3: ordered selector array + timestamp comments (xiaohongshu/publish.ts)
— first-match wins, comment records UI version and observed attribute values
- Pattern 4: nullish-coalescing field multi-path (xiaohongshu/user-helpers.ts)
— covers camelCase/snake_case variants without assuming fixed key name
* docs(explorer): split SKILL.md into reference sub-documents
- Shrink main SKILL.md from 994 to 270 lines — core workflow only
- Extract all TS templates (Tier 1~4, pagination) to references/adapter-templates.md
- Add error handling standard: { error, remedy } pattern (remedy > hint)
- Add Tier 2.5 localStorage Bearer template with multi-tenant X-Server-Id example
- Extract cascading requests, tap debug, verbose mode, anti-change patterns to references/advanced-patterns.md
- Extract record workflow to references/record-workflow.md
* docs(skills): fix verify command — split by dev scenario
browser verify only reads ~/.opencli/clis/, not repo's clis/.
Split all verify instructions:
- Repo 贡献: npm run build + opencli <site> <cmd>
- 私人 adapter: opencli browser verify <site>/<name>
Fixes blocker in explorer:L209, L224 and oneshot:L286, L298
* docs(adapter-templates): add utils.ts extraction pattern for same-site adapters
* docs(skills): add decision matrix, stop conditions, sync comments
explorer: add path decision matrix before core workflow
oneshot: add explicit stop/switch conditions (when to escalate to explorer)
both: add keep-in-sync comment on the two-scenario verify block
* feat(slock): extract utils.ts + apply { error, help } pattern; docs: remedy→help
slock/utils.ts: new — getSlockContext(), resolveChannelId()
- Shared token + workspace resolution, no more 4-line duplication
- UUID regex (/^[0-9a-f]{8}-...$/) replaces fragile !includes('-')
- Returns { error, help } instead of throwing
tasks.ts / members.ts / send.ts:
- Import from utils.ts, remove all duplicated auth boilerplate
- All errors return [{ error, help }], no more throw
- members.ts: add limit arg (was unbounded before)
docs: rename remedy → help across all skill references
* refactor(adapters): migrate pipeline adapters to func() with { error, help } pattern
- slock: agents, channels, messages, servers now use getSlockContext/resolveChannelId
from utils.ts; error handling uses { error, help } return instead of bare throws
- linux-do: export fetchLinuxDoJson from feed.ts; migrate search, topic, categories,
tags, user-posts, user-topics from pipeline+throw to func() using fetchLinuxDoJson
- xueqiu: add utils.ts with fetchXueqiuJson helper; migrate hot, feed, search, stock,
watchlist, hot-stock, groups, kline, earnings-date from pipeline+throw to func()
* fix(output): show error rows in table/csv/markdown when columns declared
When a command declares columns (e.g. ['rank', 'title', 'value']) but
returns an error row ({ error, help }), the declared columns would
render empty cells. Now resolveColumns detects the error key and falls
back to the row's actual keys, making diagnostics visible in all output
formats.
* chore: remove slock adapters from this PR
Slock adapters should be in a separate PR, not bundled with the
adapter refactor and skill docs improvements.
* docs(skills): unify browser tool names to `opencli browser` commands
Replace abstract MCP tool names (browser_navigate, browser_snapshot,
browser_network_requests, browser_click, browser_evaluate) with
concrete `opencli browser` CLI commands in explorer and oneshot skills.
This aligns all three browser-related skills into a clear hierarchy:
- opencli-browser: atomic command reference
- opencli-oneshot: 4-step quick generation workflow
- opencli-explorer: full site exploration workflow
* docs(skills): address review — demote explore, fix eval placeholder
1. Demote `opencli explore` from "recommended" to "supplementary helper"
and make `opencli browser` the explicit primary path for API discovery.
2. Fix `url` undefined variable in eval example — use `<API URL>` placeholder.
* feat: auto-close adapter windows, add OPENCLI_WINDOW_FOCUSED, document config
1. Adapter commands now close the automation window immediately after
completion instead of waiting for the 30s idle timeout.
2. OPENCLI_WINDOW_FOCUSED=1 opens automation windows in the foreground
(useful for debugging). Default remains background.
3. Add Configuration section to README (EN/ZH) and opencli-usage skill
listing all stable user-facing environment variables.
* Fix OPENCLI_WINDOW_FOCUSED to be per-request, not frozen at daemon startup
Move env var read from daemon (startup-time constant) to CLI side
(sendCommandRaw), so it works correctly with the persistent daemon model.
Each request now reads the env var fresh and includes windowFocused in
the command payload.
* fix(xiaoe): resolve missing episodes for long courses by handling lazy load
* fix(xiaoe): keep lazy-load scroll until inner list stabilizes
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* refactor: make daemon persistent, remove idle timeout
- Remove IdleManager and 4-hour idle auto-exit
- Daemon now stays alive until explicit shutdown or uninstall
- Add preuninstall hook for best-effort daemon cleanup on npm uninstall
- Update docs to reflect persistent daemon model
* fix: remove stale idle timeout references from code and docs
* refactor: remove daemon status/restart commands and lastCliRequestTime
- Remove `daemon status` and `daemon restart` CLI commands (doctor covers diagnostics)
- Remove `lastCliRequestTime` tracking (no longer needed without idle timeout)
- Keep only `daemon stop` as the explicit shutdown command
* Add AbortSignal.timeout(3s) to preuninstall shutdown fetch
Prevents npm uninstall from hanging if the daemon port accepts
connections but never responds.
* refactor: unify browser error classification and deduplicate retry logic
Replace two overlapping error classification systems with a single
classifyBrowserError() that returns retry advice (retryable + delayMs):
- Extension/daemon transient errors → retryable, 1500ms delay
- CDP target navigation errors → retryable, 200ms delay
- Non-transient errors → not retryable
Deduplicate sendCommand/sendCommandFull retry loop into sendCommandRaw,
making both public functions thin return-value wrappers.
* fix: add error kind to prevent page-level retry of extension errors
classifyBrowserError() now returns a `kind` field:
- extension-transient: retried by daemon-client only
- target-navigation: retried by page-level settle logic
- non-retryable: no retry
Page.goto() and Page.evaluate() now only settle-retry on
target-navigation, preventing extension/daemon errors from being
silently swallowed as settle noise.
Use Chrome CDP targetId (UUID) as the canonical page identity across
all layers (extension → daemon → CLI), demoting tabId to an
extension-internal routing detail.
- Add extension/src/identity.ts: bidirectional targetId ↔ tabId mapping
with lazy refresh via chrome.debugger.getTargets()
- Update protocol: Command.page and Result.page carry targetId
- Update background.ts: resolveCommandTabId() and pageScopedResult()
helpers; all page-scoped handlers return targetId
- Add sendCommandFull() to daemon-client for responses with page identity
- Update Page class: _page stores targetId, goto/selectTab extract it
- Update record.ts: injectedPages tracks by targetId
- Add extension tests to vitest config and CI test scripts
Synced all desktop adapter command lists in desktop.md with
the actual `opencli <adapter> --help` output:
- cursor: remove non-existent status/new/dump/screenshot; add composer
- codex: remove non-existent status/new/dump/screenshot
- chatgpt: add missing model command
- chatwise: remove non-existent new/screenshot
- notion: update descriptions to match help text
- discord-app: update descriptions to match help text
- doubao-app: reorder to match help output
- antigravity: remove non-existent ask; add serve/status
Also moved `status` to the top of each adapter section where it exists.
* perf: fast-path completion, version, and shell scripts to bypass full discovery
Lightweight commands (--get-completions, --version, completion <shell>) now
resolve before any heavy module loading. Key changes:
- New completion-fast.ts: manifest-based completion + shell script generators
with zero dependency on registry/discovery/cli modules
- main.ts: static imports replaced with dynamic import() for the full startup
path so the fast path never pays the cost of loading discovery, registry,
Commander, hooks, etc.
- USER_CLIS_DIR inlined to avoid importing the entire discovery module
- completion.ts: removed manifest functions (moved to completion-fast.ts),
now only used as fallback when manifest is unavailable
* fix: address review blockers from codex-mini0
1. --version fast path: only match when argv[0] is --version/-V,
not anywhere in argv. Prevents intercepting `opencli gh --version`
which should pass through to the subcommand.
2. Completion fast path: require ALL manifests to exist (hasAllManifests),
not just one. If user clis dir exists but has no manifest, fall back
to full discovery so user adapters aren't silently dropped.
If user clis dir doesn't exist at all, skip its manifest requirement
since there are no user adapters to miss.
EMPTY_RESULT and structurally-valid SELECTOR failures are often not
adapter bugs — they're the platform shaping results under anti-scrape,
or a soft 404, or a legitimately empty search. Patching a working
adapter to chase a zero-result query breaks the next working path.
Add a pre-check section at the top of opencli-autofix listing four
rule-outs that must fail before a repair round is justified:
1. Retry with an alternative query / entry point
2. Spot-check the page in a normal Chrome tab
3. Look for soft 404s (200 with empty payload)
4. Remember that "0 results" from a search is a valid answer
Placed directly before "Step 1: Collect Diagnostic Context" so the
check runs at exactly the moment the agent would otherwise commit to
a repair round.
Audience/timing is the whole point: the skill is loaded precisely when
an error has occurred and the agent is deciding whether to repair, and
the pre-check intercepts that decision before it locks in.
11 lines of markdown, zero code, single file.
---
Inspired by https://github.com/eze-is/web-access by 一泽 Eze (MIT),
specifically the "平台返回的'内容不存在'不一定反映真实状态" mental model
from its SKILL.md. Adapted into concrete, actionable checks for
opencli's EMPTY_RESULT classification.
Note: an earlier version of this PR also added a tool-selection
decision table and a subagent-verb rule to opencli-usage. Both were
removed after review because opencli-usage only loads *after* an agent
has committed to using opencli — advice placed there arrives too late
to change tool selection, and is not seen by the main agent at
delegation time. The insights are still valid; they just don't have a
load-time match in the current skill system. This PR keeps only the
change where audience and timing actually line up.
* refactor(skills): unify command reference by site instead of technology
- Merge Browser-based and Public API sections into single alphabetical
table with type emoji tags (🌐/✅/🖥️/🔧)
- Delete browser.md and public-api.md (replaced by unified SKILL.md table)
- Add GitHub/DevOps and collaboration rows to capability lookup
- Remove stale File column from capability table
* feat(skills): add commands.md with merged examples + 8 missing adapters
- Create commands.md: merge browser.md + public-api.md into single
alphabetical-by-site reference with detailed usage examples
- Add 8 missing adapters: 1688, hupu, jianyu, lesswrong, quark,
xianyu, xiaoe, yuanbao
- Bump skill version 1.6.3 → 1.6.9 to match package.json
- Add dedicated External CLI section listing all 7 registered CLIs
(gh, obsidian, docker, lark-cli, dws, wecom-cli, vercel)
- Include install/register commands so AI agents know how to manage them
- Move gh from Desktop to External CLI section
- Update desktop.md to remove gh and reference External CLI section
* refactor(skills): merge opencli-generate into opencli-explorer
opencli-generate was a thin wrapper over generateVerifiedFromUrl,
essentially an internal pipeline orchestration. Merge its entry point
into opencli-explorer as the automated fast path, keeping one unified
skill for adapter creation.
- Delete skills/opencli-generate/SKILL.md
- Add automated generation tip to opencli-explorer SKILL.md
- Update README/README.zh-CN skill references
- Update skill-generate.ts comment
* fix(docs): fix dead link in yaml-adapter deprecation page
Change ../../CONTRIBUTING.md to ./contributing (VitePress internal link).
* refactor: remove version field from GenerateOutcome and EarlyHint
All consumers are in the same repo and evolve together — version field
adds ceremony without practical value at this stage.
Keeps schema_version in VerifiedArtifactMetadata (sidecar file format).
* refactor: migrate all 123 CLI adapters from YAML to TypeScript
Remove YAML as an adapter format entirely. All adapters now use
TypeScript with cli() from @jackwener/opencli/registry.
- Convert 123 YAML adapter files to TypeScript via batch script
- Remove YAML scanning from discovery.ts (registerYamlCli, yaml import)
- Remove scanYaml() and shouldReplaceManifestEntry() from build-manifest.ts
- Change synthesize.ts to output JSON candidates (internal format)
- Change generate-verified.ts to write .ts adapter files instead of .yaml
- Delete yaml-schema.ts (dead code) and scripts/yaml-to-ts.mjs (one-time tool)
- Update all tests to match new format
Closes discussion in #OpenCLI thread 47ddba82.
* fix: close YAML migration gaps in plugin scaffold, validation, and scan
- plugin-scaffold.ts: generate hello.ts (TS pipeline) instead of hello.yaml
- plugin.ts validatePluginStructure: no longer accept .yaml as valid command file
- plugin.ts scanPluginCommands: remove .yaml/.yml from scanned extensions
- discovery.ts: add explicit log.warn() when YAML files detected in clis/ or plugins/
- plugin.test.ts: update all test fixtures from .yaml to .js
- plugin-scaffold.test.ts: update hello.yaml references to hello.ts
- Delete dead src/yaml-schema.ts
Resolves PR #887 review blockers from @mbp-codex-pr0.
* refactor: complete YAML removal across docs, skills, record, and binance adapters
Code changes:
- record.ts: candidate output changed from .yaml (yaml.dump) to .json (JSON.stringify), removed js-yaml import
- src/clis/binance: convert all 11 YAML adapters to TypeScript cli() format
- binance/commands.test.ts: rewrite to use registry instead of yaml.load
- skill-generate.test.ts, diagnostic.test.ts: update mock paths from .yaml to .ts
- build-manifest.ts, synthesize.ts: update stale YAML comments
Documentation:
- README.md: remove .yaml from Dynamic Loader, fix plugin types, fix synthesize comment
- README.zh-CN.md: fix synthesize comment
- CONTRIBUTING.md: replace YAML Adapter section with Pipeline Adapter (TS), update arg examples
- docs/developer/yaml-adapter.md: replaced with deprecation redirect
- docs/developer/architecture.md: remove YAML pipeline references
- docs/developer/contributing.md: remove YAML adapter section
- docs/developer/ai-workflow.md: YAML → TS in synthesize description
- docs/guide/getting-started.md: remove .yaml from loader, update engine description
- docs/guide/plugins.md: remove YAML plugin option, update plugin types
- docs/index.md, docs/comparison.md: remove YAML adapter references
- docs/zh/guide/plugins.md: remove .yaml from scan description
Skills:
- opencli-explorer/SKILL.md: rewrite YAML vs TS decision tree to TS-only
- opencli-oneshot/SKILL.md: replace YAML templates with TS cli() templates
- opencli-generate/SKILL.md: YAML artifact path → TS artifact path
- opencli-usage/SKILL.md, plugins.md: update adapter format references
* fix: clean up remaining YAML adapter references in docs
- docs/zh/guide/plugins.md: replace YAML plugin example with TS pipeline
- docs/developer/testing.md: YAML Adapter heading → Adapter, remove validate line
- TESTING.md: same fix in root testing doc
- CONTRIBUTING.md: remove "YAML validation" comment
- docs/.vitepress/config.mts: mark YAML Adapter Guide as (Deprecated) in nav
- docs/advanced/download.md: remove "YAML Adapters" from pipeline step heading
All consumers are in the same repo and evolve together — version field
adds ceremony without practical value at this stage.
Keeps schema_version in VerifiedArtifactMetadata (sidecar file format).
* fix: use Strategy.PUBLIC enum in skill-generate test to fix typecheck regression
* feat: add P2 EarlyHint callback channel to generateVerifiedFromUrl
Add optional onEarlyHint callback for internal cost gating before verify stage.
- EarlyHint type: version, stage, continue, reason, confidence, candidate?
- 3 emit points: explore (viable/not), synthesize (candidate/not), cascade (auth/ok)
- candidate only on synthesize/cascade + continue:true (not on stop or explore)
- unsupported-required-args goes directly to P1 terminal, no P2 hint emitted
- 6 new tests covering all hint paths + guardrails
* docs: add opencli-generate skill spec (SKILL.md)
Captures A+B consensus from team discussion:
- Input: url + goal? (natural language intent hint)
- Output: SkillOutput with machine-readable fields + human message
- Decision tree: thin mapping from GenerateOutcome
- Guardrails: no re-orchestration, no auto-escalation, no new taxonomy
- P1/P2 boundary: P1 is single source of truth, P2 transparent to skill
* fix: address review nits on skill spec
- Make path explicitly optional in needs-human-check decision tree
- Add missing non-array-result message template
* feat: add GenerateOutcome → SkillOutput thin wrapper
Implements the skill mapping layer per opencli-generate SKILL.md:
- mapOutcomeToSkillOutput: thin translation from P1 contract to agent-facing output
- executeGenerateSkill: entry point accepting SkillInput (url + goal?)
- Message templates for all StopReason and EscalationReason values
- 8 tests covering all outcome paths and contract shape validation
* fix: prefer outcome.message for richer context in needs-human-check
When GenerateOutcome has a message (e.g. "required args: id"), use it
instead of the generic template, so the specific args info reaches the user.
All 68 browser adapter links and 8 desktop adapter links in
docs/adapters/index.md were missing the .md file extension,
causing broken links when navigating the documentation on GitHub.
- Add getDaemonHealth() returning 'stopped' | 'no-extension' | 'ready'
- Delete discover.ts (thin wrapper with no value)
- Bridge uses getDaemonHealth() + _pollUntilReady() (eliminates duplicate polling)
- Doctor simplified: live check auto-starts daemon; no-live mode does minimal
auto-start only when stopped (avoids misreporting idle-exit as failure)
- CommanderAdapter preserves error message/hint detail (not just generic title)
- All callers use single unified status entry point
* fix(xiaohongshu): scope note interaction selectors to .interact-container
The .like-wrapper / .collect-wrapper / .chat-wrapper class names are
also used by every comment's like/reply buttons in the comment section.
querySelector returned the FIRST match — which on a note with comments
is a comment's count, not the post's. As a result, `xiaohongshu note`
returned wrong like/collect/comment counts for any note that had user
comments.
Scoping each selector to .interact-container (the post's main
interaction bar) returns the correct post-level counts.
Verified on multiple notes:
- Note A: was returning likes=2, now correctly returns likes=74
- Note B: was returning likes=1, now correctly returns likes=796
- Note C: was returning likes=1, now correctly returns likes=269
* test(xiaohongshu): add regression check for .interact-container selector scope
Verify the evaluate script passes scoped selectors so unscoped
versions can't silently regress. Follows reviewer suggestion to
assert on page.evaluate.mock.calls[0][0].
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add GitHub Trending, Binance, and Weather (Open Meteo) adapters
GitHub Trending (2 commands, browser mode):
- repos: trending repositories with stars, forks, language filter
- developers: trending developers with popular repos
Supports --since daily/weekly/monthly and --language filter
Binance (11 commands, public API via data-api.binance.vision):
- top: top trading pairs by 24h volume
- price: single pair 24h price stats
- prices: latest prices for all pairs
- ticker: 24h ticker statistics
- gainers: top gaining pairs by 24h change
- losers: top losing pairs by 24h change
- trades: recent trades for a pair
- depth: order book bid prices
- asks: order book ask prices
- klines: candlestick/kline data
- pairs: list active trading pairs
Weather / Open Meteo (11 commands, free public API, no key needed):
- current: current weather for a city
- forecast: daily forecast up to 16 days
- hourly: hourly forecast
- search: city geocoding lookup
- air: air quality index (simple)
- air-quality: detailed air quality (US/EU AQI, PM2.5, PM10, ozone, NO2, SO2)
- sunrise: sunrise/sunset times with UV index
- wind: detailed wind forecast with gusts and 80m altitude
- precipitation: rain/snow forecast with probability
- history: historical weather up to 92 past days
- compare: side-by-side weather comparison across cities
All 24 commands tested with live data. 258 existing tests pass.
* docs: add missing douban, sinablog, substack adapter documentation
* fix(binance): sort numeric metrics and filter active pairs
* chore: drop non-binance adapters from pr214
* chore: drop binance docs from pr214
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat: add Self-Repair protocol for automatic adapter fixing
When an AI agent uses opencli and a command fails, the agent
automatically diagnoses the failure, fixes the adapter, and retries.
- Add CLAUDE.md with Self-Repair protocol (auto-loaded by Claude Code)
- Add designs/self-repair-protocol.md documenting the approach
- Update opencli-repair skill: add Safety Boundaries (AUTH/BROWSER → STOP,
sourcePath-only scope, max 3 rounds), fix AUTH_REQUIRED guidance
- Update opencli-usage skill: add Self-Repair section
Key design decisions:
- Repair target is always RepairContext.adapter.sourcePath (works for both
repo-local clis/ and user-local ~/.opencli/clis/)
- Only adapter files may be modified, never core src/
- Max 3 repair rounds per failure
- AUTH_REQUIRED and BROWSER_CONNECT are hard stops (report, don't modify)
* fix: align auth boundary and scope language across all documents
- Remove "Auth changed (AUTH_REQUIRED)" exploration section from
opencli-repair skill — contradicted the hard stop rule above it
- Update design doc: scope language matches repo-local + explicit skill
delivery model, not universal product behavior
- Update usage skill: reference sourcePath instead of "files under clis/"
* fix: replace remaining repo-relative clis/ paths with sourcePath in design doc
* refactor: rename opencli-repair to opencli-autofix, remove CLAUDE.md
CLAUDE.md was wrong — users don't work inside the opencli repo, and
the protocol shouldn't assume Claude Code. The skill is the portable
delivery mechanism for any AI agent.
- Rename skills/opencli-repair → skills/opencli-autofix
- Remove CLAUDE.md (not the right delivery mechanism)
- Update all references in usage skill and design doc
- Design doc rewritten to reflect skill-first approach
* fix: use sourcePath in example repair session
* feat: emit AutoFix hint on repairable adapter errors
When a command fails with a repairable error (SELECTOR, EMPTY_RESULT,
COMMAND_EXEC, or generic http/not-found), the error output now includes
a hint telling agents to re-run with OPENCLI_DIAGNOSTIC=1 for repair
context. This is the trigger mechanism that bridges the gap between
"command failed" and "agent enters autofix loop".
Non-repairable errors (AUTH_REQUIRED, BROWSER_CONNECT, ARGUMENT) do not
emit the hint — these require user action, not adapter fixes.
* fix: narrow AutoFix hint to adapter-drift errors only
Remove hint from CommandExecutionError (covers env/launcher/runtime
issues, not adapter drift) and generic http errors (often temporary
site issues). Keep hint only for SelectorError, EmptyResultError,
and generic not-found — clear adapter-drift signals.
When the Browser Bridge extension is older than the CLI, sending
'network-capture-start' to the daemon returns 'Unknown action',
causing explore and operate-open to crash with an unhandled error.
Wrap startNetworkCapture calls with .catch() so they degrade
gracefully — explore continues without network capture data, and
operate-open falls back to the JS interceptor injection.
The compose page needs to load Draft.js editor which is heavier than
primaryColumn. 8s is too tight for slow networks and will cause flaky
failures. 15s aligns with the file input timeout (20s) in magnitude.
- engine.ts: replace `git add -A` with scope-aware `execFileSync` to
stage only files matching config.scope globs, and guard against empty
scope degenerating into staging all files
- fix.ts: pass prompt via stdin `input` option instead of shell string
interpolation to prevent $, backtick, and other metacharacter expansion
- generate.ts: update stale comment that claimed unimplemented pipeline
steps (register, verify, Strategy Cascade)
* refactor: remove scoring heuristic, replace with noise filter + metadata
The scoring mechanism was a pre-LLM heuristic that compressed rich endpoint
metadata into a single number. Since this project is designed for AI Agents,
the agent can reason about structured metadata directly.
Changes:
- Remove scoreEndpoint/scoreRequest/scoreWriteRequest and all score fields
- Replace with isNoiseUrl() filter (tracking/beacon/pixel) + isUsefulEndpoint()
- Remove artificial confidence percentages (was score/20)
- Sort by itemCount (transparent, observable) instead of weighted score
- Endpoints now expose full structured metadata for agent consumption
- Net reduction: -43 lines
* fix: widen endpoint filter to keep single-object JSON and stats/metric URLs
- Remove stats/metric from noise pattern — these are often business APIs
- Relax isUsefulEndpoint to keep any JSON endpoint, not just arrays
(preserves /me, /profile, /detail and other single-object APIs)
* fix: add deterministic endpoint ordering for generate/synthesize path
The AI agent path doesn't need ranking, but generate/synthesize still
pick candidates[0] as default — this needs a stable, explainable order.
- Add endpointSortKey() with transparent observable signals: array items,
detected fields, API path patterns, query params
- Update synthesize chooseEndpoint fallback to use itemCount + field count
- Sort key is internal only; not exposed as score to external consumers
* feat(linux-do): split topic content into a dedicated command
Move the old main-post path out of linux-do topic so topic stays a summarized first-page reader while topic-content becomes the Markdown-focused entrypoint for full post bodies.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* feat(linux-do): update topic content handling to include YAML front matter
* fix(linux-do): update default output format to plain for topic-content rendering
* fix(linux-do): replace js-yaml with inline YAML serialization for topic-content
Adapters must only import node builtins, relative modules, or opencli
public APIs. Hand-roll the simple front matter serialization to remove
the third-party js-yaml dependency.
* fix(linux-do): refine YAML quoting to only escape colons followed by space
Colons in URLs (e.g. https://) are valid unquoted YAML values. Only
quote when a colon is followed by a space or appears at end of line.
---------
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* refactor: extract shared scoring logic and consolidate time format utils
- Extract applyUrlScoreAdjustments() and scoreArrayResponse() to analysis.ts,
eliminating duplicated endpoint scoring between explore.ts and record.ts
- Consolidate formatDuration/formatUptime into a single formatDuration(ms)
in download/progress.ts, reused by commands/daemon.ts
* fix: preserve explore scoring semantics and round daemon uptime
- Revert explore.ts scoreEndpoint to original inline /api/ /x/ bonus
without record's tracking/analytics penalty (blocker from review)
- Math.round uptime*1000 to avoid floating-point noise in daemon status
* feat(xueqiu): add kline and groups adapters
Add kline.yaml: fetch candlestick/OHLCV data from Xueqiu v5 chart API.
Supports custom days lookback and outputs date, open, high, low, close,
volume, percent.
Add groups.yaml: list Xueqiu portfolio/group entries.
* fix(xueqiu): correct groups.yaml to use /portfolio/list.json API
The previous implementation used /portfolio/stock/list.json which only
returns stocks in a single group and does not return the group list.
Switch to /portfolio/list.json which returns all portfolio groups
including 实盘, 沪深, 港股, 美股, 模拟(pid=-4), 持仓 etc.
* fix(xueqiu): replace watchlist category param with pid selector
- Remove the unused 'category' parameter (the API ignores it;
all groups live under category=1 regardless)
- Replace with 'pid' parameter to allow fetching any group:
-4=simulated, -5=SH/SZ, -6=US stocks, -7=HK stocks, etc.
- API path still uses category=1 but pid is now user-controllable
---------
* feat(operate): unify network capture + implement CDP consoleMessages
- operate open: start session capture before navigation (catches initial requests)
- operate network: prefer readNetworkCapture() over JS interceptor
- CDPPage: implement consoleMessages() via Runtime.consoleAPICalled
Part of #810
* fix(operate): use correct daemon/CDP entry field names for network capture
Daemon and CDP capture entries use responseStatus/responseContentType/
responsePreview (not status/contentType/responseBody). Fix the
normalization in operate network to match the actual entry shape from
extension/src/cdp.ts.
* fix(cdp): capture Runtime.exceptionThrown in consoleMessages
- Register Runtime.exceptionThrown handler to capture uncaught exceptions
as error-level messages (most valuable diagnostic signal)
- 'error' filter now returns both console.error() and warning/exception
entries, matching typical severity-based logging semantics
* feat(cdp): implement session-level network capture for CDPPage
Implements startNetworkCapture() and readNetworkCapture() on CDPPage using
CDP Network domain events. Updates explore.ts to prefer session capture
over Performance API networkRequests().
Closes part of #810
* fix(cdp): use Network.loadingFinished for reliable body capture
- Move getResponseBody call from responseReceived to loadingFinished,
matching the extension's implementation pattern
- Use extension-compatible entry shape (responseStatus, responseContentType,
responsePreview) instead of custom field names
- Remove unreliable 100ms sleep hack in readNetworkCapture()
- Align with extension/src/cdp.ts:419-437 for consistency
* fix(cdp): drain buffer on readNetworkCapture to match daemon contract
readNetworkCapture() must clear the buffer after reading, matching the
daemon Page's read-and-drain behavior. Without this, repeated reads
would return stale entries.
* fix(cdp): await in-flight body fetches before returning from readNetworkCapture
Track all pending getResponseBody promises and await them in
readNetworkCapture() before draining the buffer. This ensures
explore/diagnostic consumers always get entries with responsePreview
populated, not empty shells where the body fetch hasn't resolved yet.
* fix(explore): handle both legacy and capture entry field names
parseNetworkRequests now maps both shapes:
- Legacy: status, contentType, responseBody
- Capture (extension/CDP): responseStatus, responseContentType, responsePreview
Also clears _pendingBodyFetches on startNetworkCapture reset.
* fix: add safety boundaries to diagnostic output
- Redact sensitive headers (Authorization, Cookie, etc.) from network requests
- Redact sensitive URL query parameters (token, key, secret, etc.)
- Cap individual fields: snapshot (100K chars), adapter source (50K chars),
network requests (50 entries, 4K body each), stack trace (5K chars)
- Enforce 256KB total output budget with graceful degradation:
drops snapshot first, then page state entirely
- Export truncate/redactUrl helpers for testing
* fix: add free-text redaction for all diagnostic string channels
Addresses review feedback: snapshot, consoleErrors, error message/hint/stack
could contain inline secrets (Bearer tokens, JWTs, cookie values, token=value
patterns). All string channels now pass through redactText() before emission.
- Add redactText() with patterns for Bearer tokens, JWTs, cookie values,
and inline key=value secrets
- Apply redactText to: error.message, error.hint, error.stack,
page.snapshot, page.consoleErrors
- Add 6 new test cases for redactText and error message redaction
* fix: resolve adapter source path and add page state collection timeout
Fixes#808 items 1 and 3:
1. adapter.source was missing for all command types because buildRepairContext
only checked cmd._modulePath (set only for manifest lazy-loaded TS).
Now resolveAdapterSourcePath() checks cmd.source first, skips manifest:
pseudo-paths, and maps dist/clis/*.js back to source clis/*.ts.
3. collectPageState() had no timeout — a hung CDP connection would block
error propagation indefinitely. Now wrapped with 5s Promise.race timeout,
falling back to emitting diagnostic without page state.
* fix: track sourceFile in manifest for YAML adapter source resolution
YAML commands inlined in the manifest previously lost their original file
path, causing resolveAdapterSourcePath() to return undefined. Add
sourceFile field to ManifestEntry so discovery can reconstruct the
editable source path for both YAML and TS commands.
* feat: add structured diagnostic output for AI-driven adapter repair
When OPENCLI_DIAGNOSTIC=1 is set, failed commands emit a RepairContext
JSON to stderr containing the error, adapter source, and browser state
(DOM snapshot, network requests, console errors). AI Agents consume
this to diagnose and fix adapters when websites change.
Also adds the opencli-repair skill guide for AI Agents.
* fix: correct e2e test binary path to dist/src/main.js
The e2e helpers pointed to dist/main.js but the actual build output
is at dist/src/main.js (matching package.json "main" field). This
caused all e2e-headed tests to fail with "Cannot find module".
* fix: correct dist/main.js path in autoresearch scripts
* fix: emit diagnostic for pre-session browser failures
When browser connection fails before the session callback runs
(e.g., BrowserConnectError), the inner diagnostic catch never fires.
Use a flag to ensure the outer catch emits diagnostic as a fallback.
* test: tolerate unavailable Bloomberg RSS feeds in e2e
* test: skip flaky bloomberg businessweek e2e test
The Bloomberg Businessweek RSS feed is intermittently unavailable,
causing CI failures unrelated to code changes.
* revert: restore bloomberg businessweek e2e coverage
* fix: avoid inserting completion config inside multi-line shell commands
The postinstall zshrc insertion logic splits backslash-continued blocks
(e.g. zinit stanzas) when it finds a compinit match inside them, which
breaks the user's shell config. Walk backward past continuation lines
so the insertion lands before the entire logical command.
* fix: append zsh completion to end of .zshrc instead of splicing
Replace the fragile compinit-searching splice logic with a simple
append, matching the strategy already used for bash. This avoids
breaking multi-line commands (e.g. zinit blocks with zicompinit).
Still detects existing compinit to avoid adding a duplicate call.
* fix: stop modifying shell rc files in postinstall
Replace the fragile .zshrc/.bashrc modification logic with a safer
approach: only write completion files and print setup instructions.
The previous approach tried to parse and splice into rc files, which
broke multi-line shell commands (e.g. zinit blocks with backslash
continuations matching /compinit/). Instead of attempting to fix the
parser, remove rc modification entirely — this matches the approach
used by rustup, homebrew, and other CLI tools.
Closes#788
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(discovery): expose runtime deps to user adapters
* fix: route copied adapters through opencli exports
* refactor: route adapter status output through logger
* refactor: migrate adapter imports to package exports
Replace all relative imports (../../src/registry.js, ../../browser/cdp.js, etc.)
with package exports (@jackwener/opencli/registry, @jackwener/opencli/errors, etc.)
across all 484 adapter files.
This decouples adapter import resolution from directory structure:
- User CLIs in ~/.opencli/clis/ resolve via node_modules symlink
- Internal adapters resolve via Node.js self-referencing
- No more shim files needed for import resolution
Changes:
- package.json: add sub-path exports for all public modules
- clis/**: replace relative imports with @jackwener/opencli/...
- discovery.ts: simplify ensureUserCliCompatShims to symlink-only
- registry-api.ts: export CommandArgs type
- Remove root-level shim directories (browser/, download/, pipeline/)
- Remove shim entries from tsconfig.json include and package.json files
* test: add regression tests for package exports
Prevents regressions like #788/#791 by:
1. Scanning all adapter files for forbidden relative imports
(../../src/, ../../browser/, etc.) — fails if any remain
2. Verifying every package.json export maps to an existing source file
18 new test cases.
* fix: use junction on Windows + broaden test patterns
- discovery.ts: use 'junction' symlink type on Windows (no admin required)
- package-exports.test.ts: generalize forbidden patterns to catch any
depth of ../ traversal (not just ../../ and ../../../)
* fix: update stale vi.mock/importActual paths in adapter tests
Test files still used old relative paths for vi.mock() and
vi.importActual() calls. Updated 5 test files to use package exports.
Also broadened regression test patterns to catch mock/importActual paths.
* fix: use rm instead of unlink for symlink cleanup, add warn on failure
Addresses review feedback from Astro-Han:
- rm() handles both symlinks and stale directories (unlink fails on dirs)
- Log a warning when symlink creation fails instead of silent catch
* docs: update import examples to use package exports
Update all documentation, contributing guides, and skills to use
@jackwener/opencli/registry instead of ../../src/registry.js.
Without this, users following the docs would write adapters with
broken imports since the old shim files are no longer created.
Bug 1: version.ts used a single-level parent lookup for package.json,
which broke after #784 changed rootDir from "src" to "." (version.js
now lives in dist/src/ instead of dist/). Walk up until package.json
is found — works in both dev (src/) and prod (dist/src/).
Bug 2: adapters copied to ~/.opencli/clis/ import ../../src/registry.js
etc., which resolves to ~/.opencli/src/. Derive src/ compat shims from
the existing rootShims list so these imports resolve correctly.
- Replace 140 instances of `src/clis/` → `clis/` across 12 doc files
(path changed after repo restructure)
- Remove non-command `rpc` and `rankings` from notebooklm/amazon
command lists in README, README.zh-CN, SKILL.md, and adapters index
(these are internal utility modules, not user-facing commands)
- Add `deep-research` and `deep-research-result` to gemini adapter doc
- Add `movers-shakers` and `new-releases` to amazon adapter doc
- Update notebooklm doc examples to use canonical commands instead of
deprecated aliases (`metadata` → `get`, `notes-list` → `note-list`)
- Bump SKILL.md version to 1.6.3
* docs: fix outdated commands and adapter counts in README and skills
- Add gemini deep-research and deep-research-result commands
- Fix notebooklm: remove non-existent select/metadata/notes-list, add rpc
- Add amazon movers-shakers, new-releases, rankings commands
- Add missing weibo commands in zh-CN README
- Fix linux-do missing hot/latest/category in zh-CN README
- Update adapter count from 73+ to 79+
- Update skills version to 1.6.2
- Add full spotify command list in skills SKILL.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(test): update xiaohongshu note tests for search_result URL change
buildNoteUrl now uses /search_result/<id> instead of /explore/<id> for
bare note IDs. Update test expectations to match:
- buildNoteUrl test: expect /search_result/ not /explore/
- goto URL assertion: expect /search_result/ not /explore/
- empty shell hint: match actual error message text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(test): update xiaohongshu comments test for search_result URL change
- Bare note ID now navigates to /search_result/ not /explore/
- Full URL inputs are preserved as-is (including /explore/ URLs)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: simplify core modules — remove root shims, consolidate error classification, streamline cascade/interceptor, clean up synthesize
1. Remove root-level shim files (errors.ts, logger.ts, registry.ts, types.ts, utils.ts, launcher.ts) — update all ~840 adapter imports to reference src/ directly
2. Consolidate interceptor: reuse shared DISGUISE_FN in tap interceptor instead of reimplementing
3. Unify error classification: single ClassifiedError type with icon/exitCode/hint lookup table, eliminating duplicated pattern matching between resolveExitCode and renderError
4. Simplify cascade probe: replace repetitive switch cases with PROBE_OPTIONS lookup map
12. Clean up synthesize.ts: remove deprecated snake_case field aliases (recommended_args, recommended_columns, recommendedColumnsLegacy) and unnecessary constant aliases
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update import paths in contributor docs and skill templates
Update all documentation and skill files to reference src/ directly,
matching the shim removal in the previous commit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update new Gemini adapter imports to use src/ paths
Fix imports in newly added deep-research adapter files that were
still referencing the deleted root shim files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update xiaohongshu tests for /search_result/ URL change
Tests now expect /search_result/<id> for bare note IDs (matching
the note-helpers.ts change from PR #774) and updated empty-shell
hint assertion.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update LessWrong and hupu adapter imports to use src/ paths
Fix imports in newly merged LessWrong and hupu/mentions adapter
files that were still referencing the deleted root shim files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test(xueqiu): mock logger via src path
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(hupu): add hupu cli adapter
* fix(hupu): prevent detail from returning the wrong thread
* refactor: deduplicate shared utilities in hupu adapter
- Merge postHupuJson and postHupuReplyJson into single function with mode parameter
- Move stripHtml and decodeHtmlEntities to utils.ts, remove duplicate definitions
* fix(hupu): add mentions command
* fix: move mentions.ts to clis/hupu/, remove src/clis/hupu duplicates
Post PR #782 restructure: adapter files live at root clis/, not src/clis/.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-04-05 03:26:40 +08:00
2356 changed files with 213762 additions and 87447 deletions
Adapter polish release: new web search adapters, better Browser Bridge tab group reuse, and social adapters returning to one-shot tab leases. Extension package version is bumped to 1.0.15 for the Browser Bridge fix.
### Features
* **search** — add DuckDuckGo, Brave, and Yahoo web search adapters. ([#1546](https://github.com/jackwener/opencli/issues/1546))
* **boss** — support job-seeker `chatlist` and `chatmsg` adapters. ([#1539](https://github.com/jackwener/opencli/issues/1539))
### Bug Fixes
* **extension** — reuse existing `OpenCLI Adapter` tab groups before creating new ones, including cross-window discovery, legacy `OpenCLI` title fallback, and deterministic candidate selection. ([#1541](https://github.com/jackwener/opencli/issues/1541))
* **twitter, reddit** — default browser-backed social adapters back to ephemeral tab leases. Twitter/X and Reddit commands now release their site tab after each run while keeping the shared Adapter window available for reuse; persistent sessions remain reserved for AI/chat-style adapters that need long-lived conversation state. ([#1569](https://github.com/jackwener/opencli/issues/1569))
External CLI surface cleanup + Browser Bridge WebSocket lifecycle hardening. Two BREAKING changes around external CLIs: built-in `tg`/`discord`/`wx` (was `tg-cli`/`discord-cli`/`wx-cli`) now match their real binary names, and Notion's in-tree CDP adapter is replaced by the official `ntn` external CLI.
### ⚠ BREAKING CHANGES
* **notion** — remove the in-tree `clis/notion/` CDP-on-Desktop adapter (8 commands: `status` / `search` / `read` / `new` / `write` / `sidebar` / `favorites` / `export`). Notion has shipped an official CLI at <https://ntn.dev>, registered as a first-class external CLI in `external-clis.yaml`. Migration: install `ntn` from <https://ntn.dev> (`curl -fsSL https://ntn.dev | bash`), then use `opencli ntn <command>`. Auto-install is intentionally not configured because the official installer is a shell script while OpenCLI external installs run shell-free command strings. The official CLI uses the public Notion API rather than reverse-engineering the Desktop UI, so it survives Notion app updates and exposes a wider command surface (blocks / databases / properties / comments) than the reverse-engineered adapter could. ([#1559](https://github.com/jackwener/opencli/issues/1559))
* **external** — drop the `-cli` suffix from built-in external CLI subcommand names. `opencli tg-cli`, `opencli discord-cli`, `opencli wx-cli` are now `opencli tg`, `opencli discord`, `opencli wx`, matching the real binary names that those tools install as. Root help still shows the package lineage as `tg(tg-cli)` / `discord(discord-cli)` / `wx(wx-cli)`. ([#1544](https://github.com/jackwener/opencli/issues/1544))
### Features
* **twitter** — `bookmarks` and `bookmark-folder` now include media via `extractMedia`, reaching parity with `timeline` / `search`. ([#1555](https://github.com/jackwener/opencli/issues/1555))
* **twitter/list-tweets** — include media via `extractMedia` (parity with `timeline` / `search`). ([#1464](https://github.com/jackwener/opencli/issues/1464))
### Bug Fixes
* **daemon** — report ambiguous browser command outcomes with a distinct `command_result_unknown` errorCode and `503` when the extension WebSocket drops between command dispatch and result delivery. `sendCommandRaw()` treats this code as hard non-retryable, so write-side commands (`navigate` / `click` / `type` / `eval`) won't be silently re-issued and double-executed. Daemon exposes a `commandResultUnknown` counter on `/status` for future observability. ([#1558](https://github.com/jackwener/opencli/issues/1558))
* **extension** — keep active daemon WebSocket; stale sockets no longer clobber active connection (`onopen` / `onclose` / `onmessage` are all gated by `ws !== thisWs` short-circuit), and `safeSend` only fires when `readyState === OPEN`. ([#1540](https://github.com/jackwener/opencli/issues/1540))
* **extension** — coalesce concurrent daemon WebSocket connects via an in-flight promise. Startup / keepalive / reconnect triggering `connect()` during the daemon-probe or context-lookup async gap no longer creates duplicate real WebSocket connections. ([#1554](https://github.com/jackwener/opencli/issues/1554))
* **external** — distinguish external CLI executable names from distribution/project names in root help. Built-in aliases such as `tg`, `discord`, `wx` remain the callable `opencli <name> ...` entrypoints while help renders `tg(tg-cli)`, `discord(discord-cli)`, `wx(wx-cli)` to show their package lineage. ([#1560](https://github.com/jackwener/opencli/issues/1560))
### Docs
* **browser** — clarify named session lifecycle in the Browser Bridge guide. ([#1542](https://github.com/jackwener/opencli/issues/1542))
Major hotfix + simplification batch. Extension bumped to 1.0.14. Node floor lowered to v20 so the long tail of Node v20–v21.6 users no longer crashes at module load. `opencli browser` user surface replaces required-flag `--session <name>` with a `<session>` positional. `page.evaluate(fn, ...args)` adds a type-safe alternative to the implicit auto-IIFE string form. Twitter cursor pagination no longer silently caps at ~500 items.
### ⚠ BREAKING CHANGES
* **browser** — replace the `--session <name>` flag with a `<session>` positional argument that immediately follows `browser`. `opencli browser work click 12` instead of `opencli browser --session work click 12`; `opencli browser work bind` instead of `opencli browser bind --session work`. Required-flag semantics are now encoded structurally as a positional, matching the Docker/git convention for required operation-target identifiers. The internal `--session` flag is preserved for the daemon protocol and for direct `program.parseAsync` callers but is no longer part of the user-facing surface. ([#1505](https://github.com/jackwener/opencli/issues/1505))
* **env** — remove `OPENCLI_KEEP_TAB`. The flag was a debugging shortcut, not a config dimension: `--keep-tab true|false` on the command line is the single source of truth, and adapter `siteSession: 'persistent'` already pins persistent site tabs as a hard constraint. Removing the env eliminates a globally-leaking process state that overrode every browser command in the shell. ([#1509](https://github.com/jackwener/opencli/issues/1509))
* **extension** — remove the internal `surface\\0session` command-session backdoor. Browser Bridge commands now route only through structured `session` + `surface` fields; lease-key strings remain an extension-internal registry detail. ([#1510](https://github.com/jackwener/opencli/issues/1510))
### Features
* **browser** — add `page.evaluate(fn, ...args)` for type-safe browser-context evaluation with JSON-serialized arguments. String evaluation remains supported, but new adapter code should use function form to avoid implicit `wrapForEval` auto-IIFE magic. ([#1508](https://github.com/jackwener/opencli/issues/1508))
* **twitter** — default `tweets` command to the logged-in user when `user` is omitted, and fix the sibling envelope-unwrap silent bug. ([#1531](https://github.com/jackwener/opencli/issues/1531))
* **zhihu** — add `answer-detail` to fetch a single answer's full content. ([#1528](https://github.com/jackwener/opencli/issues/1528))
* **zhihu** — paginate question answers and recommendations. ([#1517](https://github.com/jackwener/opencli/issues/1517))
* **browser** — `page.evaluate()` / `evaluateInFrame()` now return the user JavaScript value directly. Browser Bridge `exec` previously routed through a shared `pageScopedResult` helper that spread / wrapped the lease's `session` into the result `data`, contaminating arbitrary user returns: array / primitive returns came back as `{ session, data }` envelopes, and plain-object returns had an extra `session` key injected (overwriting any user `session` field). `google search` and `xiaohongshu search` were the visible repro — Chrome rendered results correctly but adapters extracted an empty array. Fixed in extension 1.0.14 by reverting `pageScopedResult` to its pre-1461 form (`{ id, ok, data, page }`); no client-side unwrap is needed. ([#1518](https://github.com/jackwener/opencli/issues/1518))
* **twitter** — raise fixed cursor-pagination caps in `bookmarks` / `likes` / `tweets` / `timeline` / `bookmark-folder` / `list-tweets` / `search` / `following`. The old `i < 5` / `i < 10` literals and following's `Math.ceil(limit / 50) + 2` formula imposed hidden result ceilings below `--limit`; the loop now treats the page count as a high runaway guard while `--limit` and cursor exhaustion control normal pagination. ([#1532](https://github.com/jackwener/opencli/issues/1532))
* **twitter** — repair `list-add` / `list-tweets` / `lists` / `following` after 2026-05 site changes. ([#1503](https://github.com/jackwener/opencli/issues/1503))
* **twitter** — repair `search` and `tweets` readback. ([#1512](https://github.com/jackwener/opencli/issues/1512))
* **twitter** — make reply submission robust. ([#1511](https://github.com/jackwener/opencli/issues/1511))
* **google/search** — wait for `#rso a h3` before extracting, falling back to the existing fixed wait. On Chrome 148 + Linux Wayland the DOM can settle before SERP anchors are populated, making extraction return empty even with the envelope bug fixed. ([#1518](https://github.com/jackwener/opencli/issues/1518))
* **google/search** — wrap evaluate return value in object to fix serialization. ([#1523](https://github.com/jackwener/opencli/issues/1523))
* **google-scholar/search** — wrap evaluate return to fix serialization. ([#1525](https://github.com/jackwener/opencli/issues/1525))
* **xiaohongshu/search** — extract initially visible cards before scrolling, then merge post-scroll rows by URL. Xiaohongshu's virtualized masonry layout can evict the initial cards from the DOM after scroll, so the previous always-scroll-then-extract flow could lose the top results. ([#1518](https://github.com/jackwener/opencli/issues/1518))
* **xiaohongshu+rednote/search** — fall back to href-based note cards when `section.note-item` class is dropped. ([#1507](https://github.com/jackwener/opencli/issues/1507))
* **xueqiu** — `kline` / `earnings-date` format dates in Asia/Shanghai instead of UTC. ([#1498](https://github.com/jackwener/opencli/issues/1498))
* **runtime** — lower the Node floor to `>=20.0.0`. Three coupled changes: drop all `util.styleText()` usage (added in Node v21.7.0 / v20.12.0; previously crashed v21.0–v21.6 at module load), downgrade `undici` from `^8.0.2` (engines `>=22.19.0`) to `^6.25.0` (engines `>=18.17`, retains `Agent` / `EnvHttpProxyAgent` / `fetch`), and lower `MIN_SUPPORTED_NODE_MAJOR` from 21 to 20 so the startup guard matches the declared `engines.node`. Smoke-tested on v20.0.0 / v21.2.0 / v22.22.2. The semantic markers (`[OK]` / `[WARN]` / `[FAIL]` / `ℹ` / `⚠` / `✖`) keep their meaning; ANSI colors were redundant for the primarily agent-facing CLI. ([#1524](https://github.com/jackwener/opencli/issues/1524))
* **extension 1.0.14** — `pageScopedResult` no longer injects `session` into `data`. The field had no consumers and contaminated `exec` results with arbitrary user-JS shapes; routing-relevant identity is already exposed via `Result.page`. ([#1518](https://github.com/jackwener/opencli/issues/1518))
* **ci** — drop `e2e-headed` and `adapter-test` from `pull_request` triggers (kept on `push` to main / nightly / `workflow_dispatch`). PR-time CI now targets ~2 min wall-time. ([#1521](https://github.com/jackwener/opencli/issues/1521), [#1522](https://github.com/jackwener/opencli/issues/1522))
* **scripts** — auto-refresh `dist/` before `build-manifest`. ([#1490](https://github.com/jackwener/opencli/issues/1490))
Hotfix release for the 1.7.17 doctor regression: `opencli doctor` failed connectivity probe with `Browser session is required` because the doctor probe didn't pass a session to the new strict-session browser bridge. Also adds new adapters and adapter fixes that were ready immediately after 1.7.17.
### Bug Fixes
* **doctor** — pass an internal `__doctor__` browser session to the live connectivity probe so `opencli doctor` works again under the explicit-session browser model introduced in 1.7.17. ([#1485](https://github.com/jackwener/opencli/issues/1485))
* **browser** — `--session <name>` is now declared as a `requiredOption` so Commander itself rejects calls missing the flag before runtime, and the help line is marked `(required)` instead of being hidden under `Options:`. ([#1485](https://github.com/jackwener/opencli/issues/1485))
* **doubao/ask** — restore Assistant detection after the 2026-05 DOM refactor. ([#1484](https://github.com/jackwener/opencli/issues/1484))
* **youtube** — request `srv3` format for caption URLs. ([#1422](https://github.com/jackwener/opencli/issues/1422))
Extension bumped to 1.0.12 (workspace → session lease routing, drop `handleSessions` handler). Major simplification pass: browser/adapter session model rewrite, `--workspace` removed, doctor surface trimmed to its core job.
### ⚠ BREAKING CHANGES
* **browser session model** — replace the browser-facing `--workspace` model with explicit `--session <name>` on `opencli browser *`. Browser commands now require a session name, `browser bind`/`unbind` use `--session`, and bind no longer accepts `--domain`, `--path-prefix`, or `--allow-navigate-bound`. Browser primitives keep their session tab by design; the browser namespace no longer exposes `--keep-tab`. ([#1461](https://github.com/jackwener/opencli/issues/1461))
* **adapter site sessions** — replace adapter metadata `browserSession: { reuse: 'site' }` with `siteSession: 'persistent'`, and replace the user override `--reuse <none|site>` / `OPENCLI_BROWSER_REUSE` with `--site-session <ephemeral|persistent>`. Persistent site sessions keep a stable site tab open without idle expiry. ([#1462](https://github.com/jackwener/opencli/issues/1462))
* **doctor** — remove `--no-live` and `--sessions` flags from `opencli doctor`. Doctor always runs the live browser connectivity probe (that's its core job); session enumeration was never part of health diagnosis. The underlying `'sessions'` daemon protocol action and the `BrowserSessionInfo` public type are removed as dead code. ([#1470](https://github.com/jackwener/opencli/issues/1470))
### Features
* **chatgpt** — `ask` and `send` now accept local image paths and upload them through the composer before submitting the prompt. ([#1476](https://github.com/jackwener/opencli/issues/1476))
### Internal
* **extension 1.0.12** — drop `handleSessions` action handler (no remaining consumers after doctor cleanup).
* **extension 1.0.11** — switch Browser Bridge lease routing from user-facing workspaces to explicit browser sessions.
* **external** — register `tg-cli`, `discord-cli`, and `wx-cli` as external CLI integrations. ([#1458](https://github.com/jackwener/opencli/issues/1458))
### Bug Fixes
* **xiaohongshu** — fall back to base64 upload when CDP `DOM.setFileInputFiles` returns `Not allowed` on creator center. ([#1374](https://github.com/jackwener/opencli/issues/1374))
* **chatgpt** — switch to locale-stable send button selector so non-English UIs don't break send. ([#1354](https://github.com/jackwener/opencli/issues/1354))
### Performance
* **adapters** — hoist cookie reads to `page.getCookies` across Tier 1 (25 files), eliminating per-call CDP round trips. ([#1450](https://github.com/jackwener/opencli/issues/1450))
* **twitter** — drop redundant `goto + wait` in adapter steps; framework auto pre-navigates. ([#1451](https://github.com/jackwener/opencli/issues/1451))
* **twitter** — enable `browserSession.reuse: 'site'` on 17 read-only adapters so repeated reads share one tab. ([#1454](https://github.com/jackwener/opencli/issues/1454))
* **browser** — split interactive and automation windows so `opencli browser *` and adapter-driven background commands no longer share one Chrome window; tab groups are isolated by role.
### Internal
* **extension 1.0.10** — rename the adapter-owned Chrome tab group from `OpenCLI Automation` to `OpenCLI Adapter`. ([#1457](https://github.com/jackwener/opencli/issues/1457))
* **docs** — list `tg-cli`, `discord-cli`, `wx-cli` in External CLI README sections. ([#1459](https://github.com/jackwener/opencli/issues/1459))
Extension bumped to 1.0.9 (Accessibility.enable allowlist + downloads permission + cross-origin frame target attach for AX). Major Browser Agent Runtime release: full Phase 0/1/2 alignment with `vercel-labs/agent-browser` model — CDP-primary input, AX snapshot/refs with stale recovery, semantic locators across all primitives, full form toolbelt (hover/focus/dblclick/check/uncheck/upload/drag/wait-download), annotated screenshots, and same-origin iframe AX routing. Cross-origin OOPIF AX is best-effort (Chrome extension API limitation).
### ⚠ BREAKING CHANGES
* **browser lifecycle** — replace `--focus` / `OPENCLI_WINDOW_FOCUSED` with `--window foreground|background` / `OPENCLI_WINDOW`, and replace `--live` / `OPENCLI_LIVE` with `--keep-tab true|false` / `OPENCLI_KEEP_TAB`. `opencli browser *` defaults to a foreground window and keeps its tab; browser-backed adapter commands default to a background automation window and release their tab unless the adapter uses site-level reuse.
### Features
* **help / browser** — `opencli browser --help -f yaml|json` now emits a structured, agent-ready index of all browser leaf commands (including nested `tab`, `get`, and `dialog` commands), their positionals, command options, namespace options, and root global options. Individual browser commands also support structured help, backed by a shared Commander option/argument spec extractor.
* **help / built-in namespaces** — `opencli daemon|plugin|adapter|profile --help -f yaml|json` now emit the same structured payload as `browser`. One agent call returns every leaf's positionals, options, descriptions, and global options — no per-leaf `--help` follow-ups needed. Original namespace descriptions are preserved through `applyRootSubcommandSummaries()` via a snapshot at namespace declaration time.
* **browser state** — add opt-in AX snapshot refs via `browser state --source ax`, including backend-node click resolution and role/name stale-ref recovery for the Phase 0 browser-agent runtime prototype.
* **browser state** — AX snapshots now include same-origin iframe refs, and `browser state --compare-sources` prints DOM-vs-AX observation metrics for the Phase 1 default-source decision without dumping page contents.
* **browser locators** — `browser find`, `browser click`, and `browser get text|value|attributes` now accept semantic locator flags (`--role`, `--name`, `--label`, `--text`, `--testid`) so agents can act on common controls without a separate state-ref lookup.
* **browser locators** — semantic locator flags now work across input/action primitives (`type`, `fill`, `select`, `hover`, `focus`, `dblclick`, `check`, `uncheck`, `upload`) plus prefixed `--from-*` / `--to-*` locators for `drag`.
* **browser actions** — add `browser hover`, `browser focus`, and `browser dblclick` primitives backed by the same target resolver and CDP input path as `browser click`.
* **browser actions** — add `browser check` and `browser uncheck` primitives that ensure checkbox / radio / aria-checked controls reach the requested state instead of blindly toggling.
* **browser upload** — add `browser upload <target> <file...>` to attach local files to `input[type=file]` targets through CDP `DOM.setFileInputFiles`, with local path validation and file-input verification.
* **browser actions** — add `browser drag <source> <target>` for CDP mouse drag sequences between two resolved element centers.
* **browser wait / extension 1.0.8** — add `browser wait download [pattern]` backed by Chrome's downloads lifecycle API, so agents can wait for file downloads by filename/URL pattern and receive completed/failed download metadata.
* **browser state / extension 1.0.9** — AX snapshots can now route same-origin iframe refs through `frameId`. Cross-origin OOPIF AX routing is best-effort because real Chrome extension smoke tests show `chrome.debugger` may not expose attachable iframe targets to extensions.
* **browser screenshot** — add `browser screenshot --annotate`, which refreshes DOM refs and overlays visible `[N]` labels on the screenshot so visual inspection maps back to `browser click <ref>` targets.
### Bug Fixes
* **browser click** — `browser click` now prefers CDP `Input.dispatchMouseEvent` over DOM `el.click()`, so custom dropdowns that depend on pointer/mouse events (Radix, shadcn, Material UI, Mercury-style category pickers) open and select reliably while retaining JS click as a fallback for older backends or zero-rect targets.
* **browser state / extension 1.0.7** — `browser state --source ax` now enables the CDP Accessibility domain before reading the AX tree, fixing real-Chrome snapshots that previously returned only `RootWebArea` with zero refs.
* **help / build** — every positional arg must now declare a non-empty `help` string. The build-manifest step fails closed when a positional has empty / whitespace-only / missing `help`, so `opencli <site> <cmd> --help` always shows callers what each parameter is for. Pre-existing offenders (`twitter followers/following/list-add/list-remove/list-tweets/search/thread`, `reddit search/subreddit/user/user-comments/user-posts`, `douyin stats/update`, `bilibili subtitle`, `jike search`) now have explicit help text — most notably `twitter followers [user]` and `following [user]` now document that omitting the user fetches the currently logged-in account.
* **help** — adapter help is now agent-friendly: per-command listings drop the `[options]` noise from globally-shared options (`--format`, `--trace`, `-v`, `-h`, etc.) and only mention them at the site level, so `opencli twitter` etc. read like a flat command index. ([#1401](https://github.com/jackwener/opencli/issues/1401))
* **twitter** — write-action symmetry P0: add `unlike`, `retweet`, `unretweet`, and `quote` to round out the read/write coverage. ([#1400](https://github.com/jackwener/opencli/issues/1400))
### Bug Fixes
* **browser daemon** — `npm install -g @jackwener/opencli@latest` now correctly auto-restarts a stale ready-state daemon so users pick up the new version without a manual `opencli daemon restart`. ([#1399](https://github.com/jackwener/opencli/issues/1399))
Extension bumped to 1.0.6 (screenshot `--width` / `--height` / `--full-page` flags, automation tab group color marker, automation container reuse fix).
### ⚠ BREAKING CHANGES
* **linux-do** — remove deprecated compatibility shims `linux-do hot`, `linux-do category`, `linux-do latest`. Use `linux-do feed --view top --period <period>`, `linux-do feed --category <id-or-name>`, and `linux-do feed --view latest` instead.
* **grok ask** — drop the `--web` flag and the legacy `<textarea>` composer path. The default flow is now the only path and uses the current ProseMirror+TipTap composer (the path that used to require `--web true`). Existing scripts passing `--web` will get an "unknown option" error from commander; remove the flag.
* **env** — rename `OPENCLI_BROWSER_TIMEOUT` to `OPENCLI_BROWSER_IDLE_TIMEOUT`. The variable controls workspace lease idle release time, not per-command runtime; the new name reflects that. Old name was undocumented and removed without a fallback.
* **registry** — remove the unused `Strategy.HEADER`; adapter authors should use `Strategy.COOKIE` and set headers explicitly inside browser-side fetches.
### Features
* **observation** — add trace artifact primitives, `browser console`, `browser network --since/--follow/--failed`, and adapter `--trace=retain-on-failure` for failure-retained browser evidence.
* **autofix** — retire `OPENCLI_DIAGNOSTIC`; adapter repair now uses `--trace retain-on-failure`, trace `summary.md`, and error-envelope trace metadata.
* **browser** — `bind` attaches `bound:*` workspaces to user-owned Chrome tabs without taking over window lifecycle; `sessions` reports `idleMsRemaining: null` for bound workspaces because they do not schedule idle close timers. ([#1169](https://github.com/jackwener/opencli/issues/1169), [#929](https://github.com/jackwener/opencli/issues/929))
* **browser lifecycle** — owned browser workspaces now lease tabs inside a shared dedicated automation container instead of owning one Chrome window per workspace; lease state is persisted for MV3 service-worker reconciliation and idle cleanup is backed by alarms.
* **browser session** — adapter commands can opt into site-level tab reuse with `browserSession.reuse = 'site'`; Grok and other browser-backed LLM adapters now keep a shared site tab by default, and users can override with `--reuse <none|site>`.
* **grok** — add browser-web baseline commands: `read`, `history`, `detail`, `new`, `send`, and `status` (existing `ask` and `image` unchanged).
* **yuanbao** — add browser-web baseline commands: `send`, `status`, `read`, `history`, and `detail` (joining the existing `ask` and `new`).
* **qwen** — add `detail` command for opening a specific historical conversation by id.
* **web read** — make page extraction render-aware: same-origin iframe content is merged into the Markdown source, `--wait-for` can wait inside main/iframe documents, `--wait-until networkidle` waits for captured requests to settle, and `--diagnose` reports frames, empty containers, and API-like XHRs for shell/AJAX pages.
### Bug Fixes
* **pipeline / capabilityRouting** — the `fill` pipeline step (introduced in [#1222](https://github.com/jackwener/opencli/issues/1222)) now correctly triggers a browser session and gets transient retry coverage; previously a pipeline using only `fill` could crash on a missing page object. ([#1393](https://github.com/jackwener/opencli/issues/1393))
* **xiaohongshu publish** — improve image publishing reliability via creator-center URL routing, tab priority handling, and DataTransfer fallback.
* **youtube** — use watch-page HTML for transcript captions to recover when the public transcript API is unavailable.
* **desktop adapters** — restore 11 desktop adapter commands that were lost from the manifest due to a factory-pattern regression.
### Internal
* **cleanup** — remove dead `src/analysis.ts` (179 lines, 0 importers), retire `OPENCLI_DIAGNOSTIC` test residue, derive validator step allowlist from the live pipeline registry to prevent future drift.
Extension bumped to 1.0.2 (body-truncation signal unified across raw / detail / fallback paths).
### Features
* **Window lifecycle flags** — `--live` (or `OPENCLI_LIVE=1`) keeps the automation window open after a command finishes; `--focus` (or `OPENCLI_WINDOW_FOCUSED=1`) brings the window to the foreground. Works on any subcommand. ([#1122](https://github.com/jackwener/opencli/issues/1122))
* **Agent-native browser payload** — structured `network` bodies with truncation signal, `get html --as json` with `--depth` / `--children-max` / `--text-max` budgets, new `browser extract` command for long-form content with resume cursor. ([#1104](https://github.com/jackwener/opencli/issues/1104))
* **`network --filter <fields>`** — filter captured requests by body-shape path segments for quick API discovery. ([#1103](https://github.com/jackwener/opencli/issues/1103))
* **`get html --as json`** — structured HTML tree output; no more silent truncation on raw `--as html`. ([#1102](https://github.com/jackwener/opencli/issues/1102))
* **`browser network` rewrite** — agent-native discovery with cache keys and shape preview. ([#1100](https://github.com/jackwener/opencli/issues/1100))
* **Compound form fields** — date / select / file controls surface a `compound` envelope with format, options, `accept`. Cascading stale-ref recovery + bbox 0.99 dedup for tagged elements. ([#1116](https://github.com/jackwener/opencli/issues/1116))
* **twitter `tweets`** — fetch a user's recent posts. ([#1098](https://github.com/jackwener/opencli/issues/1098))
* **bilibili `video`** — new video command. ([#1110](https://github.com/jackwener/opencli/issues/1110))
* **deepseek `--file`** — file upload support on `ask`. ([#1093](https://github.com/jackwener/opencli/issues/1093))
### Bug Fixes
* **twitter** — 5s timeout on `resolveTwitterQueryId` to prevent hang. ([#1106](https://github.com/jackwener/opencli/issues/1106))
* **youtube** — fall back to Videos tab when Home has no videos. ([#1109](https://github.com/jackwener/opencli/issues/1109))
* **jianyu** — keep accessible detail urls in search. ([#1099](https://github.com/jackwener/opencli/issues/1099))
* **xiaoyuzhou** — migrate from broken SSR to authenticated API ([#1059](https://github.com/jackwener/opencli/issues/1059)); accept `CONFIG_ERROR` in E2E guard ([#1066](https://github.com/jackwener/opencli/issues/1066))
* **xiaohongshu** — detect draft save success ([#1060](https://github.com/jackwener/opencli/issues/1060)); verify title input sticks on publish ([#1050](https://github.com/jackwener/opencli/issues/1050))
* **twitter** — repair lists scraping from detail pages ([#1053](https://github.com/jackwener/opencli/issues/1053))
* **zsxq** — separate content from title, remove title truncation ([#1079](https://github.com/jackwener/opencli/issues/1079))
* **extension** — per-workspace idle timeout for browser sessions ([#1064](https://github.com/jackwener/opencli/issues/1064))
This is a major release with significant internal architecture changes.
Adapter code, validation, and error handling have been modernized.
### ⚠ BREAKING CHANGES
* **Node.js >= 21 required** — `import.meta.dirname` is used in core modules; Node 20 and below will fail at startup.
* **YAML adapters deprecated** — YAML-based `.yaml` adapters are no longer loaded. Existing YAML adapters must be converted to JS via `cli()` API. A deprecation warning is emitted if `.yaml` files are detected.
* **`.ts` adapters no longer loaded at runtime** — The runtime only discovers `.js` files. If you have `.ts` adapters in `~/.opencli/clis/`, compile them to `.js` or rewrite using plain JS. A warning is printed when `.ts` files without a matching `.js` are found.
* **Error output format changed** — All errors are now emitted as a structured YAML envelope to stderr. Scripts parsing stdout for `[{error, help}]` must switch to stderr / exit code. ([#923](https://github.com/jackwener/opencli/issues/923))
* **`tabId` replaced by `targetId`** — Cross-layer page identity now uses `targetId`. Extensions and plugins referencing `tabId` must update. ([#899](https://github.com/jackwener/opencli/issues/899))
* **`operate` renamed to `browser`** — All `opencli operate` commands are now `opencli browser`. ([#883](https://github.com/jackwener/opencli/issues/883))
### Features
* **auto-close adapter windows** — Browser tabs opened by adapters are automatically closed after execution; configurable via `OPENCLI_WINDOW_FOCUSED`. ([#915](https://github.com/jackwener/opencli/issues/915))
* **auto-downgrade to YAML in non-TTY** — Machine-readable output when piped. ([#737](https://github.com/jackwener/opencli/issues/737))
* **Browser Use improvements** — Better click/type/state handling for browser automation. ([#707](https://github.com/jackwener/opencli/issues/707))
* **CDP session-level network capture** — Full network capture support for CDPPage. ([#815](https://github.com/jackwener/opencli/issues/815), [#816](https://github.com/jackwener/opencli/issues/816))
* **twitter:** relax reply composer timeout, use composer for text replies ([#862](https://github.com/jackwener/opencli/issues/862), [#860](https://github.com/jackwener/opencli/issues/860))
* **gemini:** stabilize ask reply state handling ([#735](https://github.com/jackwener/opencli/issues/735))
* **douban:** fix marks pagination and improve subject data extraction ([#752](https://github.com/jackwener/opencli/issues/752))
* **jianyu:** avoid early API bucket cutoff, stabilize search ([#916](https://github.com/jackwener/opencli/issues/916), [#912](https://github.com/jackwener/opencli/issues/912))
* **xiaoe:** resolve missing episodes for long courses via auto-scroll ([#904](https://github.com/jackwener/opencli/issues/904))
### Refactoring
* **adapters:** convert adapter layer from TypeScript to JavaScript ([#928](https://github.com/jackwener/opencli/issues/928))
* **adapters:** migrate all CLI adapters from YAML to TypeScript, then to JS ([#887](https://github.com/jackwener/opencli/issues/887), [#922](https://github.com/jackwener/opencli/issues/922))
* **validate:** switch from YAML-file scanning to registry-based validation ([#943](https://github.com/jackwener/opencli/issues/943))
* **strategy:** normalize strategy into runtime fields at registration time ([#941](https://github.com/jackwener/opencli/issues/941))
* **errors:** unify error output as YAML envelope to stderr ([#923](https://github.com/jackwener/opencli/issues/923))
* **daemon:** make daemon persistent, remove idle timeout ([#913](https://github.com/jackwener/opencli/issues/913))
* fix stale `.ts` references across skills and docs ([#954](https://github.com/jackwener/opencli/issues/954))
* unify skill command references and merge opencli-generate into opencli-explorer ([#891](https://github.com/jackwener/opencli/issues/891), [#894](https://github.com/jackwener/opencli/issues/894))
### Upgrade Guide
1.**Update Node.js** to v21 or later (v22 LTS recommended).
2.**Run `npm install -g @jackwener/opencli@latest`** — the preuninstall hook gracefully stops the old daemon; the first browser command after upgrade auto-restarts it.
3.**If you have custom `.ts` adapters** in `~/.opencli/clis/`, rename or compile them to `.js`. A warning will be printed on startup if stale `.ts` files are detected.
4.**If you have custom `.yaml` adapters**, convert them to JS using the `cli()` API (see `skills/opencli-adapter-author/references/adapter-template.md`).
5.**If you parse error output from stdout**, switch to stderr. Errors are now structured YAML envelopes with typed exit codes.
# 5. Link globally (optional, for testing `opencli` command)
npm link
@@ -26,54 +25,48 @@ npm link
## Adding a New Site Adapter
This is the most common type of contribution. Start with YAML when possible, and use TypeScript only when you need browser-side logic or multi-step flows.
All adapters use TypeScript. Use the pipeline API for data-fetching commands, and `func()` for complex browser interactions.
### YAML Adapter (Recommended for data-fetching commands)
### Pipeline Adapter (Recommended for data-fetching commands)
Create a file like `src/clis/<site>/<command>.yaml`:
```yaml
site:mysite
name:trending
description:Trending posts on MySite
domain:www.mysite.com
strategy:public # public | cookie | header
browser:false# true if browser session is needed
args:
query:
positional:true
type:str
required:true
description:Search keyword
limit:
type:int
default:20
description:Number of items
pipeline:
- fetch:
url:https://api.mysite.com/trending
- map:
rank:${{ index + 1 }}
title:${{ item.title }}
score:${{ item.score }}
url:${{ item.url }}
- limit:${{ args.limit }}
columns:[rank, title, score, url]
```
See [`hackernews/top.yaml`](src/clis/hackernews/top.yaml) for a real example.
Use `opencli explore <url>` to discover APIs and see [opencli-explorer skill](./skills/opencli-explorer/SKILL.md) if you need the full adapter workflow.
Install the [`opencli-adapter-author` skill](./skills/opencli-adapter-author/SKILL.md) if you need the full adapter workflow — recon → API discovery → field decoding → `opencli browser verify`.
### Validate Your Adapter
```bash
# Validate YAML syntax and schema
# Validate adapter
opencli validate
# Test your command
@@ -137,16 +130,12 @@ Use **positional** for the primary, required argument of a command (the "what"
Do **not** convert an argument to positional just because it appears first in the file. If the argument is optional, acts like a filter, or selects a mode/configuration, it should usually stay a named option.
YAML example:
```yaml
args:
query:
positional:true# ← primary arg, user types it directly
A CLI tool that turns **any website**, **Electron app**, or **local CLI tool** into a command-line interface — Bilibili, Zhihu, 小红书, Twitter/X, Reddit, YouTube, Antigravity, `gh`, `docker`, and [many more](#built-in-commands) — powered by browser session reuse and AI-native discovery.
OpenCLI gives you one surface for three different kinds of automation:
**Built for AI Agents** — Load the [`opencli-operate` skill](./skills/opencli-operate/SKILL.md) to give any AI agent (Claude Code, Cursor) direct browser control. Operate any website, then crystallize those interactions into reusable CLI commands. Configure `opencli list` in your `AGENT.md` or `.cursorrules` so the AI auto-discovers all available tools.
- **Use built-in adapters** for sites like Bilibili, Zhihu, Xiaohongshu, Reddit, HackerNews, Twitter/X, and [many more](#built-in-commands).
- **Let AI Agents operate any website** — install the `opencli-adapter-author` skill in your AI agent (Claude Code, Cursor, etc.), and it can navigate, click, type/fill, extract, and inspect any page through your logged-in browser via `opencli browser` primitives.
- **Write new adapters** end-to-end with `opencli browser` + the `opencli-adapter-author` skill, which guides from first recon through field decoding, code, and `opencli browser verify`.
**CLI Hub**— Register any local CLI (`opencli register mycli`) so AI agents can discover and call it alongside built-in commands. Auto-installs missing tools via your package manager (e.g. if `gh` isn't installed, `opencli gh ...` runs `brew install gh` first then re-executes seamlessly).
**CLI for Electron Apps** — Turn any Electron application into a CLI tool. Recombine, script, and extend apps like Antigravity Ultra from the terminal. AI agents can now control other AI apps natively.
---
It also works as a **CLI hub**for local tools such as `gh`, `docker`, `tg`, `discord`, `wx`, `ntn` (Notion), and other binaries you register yourself, plus **desktop app adapters** for Electron apps like Cursor, Codex, Antigravity, and ChatGPT.
## Highlights
- **CLI All Electron** — CLI-ify apps like Antigravity Ultra! Now AI can control itself natively.
- **Browser Automation** — `operate` gives AI agents direct browser control: click, type, extract, screenshot — any interaction, fully scriptable.
- **Website → CLI** — Turn any website into a deterministic CLI: 70+ pre-built adapters, or crystallize your own with `opencli record`.
- **Desktop App Control** — Drive Electron apps (Cursor, Codex, ChatGPT, etc.) directly from the terminal via CDP.
- **Browser Automation for AI Agents** — Install the `opencli-adapter-author` skill, and your AI agent can operate any website: navigate, click, type/fill, extract, screenshot — all through your logged-in Chrome session.
- **Multi-profile Browser Bridge** — Install the extension in each Chrome profile you want to use, then route commands with `--profile`, `OPENCLI_PROFILE`, or `opencli profile use`.
- **Website → CLI** — Turn any website into a deterministic CLI: 100+ site surfaces are already registered, or write your own with the `opencli-adapter-author` skill + `opencli browser verify`.
- **Account-safe** — Reuses Chrome/Chromium logged-in state; your credentials never leave the browser.
- **Anti-detection built-in** — Patches `navigator.webdriver`, stubs `window.chrome`, fakes plugin lists, cleans ChromeDriver/Playwright globals, and strips CDP frames from Error stack traces. Extensive anti-fingerprinting and risk-control evasion measures baked in at every layer.
- **External CLI Hub** — Discover, auto-install, and passthrough commands to any external CLI (gh, obsidian, docker, etc). Zero setup.
- **Self-healing setup** — `opencli doctor` diagnoses and auto-starts the daemon, extension, and live browser connectivity.
- **Dynamic Loader** — Simply drop `.ts` or `.yaml` adapters into the `clis/` folder for auto-registration.
- **AI Agent ready** — One skill takes you from site recon through API discovery, field decoding, adapter writing, and verification.
- **CLI Hub** — Discover, auto-install, and passthrough commands to any external CLI (gh, docker, obsidian, tg, discord, wx, etc).
- **Zero LLM cost** — No tokens consumed at runtime. Run 10,000 times and pay nothing.
- **Deterministic** — Same command, same output schema, every time. Pipeable, scriptable, CI-friendly.
- **Broad coverage** — 73+ sites across global and Chinese platforms (Bilibili, Zhihu, Xiaohongshu, Reddit, HackerNews, and more), plus desktop Electron apps via CDP.
---
## Quick Start
### 1. Install Browser Bridge Extension
### 1. Install OpenCLI
> OpenCLI connects to your browser through a lightweight **Browser Bridge** Chrome/Chromium extension + micro-daemon (zero config, auto-start).
OpenCLI requires **Node.js >= 21**.
1. Go to the GitHub [Releases page](https://github.com/jackwener/opencli/releases) and download the latest `opencli-extension.zip`.
2. Unzip the file and open `chrome://extensions`, enable **Developer mode** (top-right toggle).
```bash
node --version
npm install -g @jackwener/opencli
```
### 2. Install the Browser Bridge Extension
OpenCLI connects to Chrome/Chromium through a lightweight Browser Bridge extension plus a small local daemon. The daemon auto-starts when needed.
**Option A — Chrome Web Store (recommended):**
Install **OpenCLI** from the [Chrome Web Store](https://chromewebstore.google.com/detail/opencli/ildkmabpimmkaediidaifkhjpohdnifk).
**Option B — Manual install:**
1. Download the latest `opencli-extension-v{version}.zip` from the GitHub [Releases page](https://github.com/jackwener/opencli/releases).
2. Unzip it, open `chrome://extensions`, and enable **Developer mode**.
3. Click **Load unpacked** and select the unzipped folder.
### 2. Install OpenCLI
**Install via npm (recommended)**
### 3. Verify the setup
```bash
npm install -g @jackwener/opencli
opencli doctor
```
# Install AI skills for Claude Code / Cursor
### 4. Optional: name your Chrome profile
Each Chrome profile runs its own OpenCLI extension instance. If you use multiple Chrome profiles, list the connected profiles and assign local aliases:
```bash
opencli profile list
opencli profile rename <contextId> work
opencli profile use work
opencli --profile work browser state
```
With only one connected profile, OpenCLI uses it automatically. With multiple connected profiles and no default, OpenCLI asks you to choose instead of guessing.
### 5. Run your first commands
```bash
opencli list
opencli hackernews top --limit 5
opencli bilibili hot --limit 5
```
## For Humans
Use OpenCLI directly when you want a reliable command instead of a live browser session:
-`opencli list` shows every registered command.
-`opencli <site> <command>` runs a built-in or generated adapter.
-`opencli external register mycli` exposes a local CLI through the same discovery surface.
If you want to add your own commands, start with the [Extending OpenCLI guide](./docs/guide/extending-opencli.md). README keeps this short; the guide covers the directory layout, source-control model, and install commands.
| Need | Recommended path |
|------|------------------|
| Keep personal website commands in your own Git repo | `opencli plugin create` + `opencli plugin install file://...` |
| Quickly draft a private local adapter | `opencli browser init <site>/<command>` in `~/.opencli/clis/` |
| Modify an official adapter locally | `opencli adapter eject <site>` + `opencli adapter reset <site>` |
| Wrap an existing local binary | `opencli external register <name>` |
## For AI Agents
OpenCLI's browser commands are designed to be used by AI Agents — not run manually. Install skills into your AI agent (Claude Code, Cursor, etc.), and the agent operates websites on your behalf using your logged-in Chrome session.
### Install skills
```bash
npx skills add jackwener/opencli
```
### 3. Verify & Try
Or install only what you need:
```bash
opencli doctor # Check extension + daemon connectivity
opencli daemon status # Check daemon state (PID, uptime, memory)
| **opencli-adapter-author** | Operate a site in real time, or write a reusable adapter for a new site | "Help me check my Xiaohongshu notifications" / "Write an adapter for douyin trending" / "Make a command that grabs the top posts from this page" |
| **opencli-autofix** | Repair a broken adapter when a built-in command fails | "`opencli zhihu hot` is returning empty — fix it" |
| **opencli-browser** | Browser automation reference for AI agents | "Use browser commands to scrape this page" |
| **opencli-usage** | Quick reference for all OpenCLI commands and sites | "What commands does OpenCLI have for Twitter?" |
| **smart-search** | Search across existing OpenCLI capabilities | "Find me a Bilibili trending adapter" |
### 4. Browser Automation — Make Websites Accessible for AI Agents
### How it works
Point your AI agent (Claude Code, Cursor) to [`skills/opencli-operate/SKILL.md`](./skills/opencli-operate/SKILL.md). It has everything needed — full command reference, examples, and workflow.
Once `opencli-adapter-author` is installed, your AI agent can:
OpenCLI provides [skills](./skills/) for AI agents (Claude Code, etc.):
`opencli browser` commands require a `<session>` positional immediately after `browser`. `opencli browser work open <url>` and `opencli browser work tab new [url]` both return a target ID. Use `opencli browser work tab list` to inspect target IDs, then pass `--tab <targetId>` to route a command to a specific tab. `tab new` creates a new tab without changing the default browser target; only `tab select <targetId>` promotes that tab to the default target for later untargeted commands in the same session.
`opencli browser` commands are the low-level primitives that AI Agents use to operate websites. You don't run these manually — instead, install the `opencli-adapter-author` skill into your AI agent, describe what you want in natural language, and the agent handles the browser operations.
### For Developers
For example, tell your agent: *"Help me check my Xiaohongshu notifications"* — the agent will use `opencli browser <session> open`, `state`, `click`, etc. under the hood.
**Install from source**
### Built-in adapters: stable commands
```bash
git clone git@github.com:jackwener/opencli.git &&cd opencli && npm install && npm run build && npm link
```
Use site-specific commands such as `opencli hackernews top` or `opencli reddit hot` when the capability already exists. These are deterministic and work without browser — ideal for both humans and AI agents.
**Load Source Browser Bridge Extension**
### Writing a new adapter
1. Open `chrome://extensions` and enable **Developer mode** (top-right toggle).
2. Click **Load unpacked** and select the `extension/` directory from this repository.
When the site you need is not yet covered, use the `opencli-adapter-author` skill. It takes the agent end-to-end:
---
1. Recon the site and classify its pattern (SPA / SSR / JSONP / Token / Streaming).
2. Discover the right endpoint — network inspection, initial state, bundle search, token trace, or interceptor fallback.
6. Persist site knowledge to `~/.opencli/sites/<site>/` so the next adapter for the same site is faster.
### CLI Hub and desktop adapters
OpenCLI is not only for websites. It can also:
- expose local binaries like `gh`, `docker`, `obsidian`, `tg`, `discord`, `wx`, or custom tools through `opencli <tool> ...`
- control Electron desktop apps through dedicated adapters and CDP-backed integrations
## Prerequisites
- **Node.js**: >= 20.0.0 (or **Bun** >= 1.0)
- **Chrome or Chromium** running **and logged into the target site** (e.g. bilibili.com, zhihu.com, xiaohongshu.com, goofish.com).
- **Node.js**: >= 21.0.0 (required for the standard npm install path)
- **Bun**: >= 1.0 (optional alternative runtime)
- **Chrome or Chromium** running and logged into the target site for browser-backed commands
> **⚠️ Important**: Browser commands reuse your Chrome/Chromium login session. You must be logged into the target website in Chrome or Chromium before running commands. If you get empty data or errors, check your login status first.
> **Important**: Browser-backed commands reuse your Chrome/Chromium login session. If you get empty data or permission-like failures, first confirm the site is already open and authenticated in Chrome/Chromium.
## Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `OPENCLI_DAEMON_PORT` | `19825` | HTTP port for the daemon-extension bridge |
| `OPENCLI_PROFILE` | — | Browser Bridge profile alias/contextId to use when multiple Chrome profiles are connected |
| `OPENCLI_WINDOW` | command default | Set to `foreground` or `background` to override Browser Bridge window placement. Browser-backed commands also accept `--window <foreground\|background>`. |
| `OPENCLI_BROWSER_CONNECT_TIMEOUT` | `30` | Seconds to wait for browser connection |
| `OPENCLI_BROWSER_COMMAND_TIMEOUT` | `60` | Seconds to wait for a single browser command |
| `OPENCLI_CDP_ENDPOINT` | — | Chrome DevTools Protocol endpoint for remote browser or Electron apps |
| `OPENCLI_VERBOSE` | `false` | Enable verbose logging (`-v` flag also works) |
| `DEBUG_SNAPSHOT` | — | Set to `1` for DOM snapshot debug output |
`opencli browser *` requires an explicit `<session>` positional, uses a foreground browser window by default, and keeps that session's tab lease until `opencli browser <session> close` or idle cleanup. Browser-backed adapters use a background adapter window and release one-shot tab leases by default. Interactive adapters can declare `siteSession: 'persistent'` to keep a stable site tab for continuity; pass `--site-session ephemeral` for a one-shot tab.
## Update
```bash
npm install -g @jackwener/opencli@latest
# If you use the packaged OpenCLI skills, refresh them too
73+ adapters in total — **[→ see all supported sites & commands](./docs/adapters/index.md)**
100+ site surfaces in total — **[→ see all supported sites & commands](./docs/adapters/index.md)**
`*``opencli xiaoyuzhou podcast`, `podcast-episodes`, `episode`, `download`, and `transcript` require local Xiaoyuzhou credentials in `~/.opencli/xiaoyuzhou.json`.
## CLI Hub
OpenCLI acts as a universal hub for your existing command-line tools — unified discovery, pure passthrough execution, and auto-install (if a tool isn't installed, OpenCLI runs `brew install <tool>` automatically before re-running the command).
OpenCLI acts as a universal hub for your existing command-line tools — unified discovery, pure passthrough execution, and auto-install when a safe package-manager command is configured.
| **dingtalk** | DingTalk — cross-platform CLI for DingTalk's full suite, designed for humans and AI agents | `opencli dingtalk msg send --to user "hello"` |
| **wecom** | WeCom/企业微信 — CLI for WeCom open platform, for humans and AI agents | `opencli wecom msg send --to user "hello"` |
| **dws** | DingTalk — cross-platform CLI for DingTalk's full suite, designed for humans and AI agents | `opencli dws msg send --to user "hello"` |
| **wecom-cli** | WeCom/企业微信 — CLI for WeCom open platform, for humans and AI agents | `opencli wecom-cli msg send --to user "hello"` |
| **tg(tg-cli)** | Telegram — local-first sync, search, and export via MTProto for AI agents | `opencli tg search "AI news" -f json` |
| **discord(discord-cli)** | Discord — local-first sync, search, and export via SQLite for AI agents | `opencli discord recent --channel general` |
**Register your own** — add any local CLI so AI agents can discover it via `opencli list`:
```bash
opencli register mycli
opencli external register mycli
```
**Manual install** — some external CLIs use official shell-script installers rather than shell-free package-manager commands. For `ntn`, install from <https://ntn.dev> first, then run `opencli ntn ...`.
### Desktop App Adapters
Control Electron desktop apps directly from the terminal. Each adapter has its own detailed documentation:
@@ -172,9 +316,8 @@ Control Electron desktop apps directly from the terminal. Each adapter has its o
| **Cursor** | Control Cursor IDE — Composer, chat, code extraction | [Doc](./docs/adapters/desktop/cursor.md) |
See [Plugins Guide](./docs/guide/plugins.md) for creating your own plugin.
## For AI Agents (Developer Guide)
> **Quick mode**: To generate a single command for a specific page URL, see [opencli-oneshot skill](./skills/opencli-oneshot/SKILL.md) — just a URL + one-line goal, 4 steps done.
Before writing any adapter code, read the [`opencli-adapter-author` skill](./skills/opencli-adapter-author/SKILL.md). It takes you end-to-end:
> **Full mode**: Before writing any adapter code, read [opencli-explorer skill](./skills/opencli-explorer/SKILL.md). It contains the complete browser exploration workflow, the 5-tier authentication strategy decision tree, and debugging guide.
- Recon the site and pick a pattern (SPA / SSR / JSONP / Token / Streaming).
- Discover the right endpoint via `opencli browser <session> network`, `eval`, or the interceptor fallback.
opencli cascade https://api.example.com/data # Auto-probe: PUBLIC → COOKIE → HEADER
```
For long-lived personal commands that should live in your own Git repo, use a local plugin instead; see [Extending OpenCLI](./docs/guide/extending-opencli.md). Quick private adapters can still live at `~/.opencli/clis/<site>/<name>.js`. Site knowledge (endpoints, field maps, fixtures) accumulates in `~/.opencli/sites/<site>/` so the next adapter for the same site starts from context instead of zero.
## Testing
@@ -273,10 +425,10 @@ See **[TESTING.md](./TESTING.md)** for how to run and write tests.
## Troubleshooting
- **"Extension not connected"** — Ensure the Browser Bridge extension is installed and **enabled** in `chrome://extensions` in Chrome or Chromium.
- **"Extension not connected"** — Ensure the Browser Bridge extension is installed from the [Chrome Web Store](https://chromewebstore.google.com/detail/opencli/ildkmabpimmkaediidaifkhjpohdnifk) and **enabled** in `chrome://extensions`.
- **"attach failed: Cannot access a chrome-extension:// URL"** — Another extension may be interfering. Try disabling other extensions temporarily.
- **Empty data or 'Unauthorized' error** — Your Chrome/Chromium login session may have expired. Navigate to the target site and log in again.
- **Node API errors** — Ensure Node.js >= 20. Some dependencies require modern Node APIs.
- **Node API errors / missing `fetch` / startup crash on old Node** — OpenCLI requires **Node.js >= 21**. Run `node --version`, upgrade Node if needed, then retry.
"opencli operate open https://en.wikipedia.org/wiki/JavaScript",
"opencli operate eval \"JSON.stringify([...document.querySelectorAll('.toc li a, #toc li a, .vector-toc-contents a')].slice(0,8).map(a=>a.textContent?.trim()))\""
"opencli browser open https://en.wikipedia.org/wiki/JavaScript",
"opencli browser eval \"JSON.stringify([...document.querySelectorAll('.toc li a, #toc li a, .vector-toc-contents a')].slice(0,8).map(a=>a.textContent?.trim()))\""
],
"judge":{
"type":"arrayMinLength",
@@ -509,9 +509,9 @@
{
"name":"complex-books-detail",
"steps":[
"opencli operate open https://books.toscrape.com",
* Optimizes the "Save as CLI" pipeline: operate init → write adapter → run.
* Optimizes the "Save as CLI" pipeline: browser init → write adapter → run.
* Covers PUBLIC (no auth) and COOKIE (browser session) strategies.
* Metric: number of passing save-tasks.
*/
@@ -9,12 +9,12 @@
importtype{AutoResearchConfig}from'../config.js';
exportconstsaveReliability: AutoResearchConfig={
goal:'Increase "Save as CLI" pipeline pass rate to 100%. The flow is: operate init creates a scaffold, user writes adapter code, opencli discovers and runs it. Covers both PUBLIC (fetch API) and COOKIE (browser session) strategies. Focus on: init template correctness, user CLI discovery, adapter loading, verify command robustness, and browser session handling.',
goal:'Increase "Save as CLI" pipeline pass rate to 100%. The flow is: browser init creates a scaffold, user writes adapter code, opencli discovers and runs it. Covers both PUBLIC (fetch API) and COOKIE (browser session) strategies. Focus on: init template correctness, user CLI discovery, adapter loading, verify command robustness, and browser session handling.',
A 30-second morning routine that surfaces what changed overnight in reinforcement-learning and large-model research, without opening a browser.
## What I wanted
Before reading anything, decide where to spend my 20 minutes of paper time:
- which `cs.LG` and `cs.AI` papers landed in the last 24 hours
- which OpenReview submissions at recent venues (NeurIPS 2025 right now, ICLR 2024 / NeurIPS 2024 as historical reference) carry titles and primary areas relevant to my work
- which papers the Hugging Face Daily Papers community is talking about today
Skim signals, then drill in. The point is to filter, not to read everything.
## Commands
```bash
# 1. arxiv recent in the two relevant categories (newest 30 each)
That is the entire collection step. The four files together are the whole signal surface for one morning.
## What I do with the output
Pipe the four JSON files into a one-shot LLM digest with a fixed prompt:
```
Here are four JSON arrays of papers from the last 24 hours.
Group them into:
1. Direct hits on RLHF / preference optimization / reasoning RL.
2. Adjacent (offline RL, world models, agent benchmarks).
3. Notable infra (training, evaluation, data).
For each, give me title + arxiv id + one-sentence why-it-matters.
Skip everything that is review / survey / position paper.
```
The LLM compresses ~120 entries into a 10-line shortlist in seconds. I then open whichever 2 to 3 papers actually clear the bar.
## Why CLI beats the browser version
- Four pages of clicking and scrolling collapses into four `opencli` calls.
- The output is structured JSON, so the digest prompt can reason about it deterministically. No copy-paste, no "I missed paper 14".
- Works inside any agent loop. A scheduled task can run the four commands, push them to an LLM, and message the digest somewhere. No browser kept open.
- Zero token cost on the OpenCLI side. The only paid step is the digest call at the end.
The arxiv adapter's `recent <category>` (added in #1289) is the lever here. Without it I would have to fall back to the arxiv listings page, which means scraping HTML in agent code instead of consuming a structured listing.
# Find a paper's implementation and follow-up work
Given a single paper title or arxiv id, walk three sources in one chain to find the canonical reference, follow-up citations, and any community-fine-tuned models or Spaces that already build on it.
## What I wanted
I read a paper abstract, decide it is interesting, and want to answer three questions before deciding to actually re-read the paper or reproduce it:
1. Has anyone already implemented or fine-tuned on top of it (Hugging Face)?
2. Who has cited or extended it (dblp / OpenReview)?
3. What is the canonical bibliographic record (dblp key for citation, full arxiv metadata for reading)?
Doing this in a browser means three tabs and two minutes of context-switching. The point is to compress that into one shell pipeline.
## Commands
Worked example: "Direct Preference Optimization" (DPO).
```bash
# 1. Canonical arxiv record (full abstract, authors, pdf url, categories).
# Note: arxiv free-text search ranks by recency, so the original DPO
# paper does not always come back first. When the canonical id is
Three of the four are public-strategy adapters, no browser session needed. The OpenReview call also lands without auth for public venues.
## What I do with the output
For DPO the chain produces:
- arxiv record: paper id `2305.18290`, full abstract, pdf link.
- dblp record: canonical key `conf/nips/RafailovSMMEF23`, NeurIPS 2023, co-author list (useful to find related work by same lab).
- HF Daily Papers (last 30 days): every paper whose title mentions DPO or preference. Each one is a candidate "follow-up work I should know about".
- OpenReview: the original submission's review thread, if posted (lets me see what reviewers actually pushed back on, which is more useful than the published abstract).
I dump all four JSON outputs into a single LLM call with the prompt: *"Build a one-paragraph 'state of the field' summary for this paper as of today. Cite each follow-up by arxiv id."* That gives me a research-debt brief in 30 seconds.
## Why this is worth a CLI chain
- Each adapter alone is just "search a website". The value is the chain. Four `opencli` calls feed into one LLM call. No browser, no copy-paste.
- Output is identifier-rich (arxiv id, dblp key, venue id, HF paper id). I can re-feed any of those into the next call, e.g. once I find a follow-up arxiv id from HF Daily Papers I run `opencli arxiv paper <new-id>` immediately.
- Survives use inside an agent loop. Same chain runs unattended for a batch of 20 papers from a reading list.
- Zero token cost for the discovery half. Only the final summary step pays for inference.
Without `opencli dblp search` (added in #1299) and `opencli openreview search` (added in #1294), this whole pipeline used to require either web scraping in agent code or paying for a research-paper API. Both adapters being public-strategy means they slot in cleanly.
# Track a conference's accepted papers and reviews from the terminal
Once an OpenReview venue opens its decisions (or releases reviews publicly during the discussion phase), I want a one-shot way to pull the full venue listing and dive into individual review threads, without clicking through 200+ submission pages.
## What I wanted
For each major venue I follow (ICLR, NeurIPS, ICML), the same three things every time decisions are visible:
1. The full list of accepted papers at the venue, with titles and forum ids.
2. For any paper I flagged interesting from the list: the full review thread, including reviewer scores, rebuttals, and the AC's decision rationale.
3. A way to pipe both into LLM-driven shortlisting ("which of these 100 oral papers actually intersect with my research direction").
The OpenReview UI is fine for one paper at a time, but unusable for batch reasoning across the whole acceptance list.
## Commands
Worked example: ICLR 2024 oral track, then drill into one paper's reviews using a real forum id.
```bash
# 1. Full list of papers at a venue (natural-language venue text;
# if the venue is not yet open OpenReview returns EMPTY_RESULT
`venue` returns each entry with a forum id you can hand straight back into `reviews` and `paper`. No id lookup gymnastics. `reviews` returns the full thread as a JSON array: a `PAPER` row with the abstract, then one `REVIEW` row per reviewer (with `rating`, `confidence`, summary, weaknesses, questions), followed by author rebuttals and the AC's decision rationale.
## What I do with the output
Two distinct workflows depending on the phase of the venue:
### Phase A: filtering the acceptance list
After `venue` returns 200 entries, dump the JSON into an LLM with the prompt:
```
Here is the full acceptance list at <venue>. Filter to papers that intersect
with my research interests:
- reinforcement learning from preference / reward feedback
- reasoning training (process reward, RLVR, RLHF variants)
- long-horizon agent benchmarks
For each match: title + forum_id + one-sentence why-it-matters.
```
This collapses 200 papers to a 10-paper shortlist in seconds. The forum ids are the keys I will use in Phase B.
### Phase B: depth-reading the shortlist
For each shortlisted forum id, run `opencli openreview reviews <forum-id>` and feed the JSON to an LLM with the prompt:
```
Summarize the review thread:
- reviewer scores
- the strongest critique
- whether the rebuttal addressed it
- final decision and AC rationale
```
This is faster than reading three reviews + rebuttal + meta-review per paper. For 10 papers this turns 60 minutes of OpenReview clicking into 10 minutes of summary reading, then I open the actual reviews only for papers where the summary flagged something worth knowing.
## Why this beats opening OpenReview
- One `venue` call replaces scrolling a paginated UI for 200+ papers.
-`reviews` returns the entire thread as JSON, so an LLM can reason over the whole review-rebuttal-decision arc at once. The web view forces you to scroll three reviews + N rebuttals + meta separately.
- Forum ids returned from `venue` are stable and reusable across calls. Easy to keep a personal reading list as `forum-ids.txt` and run `for id in $(cat forum-ids.txt); do opencli openreview reviews $id; done`.
- The whole loop is public-strategy. No login required for venues with public reviewing.
`opencli openreview` (added in #1294) is the lever. Before this adapter existed, the same workflow needed either OpenReview's Python client or HTML scraping inside agent code. Both have higher friction than `opencli openreview reviews <forum-id>` returning structured JSON in one shot.
thrownewCommandExecutionError('1688 search page did not return a readable payload','Open the same query in Chrome and verify the page is fully loaded before retrying.');
thrownewEmptyResultError('1688 search','No visible results were extracted. Retry with a different query or open the same search page in Chrome first.');
if(message.includes('Inspected target navigated or closed')
||message.includes('Cannot find context with specified id')
||message.includes('Target closed')){
thrownewCommandExecutionError(`1688 ${action} navigation lost the current browser target`,`${buildCaptchaHint(action)} If CDP is attached to a stale or blocked tab, open a fresh 1688 tab and point OPENCLI_CDP_TARGET at that tab.`);
expect(result.list_title).toBe('Amazon Best Sellers: Best Desktop & Off-Surface Shelves');
});
});
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.