* 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.