* docs: add dingtalk and wecom CLI to external CLI hub
Add dingtalk-workspace-cli and wecom-cli as external CLI integrations
alongside lark-cli, gh, docker, etc.
* feat: add confirmPrompt() to TUI module
* feat: add Electron app registry with builtin + user-defined apps
* feat: add Electron app launcher with auto-detect and restart
* fix: launcher uses processName for path discovery, platform-guard tests
* feat: integrate Electron auto-launcher into execution pipeline
- CDPBridge.connect() accepts cdpEndpoint parameter instead of requiring env var
- getBrowserFactory() selects CDPBridge for registered Electron apps by site name
- executeCommand() calls resolveElectronEndpoint() for Electron apps, skips daemon check
- Remove requiredEnv/OPENCLI_CDP_ENDPOINT from all chatwise commands
- Remove chatwise-opencli.ps1 wrapper script and chatwise/shared.ts
- Update antigravity/serve.ts to use launcher instead of manual env var
- Replace hardcoded app names in scoreCDPTarget with registry lookup
- Fix Discord bundleId typo (com.iscord.app → com.discord.app)
* fix: resolve review issues — port collision and registry completeness
- Change ChatGPT CDP port from 9224 to 9236 (was colliding with Antigravity)
- scoreCDPTarget now uses full registry (builtin + user-defined) via getAllElectronApps()
- Use displayName (falling back to processName) for target score boosting
* fix: assign unique CDP ports — antigravity 9234, chatgpt 9236
Both were sharing port 9224, which could cause silent mis-connection.
- Add content field to display topic body text
- Add member field to show topic author
- Add created field to show topic creation timestamp
- Add node field to show topic category
- Add id field for consistency with hot/latest commands
This makes v2ex topic command return meaningful details that are
not available in hot/latest listings.
* feat(youtube): add --type shorts/video/channel, --upload, --sort filters
Uses YouTube's native sp= filter params. Shorts = type 9 (sp=EgIQCQ).
Also parses reelItemRenderer for Shorts results.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(youtube): add published time to search results
Shows when video was uploaded (e.g. "8h ago", "4d ago", "3mo ago").
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(youtube): prevent duplicate sp= params and remove redundant Shorts URL rewrite
- YouTube only supports one sp= parameter; using multiple causes
unpredictable behavior. Pick the most specific filter with priority:
type > upload > sort.
- Remove the post-processing Shorts URL rewrite — the reelItemRenderer
branch already generates /shorts/ URLs directly.
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(bilibili): distinguish login-gated subtitles from empty results
* fix(test): use single toSatisfy assertion instead of double rejects.toThrow
Awaiting the same rejected promise twice is unreliable. Combine the
AuthRequiredError type check and message regex into one toSatisfy call.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(v2ex): add id field to hot and latest API responses
- Add id field to hot.yaml and latest.yaml pipeline output
- Enables downstream commands like 'v2ex topic <id>' to work seamlessly
- Fixes issue where v2ex hot/latest JSON output lacked topic IDs
* enhance(v2ex): add node and url fields to hot/latest output
In addition to the id field, include node name (板块) and topic URL
for richer output. All fields come from the existing API response.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(facebook): split search navigation from extraction
* refactor: use settleMs instead of waitUntil:none + wait:4
Replace `waitUntil: none` + separate `wait: 4` step with `settleMs: 4000`
on the navigate step. This is consistent with other Facebook adapters
(feed.yaml, memories.yaml, profile.yaml) and lets the navigate step
handle the timing in one place.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
VitePress base is /docs/, so absolute links like /adapters/browser/twitter
resolve incorrectly. Changed all links to relative paths (./browser/...,
./desktop/...) so they work correctly on the docs site.
* fix(substack): update selectors for Substack DOM redesign (fixes#621)
Substack replaced <article> elements with role="article" divs and a
new SPA-based feed. The wait() selector 'article' no longer matches,
causing 'Selector not found: article' on feed and publication commands.
- loadSubstackFeed: use 'a[href*="/p/"]' (matches actual post links)
- loadSubstackArchive: use '[role="article"]' (Substack's new ARIA roles)
The evaluate() scraping logic inside both functions is unchanged since
it already uses 'a' href pattern matching, not article tags.
* review: align substack wait selectors with scraper
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(ctrip): update search adapter to live endpoint
* review: make ctrip search a public fetch adapter
---------
Co-authored-by: 泽加武 <zejiawu@zejiawudeMac-mini.local>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(xiaohongshu): support full URL/short link and fix video extraction
Two issues fixed:
1. URL handling: The download command only accepted bare note IDs and
constructed `explore/{noteId}` URLs, which lack the `xsec_token`
parameter now required by Xiaohongshu. This made all video/image
downloads fail with "No media found". Now accepts full URLs
(with xsec_token) and short links (xhslink.com) in addition to
bare note IDs.
2. Video extraction: XHS video player uses blob: URLs in DOM, which
cannot be downloaded via HTTP. Now extracts real video URLs from
`window.__INITIAL_STATE__` (SSR data) and inline script JSON
before falling back to DOM selectors, skipping blob: URLs.
Tested with a video note via short link — successfully downloaded
21.5 MB MP4.
* review: resolve xiaohongshu note id after redirects
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(xiaohongshu): add note command and nested reply support for comments
Add `xiaohongshu note` command to read full note content (title, author,
description, engagement metrics, tags) from public note pages.
Enhance `xiaohongshu comments` with `--with-replies` flag to extract
nested replies (楼中楼), including reply_to attribution and per-reply
like counts. Limit logic counts only top-level comments so replies
are included for free.
Extract shared `parseNoteId` into side-effect-free `note-helpers.ts`
to avoid cross-module command registration leakage.
Normalize non-numeric engagement placeholders ("赞"/"收藏"/"评论")
to "0" for zero-count notes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(xiaohongshu): add note and comments --with-replies to adapter docs
Update xiaohongshu adapter documentation and README command table
to reflect the new note command and enhanced comments with nested
reply support.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(xiaohongshu): fix download example to show both note-id and url
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(xiaohongshu): expand nested reply threads before scraping
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
- Add early login-wall detection before autoScroll() in search.ts
to prevent crash when XHS shows a login gate instead of results
- Add document.body null guard in autoScrollJs (dom-helpers.ts)
- Update search.test.ts: verify autoScroll is not called on login wall
- Add autoScrollJs null-body defense test in dom-helpers.test.ts
* docs: add dingtalk and wecom CLI to external CLI hub
Add dingtalk-workspace-cli and wecom-cli as external CLI integrations
alongside lark-cli, gh, docker, etc.
* feat: register dingtalk and wecom as external CLIs
Add dws (DingTalk Workspace CLI) and wecom-cli to
external-clis.yaml so they are discoverable via opencli list
and auto-installable.
* feat(browser): add ONES adapter support for tasks and worklog commands
Add ONES auth/session commands, task listing/details utilities, and worklog operations, with related docs and helper utilities.
* fix(ones): harden worklog and task-list adapter behavior
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(tieba): add browser adapters for hot posts search and read
* fix(tieba): stabilize search and e2e coverage
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(doubao): add history, detail, meeting-summary and meeting-transcript commands
- history: list conversation history from sidebar
- detail: read a specific conversation by ID, with meeting card detection
- meeting-summary: extract summary and AI chapters from meeting minutes
- meeting-transcript: read or download meeting transcript via browser
Made-with: Cursor
* docs: update doubao command list in adapter index and README.zh-CN
Made-with: Cursor
* fix(doubao): handle meeting-only detail and merge transcript snapshots
* refactor(doubao): model conversation ids as first-class output
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(douyin): add user-videos command with top-10 comments
Adds a new adapter for fetching a public user's video list by sec_uid,
alongside the top-10 hottest comments for each video.
- Navigates to the user's profile page to establish a cookie session
- Fetches video list via /aweme/v1/web/aweme/post/
- Concurrently fetches top-10 comments per video via
/aweme/v1/web/comment/list/ (sorted by hotness, API default)
Output columns: index, aweme_id, title, duration, digg_count,
play_url, top_comments
* refactor(douyin): replace Object.assign with spread in user-videos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(douyin): validate user-videos inputs
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(xiaohongshu): add cover image URL to user notes output
Extract cover image URL from noteCard.cover.urlDefault in
__INITIAL_STATE__ and include it in the user command output columns.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(xiaohongshu): cover user note rows
* refactor(xiaohongshu): keep cover out of default columns
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(spotify): add Spotify playback adapter
Adds a new adapter for controlling Spotify via the official Web API.
Uses Strategy.PUBLIC with OAuth2 — no browser session required.
Commands: auth, status, play, pause, next, prev, volume, search, queue, shuffle, repeat.
Credentials are loaded from ~/.opencli/spotify.env or environment variables.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(spotify): rename index.ts → spotify.ts and fix CliError calls
- Renamed src/clis/spotify/index.ts to spotify.ts so the build-manifest
picks it up (index.js is intentionally excluded from manifest scanning)
- Fixed 4 CliError calls: constructor now requires (code, message, hint?)
so each throw now passes an appropriate error code as first argument
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(spotify): fix token refresh corruption, env parse, null guards, validation
- refreshAccessToken: check res.ok before parsing; construct Tokens object
directly instead of mutating loadTokens() result to avoid writing
undefined/NaN on Spotify error responses; preserve existing refresh_token
when Spotify omits it from the response
- loadEnv: split on first '=' only so values containing '=' are preserved
- SCOPES: remove write/library/top scopes not used by any command
- status: guard against data.item being null (active device but no track)
- volume: validate 0-100 range before API call
- auth: check tokenRes.ok on initial token exchange; add server.on('error')
handler for EADDRINUSE; add 5-minute timeout with clearTimeout on close
* feat(postinstall): auto-create ~/.opencli/spotify.env template on install
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(spotify): guard null progress, podcast items, missing tracks data, corrupted tokens, invalid search limit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(spotify): improve missing credentials error with step-by-step guidance
* fix(spotify): harden setup and add docs coverage
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(band): add bands, posts, and mentions commands for band.us
- bands: lists all Bands via get_band_list_with_filter intercept
- posts: lists posts from a Band via get_posts_and_announcements intercept
- mentions: shows @mention notifications via get_news intercept
All use Strategy.INTERCEPT since band.us API requires an HMAC md header
generated by its own JS. SPA navigation to /band/{no}/post triggers the
band list and posts APIs; bell + @メンション tab click triggers mentions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(band): clean up all three band adapters
- Fix doc comments: Band uses XHR not fetch; clarify INTERCEPT rationale
- bands: replace for-loop with flatMap; explain why band page nav is needed
- posts: remove item.post ?? item fallback (API always wraps in post); rename
finalRequests → requests for consistency; extract stripBandTags helper
- mentions: remove redundant ?? defaults (args have defaults defined); fix
unreadOnly bug (was not applied to post/comment modes); consolidate Band tag
stripping to single regex; cast kwargs types directly instead of converting;
add comments explaining last-response strategy and 'referred' filter flag
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(band/posts): handle mixed post/announcement items from API
get_posts_and_announcements returns both regular posts and announcements
that have different shapes — some lack post_no and wrap differently.
Restore item.post ?? item fallback and filter out items with no resolvable
identifier to prevent undefined in URLs and empty rows in output.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(band): add post command — full post export with comments and photo download
Exports the complete content of a single Band post:
- Post body (with Band markup tags stripped)
- All comments in chronological order
- Photo URLs shown inline, or downloaded with --output <dir>
Uses Strategy.INTERCEPT with a broad 'band.us' pattern to capture both the
batch request (embedding get_post) and get_comments in one SPA navigation.
Responses are identified client-side by shape: batch_result array vs items
array with comment_id fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(band): replace XHR interception with direct DOM extraction
- bands, posts, post: navigate directly to target URL instead of home→SPA detour
- All three switch from Strategy.INTERCEPT to Strategy.COOKIE with navigateBefore: false
(bands uses framework pre-nav to home; posts/post disable it and goto target directly)
- DOM extraction polls for specific content elements rather than fixed waits
- post: confirm selectors via browser inspection (a.text, time.time, .sCommentList,
.sReplyList for nested replies); add --comments flag to skip comment fetch
- posts: extract from rendered post list DOM; correct comment item selector (div.cComment)
- Fix: post empty-result guard changed from && to handle null data safely
- Fix: photo download now checks HTTP status code before piping to avoid writing
redirect HTML into image files
- Fix: mentions unread client-side filter skipped for 'mentioned' mode since
server already filtered via 未確認のみ button click
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(band): address code review feedback
- post: replace manual http/https download with shared downloadMedia utility
(handles redirects, timeouts, stream errors correctly)
- post: fix photo URL resolution to use location.href as base, handling
protocol-relative and relative URLs without throwing
- post: switch to node:-prefixed imports per repo convention
- post/posts: remove redundant ArgumentError guards — framework already
validates required args before func() is called
- mentions: INTERCEPT strategy is intentional (Band HMAC prevents DOM-only
approach for notifications; update PR description to clarify)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(band): address second round of code review feedback
- bands: tighten href selector to /band/{id}(?:/post)?$ so feed/post-detail
links are excluded; only sidebar navigation links match
- mentions: replace fixed page.wait(2) sleeps with polling on
getInterceptedRequests() — waits up to 8 s per action, exits as soon
as the expected number of captures arrives (avoids flakiness on slow XHR)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(band): fix selector bugs found during testing
- bands: use a.bandCover._link + p.uriText + span.member em selectors
(previous a[href*="/band/"] + .bandName combo leaked "メンバー" text)
- posts: use article.cContentsCard._postMainWrap + span.count selectors
(previous li._postListItem selector matched nothing; DOM changed)
- mentions: fix page.wait(500) → page.wait(0.5) (was waiting 500s not ms);
use timestamp-suffixed URL to force fresh page load each run so the
notification panel is closed; fix get_news vs get_news_count capture
ambiguity with result_data.news check; replace cumulative waitForCaptures
with waitForOneCapture (getInterceptedRequests clears array on each call)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(band/mentions): use CSS class selector for bell button instead of locale-dependent text match
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(band): address third round of code review feedback
- post: pass browser cookies to downloadMedia so Band's login-protected
photo URLs don't fail with 401/403
- post: include photos.length in empty-result guard so photo-only posts
are not falsely reported as not found
- mentions: accumulate captures across poll iterations so get_news_count
responses don't cause early exit before the real get_news arrives
- mentions: update docstring to match actual implementation (client-side
filtering, no tab-click)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(band): address fourth round of code review feedback
- mentions: fail fast with a clear error when bell button is not found,
instead of silently no-op and waiting 8s before EmptyResultError
- post: use shared formatCookieHeader() instead of manual cookie string
construction
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(band): address fifth round of code review feedback
- mentions: replace fixed page.wait(2) with polling for bell button
readiness (up to 10s), eliminating the fixed sleep and fail-fast
when the selector is missing
- mentions: add explicit !newsReq guard with a clear error message when
get_news capture times out, instead of falling through to a misleading
"No notifications found"
- posts: skip posts with no permalink href instead of emitting a bogus
'https://www.band.us' URL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(band): address sixth round of code review feedback
- post: only send Band cookies to *.band.us photo URLs; third-party CDN
URLs are downloaded without cookies to avoid cross-domain cookie leakage
- bands: strip non-digit chars before parseInt so member counts like
"1,234" parse correctly
- posts: same fix for comment counts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(band): address seventh round of code review feedback
- posts: check limit before push so --limit 0 returns empty result
- post: indent replies proportionally by depth (' '.repeat(depth))
so multi-level threads remain readable in table output
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(band/bands): anchor href regex to prevent matching post-detail URLs
Pattern now requires /band/{id} or /band/{id}/post (with optional trailing
slash) so deeper paths like /band/{id}/post/{postNo} are excluded.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(band): address ninth round of code review feedback
- mentions: guard bell click with a boolean return so a disappearing
element throws a clear EmptyResultError instead of a raw TypeError
- post: wait for comment list container instead of first .cComment so
posts with zero comments don't incur a fixed 6s delay
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(band): use page.getCookies() for login detection across all commands
Replaces document.cookie.includes('band_session') with
page.getCookies({ domain: 'band.us' }) so login detection works even
if Band.us marks the session cookie as HttpOnly in the future.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(band): address eleventh round of code review feedback
- mentions: replace EmptyResultError with SelectorError for missing/
disappeared bell button — produces a clearer SELECTOR error code
- post: assign per-photo filenames using a global index across both
download batches so band-hosted and CDN photos don't overwrite each other
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(band): address twelfth round of code review feedback
- post: derive file extension from URL path and include in filename
(e.g. photo_1.jpg) so downloaded photos have correct extensions
- posts: remove dead code guard (!url && !content) — url is always
non-empty here since href-empty posts are already skipped above
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(band/post): use url-scoped getCookies for photo download auth
Domain-scoped getCookies may omit host-only cookies scoped to www.band.us;
using url: 'https://www.band.us' ensures all relevant cookies are included
in the auth header for Band-hosted photo downloads.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(band): add adapter documentation and sidebar entry
Required by CI doc-check --strict: every adapter in src/clis/ must have
a corresponding docs/adapters/browser/*.md file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(e2e): wire band auth coverage into default matrix
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(xiaohongshu): use CDP DOM.setFileInputFiles for image upload
Replace base64 DataTransfer injection with CDP DOM.setFileInputFiles,
which lets Chrome read image files directly from the local filesystem.
This eliminates payload size limits that caused "fetch failed" errors
when uploading large images (>500KB) through the browser bridge.
Changes:
- Add 'set-file-input' action to protocol, extension handler, and CDP executor
- Add Page.setFileInput() method for CLI-side usage
- Rewrite publish image upload to use CDP path, with base64 fallback
for older extension versions that don't support the new action
- Add clear warning when falling back to base64 with large payloads
Closes#542 (partially — image upload reliability)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: cover cdp file input upload path
* fix: keep image upload on image-only inputs
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat: add 知识星球(zsxq) site adapter
Add cookie-based adapter for 知识星球 (zsxq.com) with 5 commands:
- groups: list joined groups
- topics: list topics in current group
- topic: get single topic detail with comments
- search: search topics within a group
- dynamics: latest cross-group activity feed
Uses XHR over Chrome extension (Strategy.COOKIE) to call
https://api.zsxq.com/v2/ APIs with credential forwarding.
* fix(zsxq): map missing topics to not found
* refactor(zsxq): preserve detail response semantics
---------
Co-authored-by: xiaojian <xiaojian@xiaojiandeMac-mini.local>
Co-authored-by: jackwener <jakevingoo@gmail.com>
Argument/usage errors now correctly exit with code 2 (EX_USAGE) since
the exit-codes feature landed. Update the two affected E2E assertions:
- unknown command → 2 (usage error, not generic failure)
- plugin update without args → 2 (ArgumentError)
* feat(exit-codes): add Unix-standard exit codes to all CliError types
Introduce EXIT_CODES constant table (sysexits.h conventions) and wire
exitCode into every CliError subclass so the process exit code reflects
the semantic type of failure:
0 success (default)
1 generic / unexpected error
2 argument / usage error (ArgumentError)
66 empty result / not found (EmptyResultError, SelectorError)
69 service unavailable (BrowserConnectError, AdapterLoadError)
77 permission / auth required (AuthRequiredError)
78 configuration error (ConfigError)
124 timeout (TimeoutError)
130 Ctrl-C / SIGINT (unchanged, tui.ts)
resolveExitCode() in commanderAdapter.ts reads err.exitCode for typed
CliErrors, and falls back to pattern-matching message text for untyped
adapter errors (auth pattern → 77, not-found pattern → 66, else → 1).
Shell scripts can now distinguish error categories:
opencli spotify status || echo "exit $?" # 69 if browser not running
opencli github issues --repo x 2>/dev/null; [ $? -eq 77 ] && opencli github auth
* fix(exit-codes): address review findings
- TIMEOUT: change from 124 → 75 (EX_TEMPFAIL); 124 is bash timeout(1)'s
own exit code, creating ambiguity when shell runs `timeout 30 opencli`
- SelectorError: change from EMPTY_RESULT(66) → GENERIC_ERROR(1); a
missing DOM selector is an adapter bug, not a user "no data" condition
- normalizeArgValue: throw ArgumentError instead of bare CliError so
invalid bool args correctly exit with USAGE_ERROR(2) not GENERIC_ERROR(1)
- resolveExitCode: explicitly map 'http' classification to GENERIC_ERROR
to keep exit-code path in sync with the render path
- tui.ts: replace hardcoded process.exit(130) with EXIT_CODES.INTERRUPTED
* feat(exit-codes): replace all hardcoded exit numbers with EXIT_CODES constants
Extend the exit code system to cover every process exit point in the codebase.
No magic numbers remain — all exit codes are now referenced by name.
Semantic upgrades beyond pure renaming:
- plugin update missing args → USAGE_ERROR (2) instead of 1
- plugin update conflicting → USAGE_ERROR (2) instead of 1
- opencli install <unknown> → USAGE_ERROR (2) instead of 1
- unknown command fallback → USAGE_ERROR (2) instead of 1
- record with no candidates → EMPTY_RESULT (66) instead of 1
- external CLI install fail → SERVICE_UNAVAIL (69) instead of 1
- daemon EADDRINUSE → SERVICE_UNAVAIL (69) instead of 1
Files touched: cli.ts, external.ts, daemon.ts, main.ts,
clis/antigravity/serve.ts
* feat(sinafinance): rewrite stock as public API adapter
Replace browser-based DOM scraping with direct Sina public APIs:
suggest3.sinajs.cn — symbol search (GBK, no auth)
hq.sinajs.cn — real-time quote (GBK, no auth)
Strategy.PUBLIC, browser: false — no Chrome or login required.
Supports A股 (sh/sz), 港股 (hk prefix), 美股 (gb_ prefix).
US MarketCap parsed from hq field [12]; formatted as T/B/M.
* feat(exit-codes): add Unix-standard exit codes to all CliError types
Introduce EXIT_CODES constant table (sysexits.h conventions) and wire
exitCode into every CliError subclass so the process exit code reflects
the semantic type of failure:
0 success (default)
1 generic / unexpected error
2 argument / usage error (ArgumentError)
66 empty result / not found (EmptyResultError, SelectorError)
69 service unavailable (BrowserConnectError, AdapterLoadError)
77 permission / auth required (AuthRequiredError)
78 configuration error (ConfigError)
124 timeout (TimeoutError)
130 Ctrl-C / SIGINT (unchanged, tui.ts)
resolveExitCode() in commanderAdapter.ts reads err.exitCode for typed
CliErrors, and falls back to pattern-matching message text for untyped
adapter errors (auth pattern → 77, not-found pattern → 66, else → 1).
Shell scripts can now distinguish error categories:
opencli spotify status || echo "exit $?" # 69 if browser not running
opencli github issues --repo x 2>/dev/null; [ $? -eq 77 ] && opencli github auth
* review: regex escape sym, fix change precision, optimize suggest type param
* fix: remove invalid `state: 'normal'` from chrome.windows.create()
Chrome 146+ rejects 'normal' as an invalid value for the `state` parameter
in chrome.windows.create(). This causes the error:
Error: Invalid value for state
Root cause analysis:
- The Chrome Extensions API documentation states that `state` parameter
only accepts 'minimized', 'maximized', and 'fullscreen' as input values
- While WindowState enum includes 'normal', it's meant for reading window
state, not for setting it during creation
- Chrome 146 enforces stricter validation on the `state` parameter
- When `state` is omitted, the window defaults to 'normal' state anyway
Fix: Remove the `state: 'normal'` parameter entirely. The window will
default to normal state without explicitly setting it.
Tested: `opencli doctor` and `opencli bilibili hot` now work correctly
on Chrome 146.0.7680.165.
* build: rebuild dist after removing state: 'normal'
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(extension): probe daemon via HTTP before WebSocket to eliminate console noise
When the daemon is offline, `new WebSocket()` logs uncatchable
ERR_CONNECTION_REFUSED errors to Chrome's extension error page.
Add `probeAndConnect()` that checks daemon reachability with a
silent `fetch(HEAD)` before attempting WebSocket connection.
All three auto-connect paths (initialize, keepalive alarm, eager
reconnect) now go through the probe, eliminating the error noise
entirely.
Closes#505
* refactor(extension): inline probe into connect(), add /ping to daemon
Instead of a separate probeAndConnect() wrapper that all call sites had
to remember to use, bake the HTTP probe directly into connect() itself.
This makes the guard impossible to accidentally skip when adding new
connection paths in the future.
Also adds a dedicated GET /ping endpoint to the daemon (no X-OpenCLI
header required) so the probe has a clear semantic contract instead of
relying on a 403 side-effect from the root path.
- daemon: GET /ping → 200 {ok:true}, no auth needed, placed before the
X-OpenCLI header check; only chrome-extension:// and no-origin
requests reach it (origin check is still enforced above)
- background: connect() is now async; probes /ping with a 1 s timeout
before new WebSocket(); all call sites (initialize, keepalive alarm,
scheduleReconnect) remain unchanged
- probeAndConnect() removed — no longer needed
* fix(extension/daemon): address review feedback on probe refactor
- protocol.ts: replace DAEMON_HTTP_URL with DAEMON_PING_URL (clearer
semantics, single source of truth for the health-check URL)
- background.ts: import DAEMON_PING_URL from protocol instead of
defining a local constant; check res.ok so an unexpected non-200
response doesn't fall through to WebSocket; annotate all fire-and-
forget connect() call sites with `void` to make intent explicit
- daemon.ts: add security comment on /ping documenting the timing
side-channel tradeoff (loopback-only, accepted risk)
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* chore(release): 1.5.2
* test(e2e): stabilize output format checks
* docs: add perf smart-wait design spec (waitForCapture + selector wait + backoff)
* docs: add perf smart-wait implementation plan
* feat(perf): add waitForCaptureJs and waitForSelectorJs to dom-helpers
* feat(perf): extend WaitOptions with selector, add waitForCapture to IPage
* feat(perf): implement waitForCapture() and wait({ selector }) in Page
* feat(perf): implement waitForCapture() and wait({ selector }) in CDPPage
* feat(perf): stepIntercept uses installInterceptor+waitForCapture+getInterceptedRequests
* fix(perf): replace wait(N) with waitForCapture(N) in 7 INTERCEPT adapters
* feat(perf): daemon cold-start uses exponential backoff [50..3000ms]
* fix(perf): replace wait(5) with wait({ selector }) in 15 Twitter UI adapters
* fix(perf): replace wait(N) with wait({ selector }) in medium/substack/bloomberg/sinablog
* fix(types): add waitForCapture to IPage mock helpers in tests
* docs: simplify README to 50-line overview with docs link
Remove redundant command table (already in docs/adapters/index.md).
Keep badges, quick-start, and star history only.
* fix(perf): CDPPage smart wait + MutationObserver selector wait
- CDPPage.wait(N>=1) now uses waitForDomStableJs instead of fixed sleep,
matching Page.wait() behavior and saving unnecessary idle time
- waitForSelectorJs switches from 100ms polling to MutationObserver,
resolving instantly when the target element appears in the DOM
- Update dom-helpers tests to stub MutationObserver for Node eval context
* docs: move built-in commands table to docs/adapters/index.md
Replace the 70-row site/command table in README with a one-line link.
All other README sections (Highlights, Why opencli, Quick Start,
External CLI Hub, Desktop App Adapters, Download, Plugins, AI Agents,
Troubleshooting) are preserved unchanged.
* docs: move Quick Start before Prerequisites, tone down Electron promo copy
- Reorder sections: Why opencli → Quick Start → Prerequisites
so users see "how to use" before "what you need"
- Replace "The Most Powerful Update Has Arrived!" marketing copy
with a plain one-liner description of the Electron feature
- Remove redundant Bun dev/test commands from Prerequisites
(developer-only content, not relevant to end users)
- Add "(requires Extension)" hint to browser command in Quick Start
* docs: polish Quick Start — one-line source install, Verify setup section
* docs: show 4 sample adapters in Built-in Commands with link to full list
* docs: polish README — Try it out under Verify setup, trim examples, CLI Hub as top-level section
* docs: add CLI Hub intro line in header, restore auto-install note in CLI Hub section
* chore(release): 1.5.2
* test(e2e): stabilize output format checks
* docs: add perf smart-wait design spec (waitForCapture + selector wait + backoff)
* docs: add perf smart-wait implementation plan
* feat(perf): add waitForCaptureJs and waitForSelectorJs to dom-helpers
* feat(perf): extend WaitOptions with selector, add waitForCapture to IPage
* feat(perf): implement waitForCapture() and wait({ selector }) in Page
* feat(perf): implement waitForCapture() and wait({ selector }) in CDPPage
* feat(perf): stepIntercept uses installInterceptor+waitForCapture+getInterceptedRequests
* fix(perf): replace wait(N) with waitForCapture(N) in 7 INTERCEPT adapters
* feat(perf): daemon cold-start uses exponential backoff [50..3000ms]
* fix(perf): replace wait(5) with wait({ selector }) in 15 Twitter UI adapters
* fix(perf): replace wait(N) with wait({ selector }) in medium/substack/bloomberg/sinablog
* fix(types): add waitForCapture to IPage mock helpers in tests
* docs: simplify README to 50-line overview with docs link
Remove redundant command table (already in docs/adapters/index.md).
Keep badges, quick-start, and star history only.
* fix(perf): CDPPage smart wait + MutationObserver selector wait
- CDPPage.wait(N>=1) now uses waitForDomStableJs instead of fixed sleep,
matching Page.wait() behavior and saving unnecessary idle time
- waitForSelectorJs switches from 100ms polling to MutationObserver,
resolving instantly when the target element appears in the DOM
- Update dom-helpers tests to stub MutationObserver for Node eval context
* docs: move built-in commands table to docs/adapters/index.md
Replace the 70-row site/command table in README with a one-line link.
All other README sections (Highlights, Why opencli, Quick Start,
External CLI Hub, Desktop App Adapters, Download, Plugins, AI Agents,
Troubleshooting) are preserved unchanged.
* docs: move Quick Start before Prerequisites, tone down Electron promo copy
- Reorder sections: Why opencli → Quick Start → Prerequisites
so users see "how to use" before "what you need"
- Replace "The Most Powerful Update Has Arrived!" marketing copy
with a plain one-liner description of the Electron feature
- Remove redundant Bun dev/test commands from Prerequisites
(developer-only content, not relevant to end users)
- Add "(requires Extension)" hint to browser command in Quick Start
* docs: polish Quick Start — one-line source install, Verify setup section
* docs: show 4 sample adapters in Built-in Commands with link to full list
* docs: polish README — Try it out under Verify setup, trim examples, CLI Hub as top-level section
* chore(release): 1.5.2
* test(e2e): stabilize output format checks
* docs: add perf smart-wait design spec (waitForCapture + selector wait + backoff)
* docs: add perf smart-wait implementation plan
* feat(perf): add waitForCaptureJs and waitForSelectorJs to dom-helpers
* feat(perf): extend WaitOptions with selector, add waitForCapture to IPage
* feat(perf): implement waitForCapture() and wait({ selector }) in Page
* feat(perf): implement waitForCapture() and wait({ selector }) in CDPPage
* feat(perf): stepIntercept uses installInterceptor+waitForCapture+getInterceptedRequests
* fix(perf): replace wait(N) with waitForCapture(N) in 7 INTERCEPT adapters
* feat(perf): daemon cold-start uses exponential backoff [50..3000ms]
* fix(perf): replace wait(5) with wait({ selector }) in 15 Twitter UI adapters
* fix(perf): replace wait(N) with wait({ selector }) in medium/substack/bloomberg/sinablog
* fix(types): add waitForCapture to IPage mock helpers in tests
* docs: simplify README to 50-line overview with docs link
Remove redundant command table (already in docs/adapters/index.md).
Keep badges, quick-start, and star history only.
* fix(perf): CDPPage smart wait + MutationObserver selector wait
- CDPPage.wait(N>=1) now uses waitForDomStableJs instead of fixed sleep,
matching Page.wait() behavior and saving unnecessary idle time
- waitForSelectorJs switches from 100ms polling to MutationObserver,
resolving instantly when the target element appears in the DOM
- Update dom-helpers tests to stub MutationObserver for Node eval context
* docs: move built-in commands table to docs/adapters/index.md
Replace the 70-row site/command table in README with a one-line link.
All other README sections (Highlights, Why opencli, Quick Start,
External CLI Hub, Desktop App Adapters, Download, Plugins, AI Agents,
Troubleshooting) are preserved unchanged.
* docs: move Quick Start before Prerequisites, tone down Electron promo copy
- Reorder sections: Why opencli → Quick Start → Prerequisites
so users see "how to use" before "what you need"
- Replace "The Most Powerful Update Has Arrived!" marketing copy
with a plain one-liner description of the Electron feature
- Remove redundant Bun dev/test commands from Prerequisites
(developer-only content, not relevant to end users)
- Add "(requires Extension)" hint to browser command in Quick Start
* docs: polish Quick Start — one-line source install, Verify setup section
* docs: show 4 sample adapters in Built-in Commands with link to full list
* chore(release): 1.5.2
* test(e2e): stabilize output format checks
* docs: add perf smart-wait design spec (waitForCapture + selector wait + backoff)
* docs: add perf smart-wait implementation plan
* feat(perf): add waitForCaptureJs and waitForSelectorJs to dom-helpers
* feat(perf): extend WaitOptions with selector, add waitForCapture to IPage
* feat(perf): implement waitForCapture() and wait({ selector }) in Page
* feat(perf): implement waitForCapture() and wait({ selector }) in CDPPage
* feat(perf): stepIntercept uses installInterceptor+waitForCapture+getInterceptedRequests
* fix(perf): replace wait(N) with waitForCapture(N) in 7 INTERCEPT adapters
* feat(perf): daemon cold-start uses exponential backoff [50..3000ms]
* fix(perf): replace wait(5) with wait({ selector }) in 15 Twitter UI adapters
* fix(perf): replace wait(N) with wait({ selector }) in medium/substack/bloomberg/sinablog
* fix(types): add waitForCapture to IPage mock helpers in tests
* docs: simplify README to 50-line overview with docs link
Remove redundant command table (already in docs/adapters/index.md).
Keep badges, quick-start, and star history only.
* fix(perf): CDPPage smart wait + MutationObserver selector wait
- CDPPage.wait(N>=1) now uses waitForDomStableJs instead of fixed sleep,
matching Page.wait() behavior and saving unnecessary idle time
- waitForSelectorJs switches from 100ms polling to MutationObserver,
resolving instantly when the target element appears in the DOM
- Update dom-helpers tests to stub MutationObserver for Node eval context
* docs: move built-in commands table to docs/adapters/index.md
Replace the 70-row site/command table in README with a one-line link.
All other README sections (Highlights, Why opencli, Quick Start,
External CLI Hub, Desktop App Adapters, Download, Plugins, AI Agents,
Troubleshooting) are preserved unchanged.
* docs: move Quick Start before Prerequisites, tone down Electron promo copy
- Reorder sections: Why opencli → Quick Start → Prerequisites
so users see "how to use" before "what you need"
- Replace "The Most Powerful Update Has Arrived!" marketing copy
with a plain one-liner description of the Electron feature
- Remove redundant Bun dev/test commands from Prerequisites
(developer-only content, not relevant to end users)
- Add "(requires Extension)" hint to browser command in Quick Start
* docs: polish Quick Start — one-line source install, Verify setup section
* chore(release): 1.5.2
* test(e2e): stabilize output format checks
* docs: add perf smart-wait design spec (waitForCapture + selector wait + backoff)
* docs: add perf smart-wait implementation plan
* feat(perf): add waitForCaptureJs and waitForSelectorJs to dom-helpers
* feat(perf): extend WaitOptions with selector, add waitForCapture to IPage
* feat(perf): implement waitForCapture() and wait({ selector }) in Page
* feat(perf): implement waitForCapture() and wait({ selector }) in CDPPage
* feat(perf): stepIntercept uses installInterceptor+waitForCapture+getInterceptedRequests
* fix(perf): replace wait(N) with waitForCapture(N) in 7 INTERCEPT adapters
* feat(perf): daemon cold-start uses exponential backoff [50..3000ms]
* fix(perf): replace wait(5) with wait({ selector }) in 15 Twitter UI adapters
* fix(perf): replace wait(N) with wait({ selector }) in medium/substack/bloomberg/sinablog
* fix(types): add waitForCapture to IPage mock helpers in tests
* docs: simplify README to 50-line overview with docs link
Remove redundant command table (already in docs/adapters/index.md).
Keep badges, quick-start, and star history only.
* fix(perf): CDPPage smart wait + MutationObserver selector wait
- CDPPage.wait(N>=1) now uses waitForDomStableJs instead of fixed sleep,
matching Page.wait() behavior and saving unnecessary idle time
- waitForSelectorJs switches from 100ms polling to MutationObserver,
resolving instantly when the target element appears in the DOM
- Update dom-helpers tests to stub MutationObserver for Node eval context
* docs: move built-in commands table to docs/adapters/index.md
Replace the 70-row site/command table in README with a one-line link.
All other README sections (Highlights, Why opencli, Quick Start,
External CLI Hub, Desktop App Adapters, Download, Plugins, AI Agents,
Troubleshooting) are preserved unchanged.
* docs: move Quick Start before Prerequisites, tone down Electron promo copy
- Reorder sections: Why opencli → Quick Start → Prerequisites
so users see "how to use" before "what you need"
- Replace "The Most Powerful Update Has Arrived!" marketing copy
with a plain one-liner description of the Electron feature
- Remove redundant Bun dev/test commands from Prerequisites
(developer-only content, not relevant to end users)
- Add "(requires Extension)" hint to browser command in Quick Start
* chore(release): 1.5.2
* test(e2e): stabilize output format checks
* docs: add perf smart-wait design spec (waitForCapture + selector wait + backoff)
* docs: add perf smart-wait implementation plan
* feat(perf): add waitForCaptureJs and waitForSelectorJs to dom-helpers
* feat(perf): extend WaitOptions with selector, add waitForCapture to IPage
* feat(perf): implement waitForCapture() and wait({ selector }) in Page
* feat(perf): implement waitForCapture() and wait({ selector }) in CDPPage
* feat(perf): stepIntercept uses installInterceptor+waitForCapture+getInterceptedRequests
* fix(perf): replace wait(N) with waitForCapture(N) in 7 INTERCEPT adapters
* feat(perf): daemon cold-start uses exponential backoff [50..3000ms]
* fix(perf): replace wait(5) with wait({ selector }) in 15 Twitter UI adapters
* fix(perf): replace wait(N) with wait({ selector }) in medium/substack/bloomberg/sinablog
* fix(types): add waitForCapture to IPage mock helpers in tests
* docs: simplify README to 50-line overview with docs link
Remove redundant command table (already in docs/adapters/index.md).
Keep badges, quick-start, and star history only.
* fix(perf): CDPPage smart wait + MutationObserver selector wait
- CDPPage.wait(N>=1) now uses waitForDomStableJs instead of fixed sleep,
matching Page.wait() behavior and saving unnecessary idle time
- waitForSelectorJs switches from 100ms polling to MutationObserver,
resolving instantly when the target element appears in the DOM
- Update dom-helpers tests to stub MutationObserver for Node eval context
* docs: move built-in commands table to docs/adapters/index.md
Replace the 70-row site/command table in README with a one-line link.
All other README sections (Highlights, Why opencli, Quick Start,
External CLI Hub, Desktop App Adapters, Download, Plugins, AI Agents,
Troubleshooting) are preserved unchanged.
chrome.windows.create rejects state:'minimized' when combined with
width/height (Chrome API constraint). Revert to state:'normal' to fix
the "Invalid value for state" error. The 30s idle timeout from #521
is preserved.
Fixes#526
* refactor: slim CI matrix, extract shared utils, unify logging, remove __test__ from public API
- CI: unit-test uses dynamic matrix (PR=ubuntu+22 only, push=full 3OS×2Node);
adapter-test reduced to ubuntu-latest (OS doesn't affect pure unit tests)
- _shared/common.ts: add sleep() and clampToRange() shared adapter utilities;
douban/utils.ts and sinablog/utils.ts now use clampToRange instead of duplicate clampLimit
- browser/daemon-client.ts: replace inline setTimeout Promise with local sleep()
- execution.ts: replace conditional console.error with log.debug
- browser/index.ts: remove __test__ from public barrel export;
browser.test.ts now imports internal helpers directly from source files
* fix: remove unused afterEach import, fix schedule/dispatch CI matrix, clarify clampToRange docs
* refactor: move sleep to src/utils.ts, simplify clamp signature to match lodash convention
* perf: smart pre-navigation — skip redundant domain nav + remove hardcoded 2s wait
- Add `getCurrentUrl()` to IPage, Page, and CDPPage to check current browser URL
- Skip pre-navigation entirely if the browser is already on the target domain
- Remove the hardcoded `page.wait(2)` after pre-navigation — `page.goto()` already
includes smart DOM-settle detection via `waitForDomStable`, making the fixed
2-second sleep redundant
- Saves ~2s per browser command in the common case (consecutive commands on the
same site), and ~1-2s even on cold navigation
* perf: smart page.wait() — DOM-stable early return for waits >= 1s
For page.wait(N) where N >= 1 second, use DOM MutationObserver-based
stability detection instead of a fixed sleep. The original wait time
becomes a hard cap, but the call returns as soon as the DOM stops
mutating (500ms quiet period).
This benefits ~200 hardcoded sleep calls across ~40 adapters without
changing any adapter code. A typical page.wait(5) now completes in
<1s when the page is already stable, instead of always waiting 5s.
Short waits (< 1s) are kept as fixed sleeps — these are typically
UI animation delays or anti-bot throttling where DOM-ready is irrelevant.
* refactor: getCurrentUrl() uses in-memory tracking instead of round-trip
Replace the sendCommand('exec', 'window.location.href') call with a
local _lastUrl field set during goto(). This eliminates a daemon HTTP
round-trip for the domain check, making isAlreadyOnDomain() zero-cost.
On fresh tabs (about:blank), _lastUrl is null so we correctly fall
through to navigation — no special-casing needed.
* fix(browser): retry settle probe after SPA client-side redirect
SPA sites like creator.xiaohongshu.com can trigger a client-side
redirect after chrome.tabs reports status 'complete', invalidating
the CDP target. The waitForDomStable probe in page.goto() was
unprotected, causing -32000 "Inspected target navigated or closed".
Wrap the settle probe in try/catch with a single 200ms-delayed retry,
consistent with the existing stealth injection error handling pattern.
The retry gives the SPA redirect time to complete, while the outer
catch ensures settle failure never crashes goto() since navigation
itself already succeeded.
Closes#502
* review: narrow settle retry to target redirects
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* test(e2e): accept current apple podcasts fetch errors
* fix(ci): stabilize plugin and public command checks
---------
Co-authored-by: pi-dal <hi@pi-dal.com>
* feat: zero onboarding, extension version check, and update notifier
- Fail-fast guard in execution.ts: when daemon is running but extension
is not connected, immediately surface a setup guide instead of waiting
for the 30s connect timeout
- Extension version handshake: extension sends `hello` with its version
on WebSocket connect; daemon stores it and exposes via /status; CLI
warns on mismatch in both execution path and `opencli doctor`
- `opencli doctor` now shows extension version inline and reports
version mismatch as an actionable issue
- Non-blocking npm update checker: registers a process exit hook so the
update notice appears after command output (same pattern as npm/gh/yarn);
background fetch writes to ~/.opencli/update-check.json for next run
- postinstall: print Browser Bridge setup instructions after shell
completion install for first-time global install users
Bug fixes caught in review:
- discover.ts: add AbortController timeout to checkDaemonStatus() fetch,
move clearTimeout after res.json() to cover body streaming
- daemon.ts: clear extensionVersion and reject pending requests in
ws.on('error') handler, not just ws.on('close')
- update-check.ts: skip update notice when process exits with non-zero
code; read cache once at module load to avoid double disk I/O;
guard isNewer() against NaN from pre-release version strings
* fix: relax extension version check to major-only in doctor, remove from hot path
* test: enable all adapter tests via wildcard glob, fix apple-podcasts url field
* fix: clearTimeout in finally block, reset extensionVersion on reconnect, fix e2e regex
- Create automation window with `state: 'minimized'` so it never
appears in the user's taskbar or steals visual attention
- Reduce idle timeout from 120s to 30s — window closes quickly after
the last command finishes, instead of lingering for 2 minutes
- CDP debugger works fine on minimized windows, no functional impact
Fixes the user-visible issue of a blank data:text/html tab appearing
during command execution.
* perf: smart pre-navigation — skip redundant domain nav + remove hardcoded 2s wait
- Add `getCurrentUrl()` to IPage, Page, and CDPPage to check current browser URL
- Skip pre-navigation entirely if the browser is already on the target domain
- Remove the hardcoded `page.wait(2)` after pre-navigation — `page.goto()` already
includes smart DOM-settle detection via `waitForDomStable`, making the fixed
2-second sleep redundant
- Saves ~2s per browser command in the common case (consecutive commands on the
same site), and ~1-2s even on cold navigation
* perf: smart page.wait() — DOM-stable early return for waits >= 1s
For page.wait(N) where N >= 1 second, use DOM MutationObserver-based
stability detection instead of a fixed sleep. The original wait time
becomes a hard cap, but the call returns as soon as the DOM stops
mutating (500ms quiet period).
This benefits ~200 hardcoded sleep calls across ~40 adapters without
changing any adapter code. A typical page.wait(5) now completes in
<1s when the page is already stable, instead of always waiting 5s.
Short waits (< 1s) are kept as fixed sleeps — these are typically
UI animation delays or anti-bot throttling where DOM-ready is irrelevant.
* refactor: getCurrentUrl() uses in-memory tracking instead of round-trip
Replace the sendCommand('exec', 'window.location.href') call with a
local _lastUrl field set during goto(). This eliminates a daemon HTTP
round-trip for the domain check, making isAlreadyOnDomain() zero-cost.
On fresh tabs (about:blank), _lastUrl is null so we correctly fall
through to navigation — no special-casing needed.
Extends parseSource() to accept any git-cloneable URL, not just GitHub:
- ssh://git@host/path/repo.git
- git@host:user/repo.git (SCP-style)
- https://any-host.com/path/repo.git
GitHub shorthand (github:user/repo) and local paths continue to work.
Updated error messages, CLI description, docs, and added 7 new unit tests.
Closes#492
When a TS plugin is installed but esbuild is unavailable or transpilation
fails silently, the plugin discovery would attempt to import() the raw
.ts file, causing 'Unknown file extension .ts' on production Node.js.
Changes:
- discovery.ts: Skip raw .ts import when no compiled .js exists; show
an actionable warning guiding the user to re-transpile or install esbuild
- plugin.ts: Upgrade esbuild-not-found from debug to warn level; log
the outer catch error instead of silently swallowing it
Closes#500
Non-browser commands (`browser: false`) ran without any timeout
protection, even when `timeoutSeconds` was explicitly set. This wraps
the non-browser execution path with `runWithTimeout()` when the
adapter defines a positive `timeoutSeconds`.
Also adds an optional `hint` parameter to `TimeoutError` so the
non-browser path shows a relevant suggestion instead of the
browser-specific `OPENCLI_BROWSER_COMMAND_TIMEOUT` env var hint.
Bluesky (9 commands, public AT Protocol API, no auth needed):
- profile: user profile info (followers, following, posts)
- user: recent posts from a user with engagement stats
- trending: trending topics on Bluesky
- search: search users
- feeds: popular feed generators
- followers: list user's followers
- following: list accounts a user follows
- thread: post thread with replies
- starter-packs: user's starter packs
All commands use the public Bluesky API, no browser or login required.
* fix(plugin): handle EXDEV cross-filesystem rename during install
fs.renameSync() fails with EXDEV when source and destination are on
different filesystem mount points. This commonly happens because plugin
clones land in os.tmpdir() (often /tmp on a tmpfs) while plugins are
installed to ~/.opencli/plugins/ (on the root filesystem).
Add a moveDir() helper that catches EXDEV and falls back to
fs.cpSync() + fs.rmSync(). Applied to both single-plugin and monorepo
install paths.
* review: clean up failed EXDEV fallback installs
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
Derive approximate publish date from note IDs, which follow MongoDB
ObjectID format (first 8 hex chars = Unix timestamp). Exported as a
pure function with UTC+8 offset for China timezone.
Closes#484
- Parallelize file scanning in discoverClisFromFs and discoverPluginDir
using Promise.all(files.map(async ...)) instead of serial for-of with
await, so isCliModule checks run concurrently
- Parallelize plugin directory scanning in discoverPlugins
- Cache loadExternalClis() result to avoid re-parsing YAML on every call
- Invalidate cache in registerExternalCli after writing to disk
- Cache strategyLabel() call in list command to avoid redundant computation
- Add comment explaining why discovery must remain sequential (plugin override semantics)
Remove guide.json API path that returned data inconsistent with what
users see on the page (#463). Use semantic caret button detection
via data-testid instead of position-based heuristics, and validate
post count text contains digits before displaying.
* fix(plugin): detect symlinked monorepo sub-plugins in discoverPlugins
discoverPlugins() used entry.isDirectory() to filter plugin directories,
but monorepo sub-plugins are installed as symlinks pointing into
~/.opencli/monorepos/. On most Node.js versions, isDirectory() returns
false for symlinks, causing monorepo plugin commands to be silently
skipped during discovery.
Add entry.isSymbolicLink() check so symlinked plugin directories are
properly discovered and their commands registered.
* fix(plugin): skip broken symlink discovery
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(xiaohongshu): adapt publish to new two-step creator center UI (#460)
The creator center now requires image upload before showing the
title/content editor form. This caused the publish command to fail
with "Could not find title input".
- Add waitForEditForm() to poll for editor after image upload
- Extract TITLE_SELECTORS constant shared by waitForEditForm and fillField
- Add contenteditable title selectors for new UI
- Make images required (new UI mandates images before editor)
- Update draft button to match both '暂存离开' and '存草稿'
- Exclude title placeholder from content fallback selector
- Update tests to match new flow
* refactor(xiaohongshu): clarify publish surface states
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
Add support for installing plugins from local directories:
opencli plugin install file:///path/to/my-plugin
opencli plugin install /path/to/my-plugin
Local plugins are symlinked (not copied) into ~/.opencli/plugins/
so code changes are reflected immediately without reinstall — ideal
for plugin development workflows.
Changes:
- parseSource() now handles file:// URLs and bare absolute paths
- New installLocalPlugin() creates symlink + installs deps + transpiles
- Lock file records 'local:<path>' as source for local plugins
- 6 new test cases for local path parsing and install behavior
Remove the module-level LOCK_FILE and MONOREPOS_DIR constants that were
computed at load time using os.homedir(). These ignored the HOME
environment variable, causing path mismatches when tests use HOME for
isolation.
All usages now go through getLockFilePath() and getMonoreposDir() which
respect process.env.HOME. Updated plugin.test.ts accordingly.
* feat: zero onboarding, extension version check, and update notifier
- Fail-fast guard in execution.ts: when daemon is running but extension
is not connected, immediately surface a setup guide instead of waiting
for the 30s connect timeout
- Extension version handshake: extension sends `hello` with its version
on WebSocket connect; daemon stores it and exposes via /status; CLI
warns on mismatch in both execution path and `opencli doctor`
- `opencli doctor` now shows extension version inline and reports
version mismatch as an actionable issue
- Non-blocking npm update checker: registers a process exit hook so the
update notice appears after command output (same pattern as npm/gh/yarn);
background fetch writes to ~/.opencli/update-check.json for next run
- postinstall: print Browser Bridge setup instructions after shell
completion install for first-time global install users
Bug fixes caught in review:
- discover.ts: add AbortController timeout to checkDaemonStatus() fetch,
move clearTimeout after res.json() to cover body streaming
- daemon.ts: clear extensionVersion and reject pending requests in
ws.on('error') handler, not just ws.on('close')
- update-check.ts: skip update notice when process exits with non-zero
code; read cache once at module load to avoid double disk I/O;
guard isNewer() against NaN from pre-release version strings
* fix: reduce fail-fast timeout to 300ms and guard stderr.write in exit hook
* fix(doctor): remove unused fix option and add release URL to extension install hint
* fix(e2e): update BrowserBridge unavailable detection regex to match current error format
* feat: smart error dispatch with inline Browser Bridge diagnosis
- BrowserConnectError: runs checkDaemonStatus() on failure, shows real-time
daemon/extension status and specific fix steps instead of a static hint
- AuthRequiredError: domain-specific login guidance
- TimeoutError: shows exact env var override command
- SelectorError/EmptyResultError: flags adapter as potentially outdated,
links to debug command and issue tracker
- Generic untyped errors (164 in adapters): pattern-classified into
auth/http/not-found/other with tailored guidance per category
- BrowserConnectError gains a `kind` field for future dispatch
- Added 6 new error icons (COMMAND_EXEC, ADAPTER_LOAD, NETWORK, etc.)
- Updated test: invalid bool now rejected eagerly in commanderAdapter
* fix: review fixes for smart error dispatch
- checkDaemonStatus: add { timeout: 300 } to match execution.ts behavior,
avoids 2s wait on an already-failed path
- catch block: use named _statusErr variable; fall back to kind-derived
state (running/extensionConnected inferred from BrowserConnectError.kind)
instead of re-accessing outer err.hint ambiguously
- Extract renderBridgeStatus() helper to share logic between real-time
and kind-derived fallback paths
- AuthRequiredError: use err.hint when set, respecting adapter-supplied
hints; fall back to generic domain-based guidance
- HTTP regex: broaden from 'http [45]xx' to also match 'status: 404',
bare '404', 'status 500', etc. — avoids false negatives
* feat: zero onboarding, extension version check, and update notifier
- Fail-fast guard in execution.ts: when daemon is running but extension
is not connected, immediately surface a setup guide instead of waiting
for the 30s connect timeout
- Extension version handshake: extension sends `hello` with its version
on WebSocket connect; daemon stores it and exposes via /status; CLI
warns on mismatch in both execution path and `opencli doctor`
- `opencli doctor` now shows extension version inline and reports
version mismatch as an actionable issue
- Non-blocking npm update checker: registers a process exit hook so the
update notice appears after command output (same pattern as npm/gh/yarn);
background fetch writes to ~/.opencli/update-check.json for next run
- postinstall: print Browser Bridge setup instructions after shell
completion install for first-time global install users
Bug fixes caught in review:
- discover.ts: add AbortController timeout to checkDaemonStatus() fetch,
move clearTimeout after res.json() to cover body streaming
- daemon.ts: clear extensionVersion and reject pending requests in
ws.on('error') handler, not just ws.on('close')
- update-check.ts: skip update notice when process exits with non-zero
code; read cache once at module load to avoid double disk I/O;
guard isNewer() against NaN from pre-release version strings
* fix: reduce fail-fast timeout to 300ms and guard stderr.write in exit hook
* feat(twitter): add time column to search output
Extract created_at from tweet data and format as ISO datetime.
This helps users filter tweets by recency during monitoring.
Closes#465
* refactor(twitter): align search timestamp field with created_at
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(imdb): add IMDb adapter with 6 commands
Add a public IMDb adapter using browser-based JSON-LD and __NEXT_DATA__
extraction. All commands use Strategy.PUBLIC with browser: true.
Commands:
- imdb search <query> — search movies, TV shows, and people
- imdb title <id> — get movie/show details (Movie, TVSeries, TVEpisode, TVMiniseries, TVMovie, etc.)
- imdb top — IMDb Top 250 chart
- imdb trending — Most Popular Movies
- imdb person <id> — actor/director info with filmography
- imdb reviews <id> — user reviews (first page, max 25)
Shared utils: ID normalization, ISO 8601 duration formatting, locale
forcing, JSON-LD extraction (supports type array filtering), and
anti-bot challenge detection.
* review: harden imdb adapter loading and tests
* test: unblock PR CI on merge head
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat: add bilibili/comments, xiaohongshu/comments, and rate-limiter plugin docs
- bilibili/comments: fetch top-level replies via /x/v2/reply/main with WBI signing
(bvid → aid resolution + signed params, no DOM dependency)
- xiaohongshu/comments: DOM extraction from note detail page with login-wall detection
and correct handling of 0-like counts (XHS shows "赞" text instead of "0")
- docs/advanced/rate-limiter-plugin.md: documents the onAfterExecute hook pattern
and shows a plug-and-play rate limiter that adds random sleep between platform
commands to reduce bot-detection risk
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(xiaohongshu): allow empty comments results
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(runtime): add runtime detection utility for Bun/Node.js
Add runtime-detect.ts module that detects whether opencli is running
under Bun or Node.js via globalThis.Bun check. Includes helper
functions for version string and label formatting.
Add corresponding unit tests that work correctly under both runtimes.
* feat(runtime): integrate Bun runtime support into CLI tooling
- doctor: show runtime label (e.g. 'node v22.13.0') in diagnostic output
- package.json: add dev:bun, start:bun, test:bun convenience scripts
- E2E helpers: support OPENCLI_TEST_RUNTIME env var for runtime selection
* ci: add Bun compatibility test job and document runtime support
- ci.yml: add bun-test job using oven-sh/setup-bun@v2
- README.md: update Prerequisites to mention Bun, add Runtime Support
section with usage examples for dev:bun, start:bun, test:bun
* ci: pin Bun version in compatibility job
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(producthunt): add Product Hunt CLI adapter
Add three commands:
- posts: RSS feed with optional category filter
- today: latest day's posts from feed
- hot: today's top posts with vote counts (browser INTERCEPT strategy)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(producthunt): add browse command for category best products
Browse top-rated products in any Product Hunt category (e.g. vibe-coding,
ai-agents, developer-tools) with name, tagline, and review count.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(producthunt): add adapter documentation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(producthunt): rebase on main and stabilize selectors
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(ci): include popup assets in extension release
Copy popup assets into the packaged Chrome extension zip and validate that manifest-referenced files exist before publishing the artifact.
Co-authored-by: Codex <noreply@openai.com>
* fix: restore executable permission on bin entries after tsc build (#446) (#452)
tsc does not preserve the +x bit when compiling, so after clean-dist
removes dist/ and tsc regenerates it, dist/main.js loses its executable
permission. This causes 'Permission denied' when users run 'npm run build'
in the installed directory.
Fix: read bin entries from package.json at the end of build-manifest and
chmod 0o755 them (skipped on Windows). Wrapped in try/catch so it never
breaks the build.
Closes#446
* fix: correct positional arg usage in tests (#449)
* fix yahoo-finance quote e2e invocation
* fix positional args in v2ex topic tests
* fix(ci): script extension release packaging
---------
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: jakevin <jakevingoo@gmail.com>
Co-authored-by: pi-dal <hi@pi-dal.com>
* fix(xiaohongshu): improve image-text publish flow
Match visible 图文 tab labels instead of relying on narrow class selectors, fail early when the page is still on the video publish surface, and avoid injecting images into a generic file input. Add regression coverage for the image-text tab flow and the video-page failure case.
* test(xiaohongshu): include publish tests in adapter project
* fix(xiaohongshu): wait for image-text surface before upload
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
tsc does not preserve the +x bit when compiling, so after clean-dist
removes dist/ and tsc regenerates it, dist/main.js loses its executable
permission. This causes 'Permission denied' when users run 'npm run build'
in the installed directory.
Fix: read bin entries from package.json at the end of build-manifest and
chmod 0o755 them (skipped on Windows). Wrapped in try/catch so it never
breaks the build.
Closes#446
* feat(linux-do): refactor adapters with unified feed, tags, user commands
- Replace hot/latest/category with unified `feed` command (tag/category/view routing)
- Add `tags`, `user-topics`, `user-posts` commands
- Add static data files for categories and tags lookup
- Fix error handling: use CliError subclasses instead of raw Error
- Fix Discourse API field mapping in search (tags, created)
- Add strategy: cookie to all YAML adapters
- Update docs and README command listings
- Update E2E tests for new command signatures
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* review: resolve linux-do feed from live metadata
* fix: restore linux-do CI
* fix: harden linux-do compatibility
* refactor: stabilize linux-do command migration
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(chatgpt): add model/mode selection and fix response polling
Add --model option to ask and send commands, and a new standalone
model command for switching ChatGPT Desktop models via Accessibility API.
Supported models: auto, instant, thinking, 5.2-instant, 5.2-thinking.
Changes:
- ax.ts: add AX_MODEL_SCRIPT (opens Options popover, searches within
AXPopover to avoid matching sidebar items, supports legacy models
submenu) and AX_GENERATING_SCRIPT (detects "Stop generating" button)
- ask.ts: add --model flag; fix polling to wait for generation to
complete instead of returning partial/thinking intermediate text
- send.ts: add --model flag
- model.ts: new standalone command to switch model/mode
* review: activate chatgpt before model selection
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
- Show helpful hint in popup when disconnected: "This is normal. The
extension connects automatically when you run any opencli command."
- Stop eager reconnect after 6 attempts (reaching 60s backoff) to
reduce ERR_CONNECTION_REFUSED noise in console; keepalive alarm
still retries every ~24s at low frequency.
Split browser-public.test.ts: core sites (bilibili, zhihu, v2ex) run
by default; all other 20+ site tests moved to browser-public-extended
and gated behind OPENCLI_E2E=1 to prevent AI agents from launching
dozens of browser instances.
brew install gws installs a git workspace manager, not Google
Workspace CLI. The npm package @nicholasgasior/gws doesn't exist
either. Remove the misleading entry entirely.
Weibo: add feed, me, user, post, comments commands with cookie-based
auth and proper AuthRequiredError handling.
YouTube: add channel info and video comments via InnerTube API.
Also remove internal source references from file headers.
* feat(tiktok): add video URL to search results
Add a 'url' field to the TikTok search adapter output, constructed from
the author's uniqueId and the video id returned by the API. This allows
downstream consumers (AI agents, pipelines, scripts) to link directly to
each video instead of only having the author handle.
The URL format is: https://www.tiktok.com/@{author}/video/{videoId}
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: add url field to 9 search adapters missing it
Add url output to search commands that were missing direct links:
YAML adapters:
- hackernews: surface existing url from map step into columns
- zhihu: pass computed url through map step into columns
- linux-do: construct url from topic id
- instagram: construct profile url from username
- xueqiu: pass computed url through map step into columns
TS adapters:
- arxiv: surface existing url from parseEntries into return + columns
- apple-podcasts: add collectionViewUrl from iTunes API
- medium: add url to columns (already computed in utils)
- weread: construct book url from bookId
This brings search adapter url coverage from 67% to 97% (32/33).
The only adapter without url is dictionary (word lookup, no URL concept).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(weread): use query arg in search
---------
Co-authored-by: Allen Song (Beyondsoft) <v-songjun@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
- Add popup.html/popup.js showing daemon connection status
(Connected / Reconnecting / No daemon connected)
- Add message listener in background.ts to expose WebSocket state
- Add PRIVACY.md with full privacy policy covering all permissions
- Add content_security_policy to manifest.json
- Update description to be clearer for CWS reviewers
* ci: add cross-platform support for E2E and smoke tests
Make headed browser tests (E2E and smoke) runnable on Linux, macOS,
and Windows:
- setup-chrome action: only install xvfb on Linux (macOS/Windows
have native GUI sessions and don't need a virtual display)
- e2e-headed.yml: add OS matrix, use xvfb-run wrapper only on Linux
- ci.yml smoke-test: add OS matrix, use xvfb-run wrapper only on Linux
The browser-actions/setup-chrome action already supports all three
platforms natively.
* ci: exclude Windows from E2E/smoke matrix (Chrome install hangs)
browser-actions/setup-chrome hangs indefinitely during Chrome MSI
installation on Windows runners (observed 10+ min with no progress).
This is a known limitation of Windows CI runners.
Keep Linux + macOS for headed browser tests. Windows is still covered
by build, unit-test, and adapter-test jobs.
* fix: pre-release cleanup — bugs, version sync, and error handling
Bug fixes:
- Fix hasLimit using wrong Set (SEARCH_PARAMS → LIMIT_PARAMS) in
analysis.ts classifyQueryParams
- Remove phantom scroll step from BROWSER_STEPS and KNOWN_STEP_NAMES
(declared but never registered, causes runtime crash if used in YAML)
- Add missing download step to KNOWN_STEP_NAMES (was producing
false-positive validation warnings)
Docs:
- Sync version numbers: SKILL.md, extension/package.json,
extension/manifest.json → 1.3.3
- Add jd, web to README command tables (both EN and zh-CN)
- Update xueqiu commands with fund-holdings, fund-snapshot
Code quality:
- Replace all 22 catch (err: any) with typed error handling using
existing getErrorMessage() utility across 13 files
* fix: remove (err as any) casts in error handling
- antigravity/serve.ts: use typed Error.cause instead of (err as any).cause
- external.ts: move instanceof guard into shouldRetryWithCmdShim,
accept unknown instead of forcing NodeJS.ErrnoException cast at call site
* fix(extension): security hardening — tab isolation, URL validation, cookie scope
Addresses issues raised in #399 (Astro-Han's community triage):
1. Tab isolation bypass: resolveTabId now verifies that an explicit tabId
belongs to the automation window (tab.windowId === session.windowId)
before accepting it. Tabs from the user's browsing session are rejected.
2. URL scheme allowlist: isDebuggableUrl switched from a blocklist
(chrome://, chrome-extension://) to an allowlist (http://, https:// only).
handleNavigate and tabs.new also reject non-http(s) URLs early, blocking
file://, javascript:, and data: scheme abuse.
3. Cookie scope restriction: handleCookies now requires domain or url.
Requests with neither are rejected instead of dumping all browser cookies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(extension): resolve data: URI vs allowlist conflict, plug tabs.select bypass
- Add BLANK_PAGE constant and whitelist it in isDebuggableUrl so
internal blank tabs are not treated as non-debuggable after the
blocklist-to-allowlist change.
- Add isSafeNavigationUrl for user-facing URL validation (http/https
only), keeping it separate from internal isDebuggableUrl.
- Fix tabs.select to verify tab belongs to automation window before
activating, closing a tab isolation bypass.
- Normalize error message style (-- instead of em dash).
* fix(extension): add try-catch for tabs.select with explicit tabId
Gracefully handle the case where cmd.tabId points to a closed tab
instead of letting the unhandled exception bubble up.
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
Replace manual < > comparison with localeCompare({ numeric: true })
so string-encoded numbers (e.g. "99" vs "1000") sort correctly
without requiring an explicit flag. This is a one-line fix that
makes sort just work for all YAML authors.
Co-authored-by: jackwener <jakevingoo@gmail.com>
* chore: fix pre-existing biome lint in template.ts
- isNaN → Number.isNaN (2 occurrences)
- string concatenation → template literal
- biome-ignore for intentional control chars in sanitize regex
* fix(pipeline): evaluate chained || in template engine (#303)
The || handler in evalExpr returned the right side as a literal string
instead of recursively evaluating it. This broke chained fallbacks like
`item.a || item.b || 'default'` — when item.a was falsy, the entire
`item.b || 'default'` was returned as text.
Fix: call evalExpr on the right side so chained || works at any depth.
* perf(pipeline): fast-path string literals in evalExpr to skip VM
When the right side of || is a quoted string like 'N/A', detect it
with a simple regex and return directly instead of falling through
to evalJsExpr which spins up a node:vm sandbox.
* refactor(pipeline): simplify evalExpr by removing hand-rolled operator parsing
Replace the manual regex-based || and arithmetic handlers with a
streamlined flow: pipe filters → fast-path literals → resolvePath →
evalJsExpr (VM). The VM already handles ||, ??, arithmetic, ternary,
etc. natively, so reimplementing them with regex was redundant and
bug-prone (see issue #303).
Key improvements:
- Fix pipe | vs || disambiguation with lookbehind/lookahead regex
(?<!|)|(?!|) so "item.a || item.b | upper" works correctly
- Remove ~20 lines of manual operator handling
- Add numeric literal fast path
- Pipe filter handler now uses evalExpr recursively (not just
resolvePath), enabling filters on complex expressions
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(xueqiu): add danjuan fund account commands
* refactor(xueqiu): convert danjuan fund YAML adapters to TS
- Replace 3 YAML files with 4 TS files (shared utils + 3 commands)
- Extract shared helpers: fetchDanjuanApi, fetchAssetGain, collectHoldings
- Fix double-navigation by using navigateBefore instead of pipeline navigate
- Unify error messages to English with Hint pattern
- Mask real account ID in docs example
- Add explicit default for --account arg
* refactor(xueqiu): optimize danjuan fund adapters
- Single page.evaluate with Promise.all for parallel account fetching
(1 browser round-trip instead of N+1)
- Merge fund-accounts into fund-holdings (account info visible per row)
- 3 files: danjuan-utils.ts (shared), fund-holdings.ts, fund-snapshot.ts
- Strong TypeScript interfaces for all data shapes
- Update docs to reflect 2-command design
* fix(xueqiu): preserve danjuan pre-navigation metadata
* fix(xueqiu): fail on incomplete danjuan snapshots
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(tiktok): add video URL to search results
Add a 'url' field to the TikTok search adapter output, constructed from
the author's uniqueId and the video id returned by the API. This allows
downstream consumers (AI agents, pipelines, scripts) to link directly to
each video instead of only having the author handle.
The URL format is: https://www.tiktok.com/@{author}/video/{videoId}
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: guard against empty uniqueId/id producing invalid URL
When uniqueId or id is missing, return empty string instead of
a malformed URL like "https://www.tiktok.com/@/video/".
---------
Co-authored-by: Allen Song (Beyondsoft) <v-songjun@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* chore: ignore worktree directory
* fix(pipeline): check HTTP status in fetch step
* fix(pipeline): align fetch error semantics
* fix(pipeline): use CliError and add warn logging in fetch step
- Replace bare Error with CliError('FETCH_ERROR') for consistent CLI output
- Return error status from browser evaluate instead of throwing inside it
- Add log.warn() for batch item failures in both browser and non-browser paths
* chore: remove unrelated .worktrees/ from .gitignore
* refactor(fetch): use getErrorMessage(), unify sentinel naming to __httpError
- Use project's existing getErrorMessage() utility instead of manual instanceof checks
- Rename sentinel from __fetchError to __httpError for consistency with other adapters
- Simplify sentinel structure (url already available in outer scope, no need to pass through evaluate)
- Add comment explaining why getErrorMessage() can't be used inside evaluate()
- Add comment explaining CDP error message rewriting behavior
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(pixiv): add Pixiv adapter with 6 commands
Add support for Pixiv (pixiv.net) with the following commands:
- ranking: daily/weekly/monthly illustration rankings
- search: search illustrations by keyword/tag
- user: view artist profile info
- illusts: list illustrations by artist
- detail: view illustration details (tags, stats)
- download: download original-quality images
All commands use COOKIE strategy to reuse Chrome's logged-in session.
YAML adapters for simple API fetches (ranking, detail, user), TypeScript
for complex logic (search, illusts, download with Referer header).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test(pixiv): add unit tests and E2E auth failure tests
- search.test.ts: auth error, result parsing, limit, empty results (4 tests)
- illusts.test.ts: auth error, empty user, two-step fetch, limit (4 tests)
- download.test.ts: auth error, no images, Referer header, partial failure (4 tests)
- Add pixiv to vitest adapter project include list
- Add 5 pixiv commands to E2E browser-auth graceful failure tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(pixiv): correct ranking API path and YAML arg naming
- ranking: use /ranking.php?format=json (not /ajax/ranking which 404s)
- ranking: fix JSON path from data.body.contents to data.contents
- user/detail: rename hyphenated args (user-id → uid, illust-id → id)
to fix YAML template evaluation (dot access doesn't support hyphens)
All 6 commands verified working against live Pixiv API.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(pixiv): use JSON.stringify to prevent code injection in page.evaluate
Address CodeRabbit review: all user inputs (query, userId, illustId,
idsParam) passed to page.evaluate are now serialized via JSON.stringify
instead of direct string interpolation, preventing code injection in
browser context.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(pixiv): address code review feedback
- ranking.yaml: add | json filter to page/limit args for defense-in-depth
- user.yaml: guard illusts/manga/novels with typeof check for robustness
- Extract shared createPageMock to test-utils.ts, deduplicate across 3 test files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(pixiv): use minimal page mock and add download E2E test
- test-utils.ts: slim down to minimal mock (goto, evaluate, getCookies)
with overrides support, matching upstream's pragmatic mock style
- Add missing download command to E2E browser-auth graceful failure tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(pixiv): address all remaining CodeRabbit review comments
- detail.yaml: add url to columns to match description mentioning "URLs"
- All adapters: differentiate HTTP errors — 401/403 → AuthRequiredError,
404 → "not found", others → generic "request failed (HTTP N)"
- Tests: use beforeAll to cache registry lookup, avoiding repeated reads
from global singleton
- Tests: assert error type (AuthRequiredError) not just message content
- Tests: add dedicated test cases for non-auth errors (500) and 404
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs(pixiv): add adapter docs and indexes
- Add pixiv.md documentation page under docs/adapters/browser/
- Update docs/adapters/index.md with pixiv entry
- Add Pixiv to sidebar in docs/.vitepress/config.mts
- Update README.md and README.zh-CN.md adapter tables
- Add pixiv to download support tables in both READMEs
Completes the documentation checklist for the pixiv adapter PR.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(pixiv): address code review findings
- Use CommandExecutionError instead of raw Error for HTTP failures
- Add page.goto() before page.evaluate() to establish browser context
- Fix search keyword double-encoding in URL construction
- Fix ranking.yaml using rating_count instead of illust_bookmark_count
- Throw on batch detail fetch failure instead of silent empty return
- Add beforeEach mock reset in download tests
- Add novels column to user.yaml output
Ensures pixiv adapter follows upstream CliError conventions and handles
edge cases correctly before submitting to upstream.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs(pixiv): improve download description in READMEs
- Replace technical Referer header detail with user-facing description
- Describe what users care about: original quality and multi-page support
Technical details belong in code comments, not user-facing docs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs(pixiv): expand usage examples with all options
- Add ranking mode examples including R18 variants
- Add search filter examples (mode, order, pagination)
- Organize examples by command category for readability
Users need to know available options without reading source code.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(pixiv): address second round of CodeRabbit review comments
- Validate illust-id is numeric to prevent path traversal
- Move URL parsing inside per-item try block for graceful error handling
- Add auth error handling for batch detail request (consistent with step 1)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(pixiv): extract shared pixivFetch helper, add input validation & batch support
- Create utils.ts with pixivFetch() for unified navigate + fetch + error handling
- Refactor search.ts, illusts.ts, download.ts to use pixivFetch (DRY)
- Add user-id/illust-id numeric validation in TS adapters
- Add batch pagination in illusts.ts for limit > 48 (Pixiv server limit)
- Add comment explaining Pixiv search API dual keyword requirement
- Update tests: new invalid-ID test cases, aligned mock format with pixivFetch
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* ci: add cross-platform matrix (Linux/macOS/Windows) to build, unit-test, adapter-test
Add OS matrix with ubuntu-latest, macos-latest, and windows-latest to
the build, unit-test, and adapter-test CI jobs. This ensures cross-
platform compatibility is verified on every push and PR.
Smoke tests remain Linux-only due to xvfb dependency.
Relates to #392 (Windows plugin path issues).
* test: replace hardcoded /tmp with os.tmpdir() for Windows compatibility
Fix Windows CI failures caused by hardcoded '/tmp' paths that don't
exist on Windows. Use os.tmpdir() which returns the correct platform-
specific temp directory on all operating systems.
Files fixed:
- src/engine.test.ts: 3 occurrences (mkdtemp, discoverClis path)
- src/plugin.test.ts: 2 occurrences (getCommitHash test, mock condition)
* test: fix remaining Windows path issues in test files
- engine.test.ts: use pathToFileURL().href for dynamic import paths
(path.join produces backslashes on Windows, breaking ES module imports)
- download.test.ts: replace hardcoded '/tmp' with os.tmpdir() + path.join
* fix(plugin): resolve Windows path and symlink issues
- Replace `new URL(import.meta.url).pathname` with `fileURLToPath()` from
node:url — the former returns `/C:/Users/...` on Windows (leading slash
before drive letter), breaking path resolution for host linking and
esbuild binary lookup.
- Use junction (`'junction'`) instead of directory symlink (`'dir'`) on
Windows in linkHostOpencli — junctions don't require admin privileges,
while `fs.symlinkSync(..., 'dir')` does on Windows.
- Use `where` instead of `which` on Windows for global esbuild lookup.
All changes are platform-conditional and preserve existing Unix behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(plugin): additional Windows fixes found during UAT
- npm execFileSync needs shell:true on Windows (.cmd wrapper)
- esbuild binary is a shebang script, needs shell:true on Windows
- resolveEsbuildBin: prefer .cmd in node_modules/.bin/ on Windows
over import.meta.resolve (which returns a shebang script)
- Updated test to accept .cmd extension on Windows
Found during UAT testing on Windows 11.
* fix: handle multi-line output from 'where' on Windows
'where esbuild' on Windows can return multiple matching paths, one per
line. Take only the first match to get a valid single path for
resolveEsbuildBin().
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: ByteYue <yj976240184@gmail.com>
* test(plugin): add E2E integration tests for plugin lifecycle
Add plugin-management.test.ts covering the full plugin lifecycle
using real GitHub clone of opencli-plugin-hot-digest:
- plugin install from github:ByteYue/opencli-plugin-hot-digest
- plugin list (table and JSON formats)
- plugin update on installed plugin
- plugin uninstall with cleanup verification
- error paths: invalid source, non-existent plugin, missing args
Tests safely backup/restore existing plugin state to avoid
interfering with user's real installed plugins.
Update TESTING.md to document the new test file.
* test(plugin): isolate lifecycle e2e from user home
* fix(plugin): respect HOME env var for test isolation
The E2E tests for plugin management were failing because os.homedir()
doesn't respect the HOME environment variable. This made test isolation
impossible since all tests would use the real ~/.opencli directory.
Added getHomeDir() helper that checks process.env.HOME first before
falling back to os.homedir(). Updated readLockFile() and writeLockFile()
to use this new function.
Fixes test failures in plugin-management.test.ts where:
- plugin install would write to real home instead of temp dir
- lock file assertions would fail with ENOENT
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(plugin): add lifecycle hooks API (onStartup, onBeforeExecute, onAfterExecute)
Introduce a hooks system that allows plugins to tap into opencli's
execution lifecycle without modifying core code.
New files:
- src/hooks.ts: hook registration, emission, and globalThis singleton
- src/hooks.test.ts: 10 unit tests covering registration, ordering,
error isolation, async support, and globalThis sharing
Modified files:
- src/execution.ts: emit onBeforeExecute/onAfterExecute around command execution
- src/main.ts: emit onStartup after discoverPlugins()
- src/registry-api.ts: export hooks API for plugin consumption
Example plugin: https://github.com/ByteYue/opencli-plugin-audit-log
* fix(discovery): load plugin files that register lifecycle hooks
The isCliModule() check only matched files containing 'cli(' calls,
silently skipping hook-only files like audit-hooks.ts that register
onBeforeExecute/onAfterExecute without any cli() command registration.
Renamed CLI_MODULE_PATTERN → PLUGIN_MODULE_PATTERN and extended the
regex to also match onStartup(, onBeforeExecute(, onAfterExecute(.
* fix(plugin): tighten lifecycle hook semantics
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
Add search element heuristics and label/span wrapper detection
- Add SEARCH_INDICATORS set to detect search-related elements
- Add isSearchElement function for heuristic detection
- Add hasFormControlDescendant to detect wrapped form controls
- Enhance isInteractive for label/span wrapper patterns
Ref: browser-use ClickableElementDetector research
Review: @codex
- Remove unused re-exports from registry.ts (serializeArg, serializeCommand, etc.)
- Unify FormatOptions into SnapshotOptions from types.ts; rename dom-snapshot's
SnapshotOptions to DomSnapshotOptions to avoid name collision
- Extract shared analysis.ts module from explore.ts and record.ts, eliminating
~200 lines of duplicated logic (urlToPattern, findArrayPath, inferCapabilityName,
inferStrategy, detectAuth*, classifyQueryParams)
- Merge snapshotFormatter from 7-pass to 4-pass pipeline by combining parse+filter
with ad/boilerplate subtree skipping, and merging three dedup passes into one
- Rename all CLI adapter shared files to consistent utils.ts naming
(boss/common.ts, douban/shared.ts, doubao*/common.ts, jike/shared.ts,
medium/shared.ts, sinablog/shared.ts, substack/shared.ts)
- Merge douban/shared.ts into douban/utils.ts
Remove kubectl from:
- README.md highlights and external CLI examples table
- README.zh-CN.md highlights and external CLI examples table
- src/external-clis.yaml external CLI registry
kubectl is not relevant to the opencli project scope and should not be showcased as a primary example.
The dictionary adapters commit (3d39574) introduced a duplicate
`}, 30_000);` at line 524 of public-commands.test.ts, causing
the vite:oxc transformer to fail with [PARSE_ERROR] Unexpected token
in the E2E Headed Chrome CI workflow.
* feat(browser): human-like delay system for anti-detection
Adds a framework-level delay/jitter system using log-normal distribution
to simulate natural browsing patterns, addressing issue #59 (P0).
- New `HumanDelay` class with configurable profiles (none/fast/moderate/cautious/stealth)
- Log-normal distribution for realistic delay variance (not uniform)
- Periodic "breaks" that simulate reading/thinking pauses
- Auto-injected between page.goto() navigations
- Configurable via OPENCLI_DELAY_PROFILE env var
- Boss search adapter migrated from hardcoded jitter to framework delay
- 10 unit tests covering all profiles and edge cases
Real-world validation against a major job board (cookie-authenticated,
aggressive bot detection):
| Scenario | Without jitter | With jitter |
|-----------------------|--------------------|--------------------|
| 50 detail pages | ✅ OK | ✅ OK |
| 200 detail pages | ❌ Banned (code 32) | ✅ OK |
| 850 requests over 5h | N/A (banned early) | ✅ Zero detection |
| 4-day sustained crawl | N/A | ✅ 1800+ records |
Closes#59
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: disable human delay in CI environment to prevent E2E timeouts
In CI environments (CI=true), resolveProfile() now defaults to the
'none' profile instead of 'moderate'. This prevents the 1-8s per-
navigation delay from causing E2E test timeouts (30s limit).
Users can override this by setting OPENCLI_DELAY_PROFILE explicitly.
---------
Co-authored-by: toolmanlab <toolmanlab@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(grok): preserve conversation across repeated ask calls (#330)
The adapter unconditionally navigated to grok.com/ on every invocation,
destroying the existing conversation URL even when --new was not passed.
Since the browser daemon already reuses the same Chrome tab, skipping
navigation lets the tab stay on the current chat thread.
- Only navigate to grok.com/ when --new is true or tab is not on grok.com
- Add tryStartFreshChat to the default path's --new branch (was dead code)
- Add isOnGrok helper with hostname-based domain matching
- Add unit tests for isOnGrok
* test(grok): add adapter to vitest project config
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
The search command defined its argument as `query` but referenced
`args.keyword`, causing the search term to be undefined.
Closes#334
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add "Why opencli?" section and comparison guide (#238)
- Add "Why opencli?" section to README.md and README.zh-CN.md
(between Highlights and Prerequisites)
- Add docs/comparison.md with 5-scenario honest evaluation
- Add Comparison entry to VitePress sidebar
* docs: refine positioning — use approximate numbers, emphasize broad coverage
- Replace specific counts (300+, 55, 20+) with approximate descriptions
- Emphasize broad global + Chinese platform coverage instead of singling out Chinese sites
- Fix Firecrawl description to mention self-hosted option
- Replace "sub-second" / "milliseconds" with accurate "seconds" / "fast deterministic"
- Add testing and AI workflow to Further Reading links
- Add "easy to extend" point to strengths
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
The favorite command was using up_mid: 0 which returns empty results. Now it correctly fetches the current user UID using getSelfUid().
Co-authored-by: 章晖 <zhanghui@MacBook-Pro.local>
* feat(linkedin): add timeline feed command
* test(linkedin): add timeline adapter unit tests
Add shape tests and utility function tests for the new timeline command.
Include linkedin in the vitest adapter project config.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
The C2 fix in PR #337 added a null-page guard after lazy-loading TS
modules, but it threw unconditionally — breaking all browser:false
commands (bloomberg, apple-podcasts, google, yollomi, etc.) that
use func() with a null page. Guard now checks updated.browser !== false.
Also fixes apple-podcasts top E2E flake: when the command times out on
CI, stderr is empty and the guard didn't catch it.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add generic `web read` command for any URL → Markdown
Adds a new `opencli web read --url <any-url>` command that fetches any
web page and exports it as clean Markdown with optional image download.
Uses browser-side DOM heuristics for content extraction:
1. <article> element
2. [role="main"] element
3. <main> element
4. Largest text-dense block fallback
Pipes through the existing article-download pipeline (Turndown + image
localization), so it inherits code block handling, frontmatter generation,
and concurrent image downloading for free.
Tested on: Anthropic blog, OpenAI blog, general news sites.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: improve web read dedup for sites with duplicated DOM paragraphs
Anthropic's blog renders each paragraph twice (a normal version + a
line-broken animation version). The previous substring-based dedup
missed these because whitespace differences changed string lengths.
Fix: compare texts after stripping ALL whitespace, and keep the
version with more proper spacing (more spaces = better formatted).
Result on Anthropic blog: 98.4KB → 53.7KB (45% reduction).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Harrison <harrison@HarrisondeMacBook-Pro.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- I1: Log pre-navigation failures in debug mode instead of silently swallowing
- I2: Validate env var timeout values, fallback on NaN/negative
- I4: Guard against indexOf returning -1 for unknown strategies in cascade
- I5: Fix shouldReplaceManifestEntry returning true for same-type entries
- I6: Prevent infinite loop in parseTsArgsBlock cursor advancement
- I7: Skip redundant Page.enable calls in CDP goto
- I8: Fix wait({time:0}) being treated as falsy
- I10: Warn when cookiesFile path doesn't exist before fallback
- I11: Sanitize tab/newline chars in cookie name/value for Netscape format
- I12: Use DEFAULT_DAEMON_PORT constant instead of hardcoded port in error
- I15: Log npm install failures in plugin lifecycle instead of swallowing
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1. execution.ts: Guard lazy-loaded func commands against null page — if a
lazy module incorrectly requires browser context, throw a clear error
instead of a cryptic TypeError on page.goto().
2. daemon.ts: Fix readBody race condition — add aborted flag to prevent
req.destroy() from triggering both reject (via error) and resolve
(via end event) on the same Promise, which could process truncated data.
3. browser/cdp.ts: Prevent CDPBridge.connect() reentry — throw if already
connected instead of silently leaking the previous WebSocket and its
message handlers.
4. interceptor.ts: Store intercept pattern in a separate global variable
so subsequent installInterceptor calls with different patterns update
the match condition without being blocked by the patchGuard.
5. record.ts: Always call cleanupEnter() after Promise.race — previously
only called in the timeout path, leaving readline open when user pressed
Enter, potentially blocking process exit. Also removed unused enterRace.
6. generate.ts: Fix undefined entering String.includes() — when c.name is
undefined, toLowerCase() returns undefined which gets coerced to the
string "undefined" by includes(), causing false positive matches.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): harden against command injection and sandbox escape
1. cli.ts: Remove auto-discover of arbitrary system binaries via denylist.
Unknown commands now require explicit registration via `opencli register`.
The previous denylist approach was trivially bypassable (bash, curl, etc.).
2. template.ts: Protect evalJsExpr against prototype chain escape.
Block expressions containing constructor/prototype/__proto__/process/etc.
Deep-copy context objects to sever prototype chains before passing to
new Function().
3. external.ts: Expand shell operator detection in parseCommand to cover
$(), $, #, \n, \r — preventing command substitution and comment injection.
4. fetch.ts: Use JSON.stringify for HTTP method in browser evaluate() instead
of raw string interpolation, preventing JS injection via crafted method values.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: harden security-sensitive execution paths
* chore: tighten template sandbox guard
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace inline sync chrome.debugger.detach() in onUpdated listener
with the shared async detach() function for consistent cleanup behavior
across all detach paths.
- Only override navigator.plugins when empty (don't replace real user
browser plugins with fakes)
- Replace Error.prepareStackTrace (V8/Node-only) with
Error.prototype.stack getter override that works in browser context
- Fix \\n escaping in template literal for stack trace split/join
- Dynamic cdc_ variable scan via getOwnPropertyNames instead of
hardcoded names
- Update tests to cover 7 patches
Add stealth.ts module that patches browser globals to hide automation
fingerprints when opencli controls a browser via CDP or daemon extension.
Patches applied:
- navigator.webdriver → undefined (CDP sets it to true)
- window.chrome stub (only if missing)
- navigator.plugins fake list (only if empty)
- navigator.languages guarantee (only if empty)
- Permissions.query normalization for notifications
- Cleanup __playwright/__puppeteer/cdc_* artifacts
CDP mode: stealth registered via Page.addScriptToEvaluateOnNewDocument
(runs before any page JS on every navigation).
Daemon mode: stealth injected via exec after navigation, with guard
flag to prevent double-injection.
- Add getErrorMessage() to errors.ts (used in 5 files)
- Add DEFAULT_DAEMON_PORT to constants.ts (used in 5 files)
- Reduces code duplication and improves maintainability
* fix: remove duplicate getErrorMessage import in discovery.ts
Squash merge left a duplicate import line causing TS2300 and oxc parse
errors in CI. Also clean up stale blank lines in discovery.ts and
execution.ts.
* fix(e2e): broaden xiaoyuzhou skip logic for overseas CI runners
The isExpectedChineseSiteRestriction function only matched FETCH_ERROR
with specific HTTP status codes. On overseas CI runners, xiaoyuzhou may
also return PARSE_ERROR (mangled HTML) or NOT_FOUND (geo-redirected
pages), causing false test failures. Now matches all CliError codes
from the adapter.
* fix(external): replace execSync with execFileSync to prevent command injection
* fix(review): preserve Windows external installs and restore docs build
* fix(review): preserve Windows external installs after rebase
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(yollomi): add new commands and update documentation in README files
- Added yollomi commands for generating images, videos, and editing capabilities.
- Updated README.md and README.zh-CN.md to include yollomi in the command list.
- Enhanced SKILL.md with yollomi-related tags and usage examples.
* feat(yollomi): add yollomi adapter to documentation
- Included yollomi in the VitePress configuration for browser adapters.
- Updated adapters index documentation to reflect yollomi's capabilities and commands.
* fix(yollomi): bug fixes, tests & improvements
- models.ts: add browser: false (no browser connection needed for hardcoded data)
- edit.ts: remove unused resolveImageInput import
- upload.ts: lower video upload limit from 100MB to 20MB (base64 OOM risk)
- generate.ts: improve file extension detection using URL.pathname
- upscale.ts: use choices for scale arg, improve extension detection
- object-remover.ts: make image/mask args positional
- Add yollomi models tests to public-commands.test.ts
- Add yollomi generate/video graceful-failure tests to browser-auth.test.ts
---------
Co-authored-by: anichikage <hanzhishuai@bytedance.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(record): add live recording command for API capture
- Add `opencli record <url>` command that injects fetch/XHR interceptors
into all tabs in the automation window, polls captured requests, and
auto-generates YAML candidate adapters
- Support multi-tab recording: new tabs discovered during polling are
automatically injected
- Add --timeout (default 60s) for agent-friendly non-blocking operation;
stops on Enter, timeout, or SIGINT — whichever comes first
- Fix idempotent re-injection: restores original fetch/XHR before
re-patching so guard flag no longer blocks subsequent record runs
- Add --poll interval option (default 2000ms)
- Expand SKILL.md with full Record Workflow section: interceptor
internals, page-type capture expectations, YAML→TS conversion guide,
and troubleshooting table
* fix(record): fix XHR listener leak, pathChain syntax error, readline hang & args interpolation
- XHR send(): add __rec_listener_added guard to prevent duplicate event
listeners when XHR is reused (abort → open → send)
- pathChain: when findArrayPath returns '' (root-level array), data access
is just 'data' not 'data?.' which was invalid JS syntax
- waitForEnter(): return cleanup fn so timeout path can close readline.Interface
preventing the process from hanging on stdin after auto-timeout
- buildRecordedYaml: replace search/page query param values with template
vars ({{args.keyword}}, {{args.page}}) so generated YAML actually uses
the declared args instead of hardcoding the recorded URL
---------
Co-authored-by: yee.wang <yee.wang@lazada.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
- docs/adapters/browser/xiaohongshu.md: fill in search command description
(was empty), update usage examples with keyword positional arg
- TESTING.md: update unit test count 31→32 (search.test.ts added in #298),
add xiaohongshu/search.test.ts to the adapter test file list
* Add weibo search command
* fix(weibo/search): correct domain to weibo.com, add browser: true, fill doc description
- Change domain from s.weibo.com to weibo.com so browser cookies are picked
up correctly (matches hot.ts which also uses weibo.com)
- Add browser: true for consistency with other browser-based adapters
- Add description for weibo search in adapter docs table
---------
Co-authored-by: 小小机器人 <14351708+little-little-robot@user.noreply.gitee.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(v2ex): add node, user, member, replies, nodes commands
Add 5 new public API commands to the v2ex adapter:
- node: browse topics by node name
- user: list topics by username
- member: show user profile
- replies: list topic replies
- nodes: list all nodes sorted by topic count
All commands use strategy: public, browser: false.
* test(v2ex): add E2E tests for node, user, member, replies, nodes commands
* docs(v2ex): update adapter docs with new commands
* fix(v2ex): address review findings - rate-limit guards, sort verification, docs
* docs(v2ex): update README command tables and add user example
* test(v2ex): improve test quality - soft guards, value assertions, smoke tests
- Replace isExpectedChineseSiteRestriction with if(code===0) soft guard
(V2EX is globally accessible; YAML fetch doesn't throw FETCH_ERROR)
- Add value assertions: member username===Livid, limit effectiveness
- Add smoke tests for node, member, replies, nodes commands
* fix(v2ex): add url field to node/user commands, add missing user smoke test
- Add url to node.yaml and user.yaml pipeline map steps and columns
(V2EX API provides item.url; improves usability for follow-up lookups)
- Add v2ex user smoke test (other 4 new commands all had smoke tests; user was missing)
- Update E2E assertions to verify url field in node/user results
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
Some environments (GUI apps, cron, IDE terminals) launch with a minimal
PATH that excludes standard directories like /usr/local/bin and /usr/sbin.
This causes external CLIs to fail when they try to run system commands
(e.g. sysctl).
Fix by ensuring standard system paths exist in process.env.PATH at
startup. This is a one-time fix that benefits ALL child processes —
isBinaryInstalled(), installExternalCli(), daemon spawn, etc. — without
needing per-call env patching.
Fixes#284
Co-authored-by: jackwener <jakevingoo@gmail.com>
* docs: add gws to External CLI Hub table in README
The Google Workspace CLI (gws) was registered in external-clis.yaml
but missing from the README table. Closes#120.
* docs: add gws to Chinese README External CLI Hub table
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
Narrow '#noteContainer img[src*="xhscdn"]' to
'#noteContainer .media-container img[src*="xhscdn"]'
to exclude user avatars and sidebar icons from downloads.
Closes#281
Expand HackerNews from 1 command to 8, covering all major HN use cases.
All YAML adapters, strategy: public, browser: false.
- new/best/ask/show/jobs: Firebase API list endpoints with deleted/dead filtering
- search: Algolia API with query + sort (relevance/date)
- user: Firebase user profile with date formatting
- top.yaml: add filter for deleted/dead items + dynamic pre-fetch limit
- E2E tests for all 7 new commands
- Update README, README.zh-CN, adapter docs
Co-authored-by: jackwener <jakevingoo@gmail.com>
turndown and @types/turndown were used in article-download.ts and
zhihu/download.test.ts but never declared in package.json, causing
CI failures on fresh npm ci installs.
Adds `opencli xiaohongshu publish` which automates posting a 图文 (image+text)
note via the creator center UI (creator.xiaohongshu.com/publish/publish).
Features:
- --title (required, max 20 chars)
- positional content argument
- --images comma-separated local file paths (jpg/png/gif/webp, max 9)
- --topics comma-separated hashtag names (without #)
- --draft flag to save as draft instead of publishing
Image upload uses DataTransfer injection into the file input element, converting
local files to base64 in Node.js and creating File blobs in the browser context.
Text fields use document.execCommand('insertText') for contenteditable editors.
Graceful debug screenshots on failure (/tmp/xhs_publish_*_debug.png).
Requires: opencli browser session logged into creator.xiaohongshu.com.
Replace fixed settleMs sleep in goto() with MutationObserver-based DOM
stability detection. The page is considered settled when no DOM mutations
occur for quietMs (default 500ms), with settleMs as a hard timeout cap.
Changes:
- Add waitForDomStableJs() shared helper to dom-helpers.ts
- Update Page.goto() and CDPPage.goto() to use smart settle
- No IPage interface changes (implementation detail only)
Key improvements over naive approach:
- Timer starts AFTER MutationObserver.observe() to avoid race condition
- Falls back to sleep(maxMs) if document.body is not available
- Monitors attributes in addition to childList/subtree
- quietMs defaults to 500ms (conservative) for async request buffering
- Add Origin header check: reject HTTP/WS from non chrome-extension:// origins
- Require X-OpenCLI custom header on all HTTP requests
- Remove Access-Control-Allow-Origin: * from all responses
- Add WebSocket verifyClient to reject malicious connections at upgrade
- Add 1MB body size limit to prevent OOM
- Update file header with security model documentation
Closes#268
- Remove setup command completely (no backward compat needed)
- Doctor now runs live connectivity test by default
- Add --no-live flag to skip if needed
- Update SKILL.md docs
- Delete setup.ts (fully redundant with doctor)
- opencli setup now prints deprecation warning and delegates to doctor
- doctor auto-starts daemon if not running (no more false 'not connected')
- Update all doc references (README, SKILL.md, docs/)
Removed from both EN/CN READMEs:
- Table of Contents (GitHub auto-generates TOC)
- Method 2: Load from npm Package (keep recommended + dev only)
- Bloomberg detailed note (too specific for README)
- Pipeline Step YAML example (developer-internal)
- Releasing New Versions (belongs in CONTRIBUTING.md)
EN README only:
- Simplified Testing section to one-liner + link to TESTING.md
Cleanup:
- Remove redundant double-retry in resolveTabId (was retrying data: URI
with the same data: URI)
- Fix stale comment (30s → 120s idle timeout)
- Remove verbose debug logging in resolveTabId
- Built extension is now smaller (16.66kB vs 17.18kB)
Extension conflict:
- Add hint to attach-failed error when chrome-extension:// URL is detected
- Add troubleshooting entry for extension conflicts (e.g. youmind, New Tab
Override) to both README.md and README.zh-CN.md
Ref: #249
When a new automation window is created, the initial tab URL may be
empty briefly while Chrome loads the data: URI. isDebuggableUrl('') was
returning false, causing ensureAttached to reject the tab.
Fix: only reject known non-debuggable URLs (chrome://, chrome-extension://).
Empty/undefined URLs are now treated as debuggable since they represent
tabs still loading.
Also adds 200ms delay after window creation to let Chrome populate the
tab URL.
CRX files cannot be installed in modern Chrome without Chrome Web Store
publishing. Updated all docs to recommend 'Load unpacked' installation
method only. Added npm package loading method as alternative.
- Removed CRX build step from build-extension.yml workflow
- Removed CRX from artifact upload and release attachment
- Updated README.md, README.zh-CN.md, browser-bridge docs (en/zh)
- Added 'Load from npm package' as installation method
Root cause: getAutomationWindow and resolveTabId used about:blank which
New Tab Override extensions intercept immediately, replacing it with
chrome-extension:// URLs that cannot be debugged.
Changes:
- Window creation: about:blank → data:text/html
- reuseTab fallback: about:blank → data:text/html
- newTab handler: about:blank → data:text/html
- Added diagnostic logging to resolveTabId for debugging
- Synced extension version to 1.2.4
Ref: #249
resolveTabId's reuseTab path now verifies the URL is actually debuggable
after navigating to about:blank. If a New Tab Override extension intercepts
it (setting it back to chrome-extension://), falls back to a data: URI,
then creates a fresh tab as last resort.
This fixes the persistent 'attach failed: Cannot access chrome-extension://'
error for users with New Tab Override extensions installed.
Ref: #249
- resolveTabId: validate URL even for explicit tabId, fall through to
auto-resolve when tab is not debuggable or has been closed
- handleNavigate: wait for URL change before checking 'complete' status
to avoid race condition with stale about:blank
- ensureAttached: pre-check tab URL, verify cached attach with probe,
invalidate cache on URL change via onUpdated listener
- daemon-client: recognize transient extension errors (disconnected,
attach failed) as retryable with 1500ms delay; fresh command ID per attempt
- pipeline executor: add per-step retry for browser steps (up to 2 retries
on transient errors); cleanup automation window on pipeline failure
- page.ts: selectTab/newTab/closeTab properly update/invalidate _tabId
- daemon.ts: add WebSocket ping/pong heartbeat (15s interval, 2-miss disconnect)
- Increase automation window idle timeout from 30s to 120s
- Fix timeout param edge cases in BrowserBridge._ensureDaemon
- Remove unused chalk import; fix trailing import placement
Closes#249
* feat(douban): add movie adapter with search, top250, subject, marks, reviews commands
- search: search movies by keyword
- top250: get top 250 movies
- subject: get movie details by id
- marks: export personal viewing marks
- reviews: export personal movie reviews
* review: resolve douban adapter blockers
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(devto): add devto adapter
* refactor(devto): improve adapters to match project conventions
- Make tag/username args positional for natural CLI usage:
opencli devto tag javascript (instead of --tag javascript)
opencli devto user ben (instead of --username ben)
- Add rank field (index + 1) matching hackernews/lobsters pattern
- Add tags field from tag_list for richer output
- Remove redundant author column from user command (already filtering by user)
- Use type: str (project convention) instead of type: string
- Increase default limit from 10 to 20 (matching other adapters)
- Update docs with positional arg examples
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(wikipedia): fix search arg name + add random and trending commands
- fix: search.ts referenced `args.keyword` but the argument is defined
as `query`, causing the search term to always be undefined
- feat: add `random` command (random article summary via REST API)
- feat: add `trending` command (most-read articles, yesterday's data)
All commands are PUBLIC strategy, no browser required, reuse wikiFetch.
* refactor(wikipedia): extract shared types + add docs for random/trending
- Extract WikiSummary, WikiMostReadArticle types to utils.ts
- Extract EXTRACT_MAX_LEN/DESC_MAX_LEN constants
- Add formatSummaryRow() helper to eliminate duplicate mapping in
summary.ts and random.ts
- Update docs with random and trending command examples
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
Support switching between For You (algorithmic) and Following
(chronological) timelines via `--type for-you|following`.
Both endpoints share the same response structure; only the GraphQL
endpoint name and queryId differ. QueryId is resolved dynamically
from fa0311/twitter-openapi with a hardcoded fallback, and validated
against /^[A-Za-z0-9_-]+$/ to prevent injection from upstream.
* fix(doctor): refresh status after live check to resolve#121
* refactor: reorder live check before status read for natural consistency
Instead of calling checkDaemonStatus() twice (before and after the
connectivity check), reorder so that the live connectivity check runs
first, then read daemon status only once. This:
- Eliminates redundant checkDaemonStatus() call
- Naturally avoids the timing inconsistency (fixes#121)
- Also fixes the sessions query using stale status
- Simplifies test assertions to avoid over-coupling to exact wording
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(google): add search, suggest, news, and trends adapters
Four new commands under `google`:
- search: browser-based DOM extraction from google.com/search
- suggest: public JSON API (suggestqueries.google.com)
- news: public RSS feed (top stories + keyword search)
- trends: public RSS feed (daily trending searches by region)
Shared RSS parser in utils.ts with attribute/CDATA support.
Unit tests for parseRssItems, E2E tests with network skip guards.
* refactor(google): downgrade search strategy from COOKIE to PUBLIC
Google search results are public data, no login needed. Browser is
required for DOM rendering, not authentication. Standalone mode
confirmed working in testing.
* fix: update test comment to reflect PUBLIC strategy
Add new YAML adapter to fetch upcoming earnings dates from xueqiu's
company events API (公司大事). Supports A-share and H-share stocks.
Features:
- Filter by subtype=2 (预计财报发布) from event timeline
- Show date, report name, and release status (⏳/✅)
- --next flag to return only the closest upcoming earnings date
- --limit to control result count
Co-authored-by: nekomoto911 <nekomoto911@gmail.com>
Browser adapters using COOKIE/HEADER strategy need the page on the target
domain so credentialed fetch() carries cookies. Previously, execution.ts
hardcoded `cmd.site === 'boss'` to skip this pre-navigation for adapters
that handle their own goto().
Now each adapter self-declares via `navigateBefore: false` on CliCommand.
This is more extensible — new sites that manage their own navigation just
add the field instead of editing execution.ts.
Changes:
- Add `navigateBefore?: boolean | string` to CliCommand interface
- Add `resolvePreNav()` helper in execution.ts (replaces hardcoded check)
- All 14 boss adapters declare `navigateBefore: false`
- Wire through discovery.ts (YAML + manifest) and build-manifest.ts
* refactor(boss): extract common utilities, fix missing login detection
- Add src/clis/boss/common.ts with shared helpers:
- bossFetch(): unified XHR template with auto cookie-expiry detection (code 7/37)
- navigateToChat()/navigateTo(): page navigation helpers
- checkAuth()/assertOk(): centralized login state validation
- fetchFriendList()/fetchRecommendList()/findFriendByUid(): data queries
- clickCandidateInList()/typeAndSendMessage(): UI automation helpers
- verbose(): conditional debug logging
- Refactor all 14 boss adapters to use common.ts:
- chatlist.ts: was missing cookie-expiry check (fixes #login-detect)
- chatmsg.ts: was missing cookie-expiry check (fixes #login-detect)
- Remaining 12 adapters: deduplicated XHR boilerplate and error handling
- Fix execution.ts: skip redundant pre-navigation for TS adapters
- TS adapters handle their own goto(), pre-navigating caused double
page loads and could trigger duplicate login prompts
- Pre-navigation preserved for YAML pipeline commands that need it
Net reduction: ~730 lines of duplicated code across boss adapters.
All 244 unit tests pass.
* fix(review): fix execution.ts pre-nav regression, sanitize UID input, restore docs
- execution.ts: use site-specific skip (boss only) instead of isYamlPipeline.
The original check skipped pre-navigation for ALL TS adapters, but weread,
chaoxing, and others don't do their own goto() and depend on it.
- common.ts: sanitize numericUid to digits-only and use JSON.stringify for
safe interpolation in page.evaluate() (prevents template literal injection).
- resume.ts: restore HTML structure doc comments (scraping selector guide).
- send.ts: restore MQTT architecture note (explains why UI automation is needed).
* fix: restore DEBUG env support in verbose(), improve skipPreNav comment
- verbose() now checks both OPENCLI_VERBOSE and DEBUG=opencli,
matching the original behavior from search.ts and detail.ts
- Clarify skipPreNav comment with TODO for future adapter-level flag
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(instagram,facebook): add write actions and extended commands
Instagram write actions (7 commands, internal REST API + CSRF token):
- like/unlike: like or unlike a user's post by username + index
- comment: comment on a user's post
- save/unsave: bookmark or remove bookmark on a post
- follow/unfollow: follow or unfollow a user
Facebook extended commands (6 commands, DOM scraping):
- friends: friend suggestions list
- groups: list your joined groups with last post time
- memories: On This Day memories
- events: browse event categories
- add-friend: send friend request by username
- join-group: join a group by ID
All commands tested with live data. 258 existing tests pass.
* docs: add adapter documentation for instagram, facebook, lobsters
* docs: add missing medium adapter documentation
* fix(extension): skip chrome-extension:// tabs in resolveTabId fallback
Remove the unsafe fallback that returned `tabs[0]` regardless of URL
type. When no web-accessible tab exists in the automation window (e.g.
a New Tab Override extension replaced about:blank with its own
chrome-extension:// page), we now always create a fresh about:blank
tab instead. This prevents chrome.debugger.attach from failing with
"Cannot access a chrome-extension:// URL of different extension".
Fixes#195, fixes#197
* refactor(extension): rename isWebUrl → isDebuggableUrl & reuse tabs in resolveTabId
Improvements over the original fix:
1. Rename isWebUrl() → isDebuggableUrl(): better reflects the intent —
the function determines whether a URL can be attached via CDP, not
just whether it's a "web" URL (about:blank is debuggable but not
really a web URL).
2. Reuse existing non-debuggable tabs: when a New Tab Override extension
replaces about:blank with chrome-extension://, use chrome.tabs.update()
to navigate the existing tab to about:blank instead of creating a new
one. This prevents orphan tab accumulation since chrome.tabs.create()
may also get intercepted by the same extension.
3. Only fall back to chrome.tabs.create() when the window has zero tabs,
which is the truly empty-window edge case.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(tiktok): add TikTok adapter with 15 commands
TikTok (15 commands, browser mode):
Read commands:
- profile: user profile info via rehydration script parsing
- search: search videos via internal search API
- explore: trending videos from explore page (DOM scraping)
- user: recent videos from a user page (DOM scraping)
- following: list accounts you follow
- friends: friend suggestions
- live: browse live streams with viewer counts
- notifications: activity notifications
Write commands (verified with real interactions):
- like/unlike: like or unlike a video by URL
- save/unsave: add or remove video from Favorites
- follow/unfollow: follow or unfollow a user
- comment: comment on a video
All write operations verified with live TikTok interactions.
* docs: add missing adapter documentation for doc-coverage CI
* feat(lobsters): add Lobste.rs adapter with hot, newest, active, tag commands
Add public API adapter for Lobste.rs (lobste.rs), a developer-focused
link aggregation community. All commands use the public JSON API and
require no authentication or browser.
Commands:
- hot: hottest stories
- newest: latest stories
- active: most active discussions
- tag: filter stories by tag (e.g. rust, security, programming)
* feat(instagram,facebook): add Instagram and Facebook adapters
Instagram (7 commands, browser mode - internal REST API):
- profile: user profile info (followers, following, posts, bio)
- search: search users
- user: recent posts from a user
- followers: list user's followers
- following: list user's following
- saved: saved posts
- explore: discover trending posts
Facebook (4 commands, browser mode - DOM scraping):
- profile: user/page profile info
- notifications: recent notifications
- feed: news feed posts
- search: search people, pages, posts
All commands require Chrome to be logged in to the respective site.
Instagram uses stable internal API endpoints with cookie auth.
Facebook uses DOM scraping via role attributes and semantic selectors.
* feat: plugin system (Stage 0-2)
- Stage 0: discoverPlugins() scans ~/.opencli/plugins/ at startup
- Stage 1: demo plugin repos (github-trending, hot-digest)
- Stage 2: opencli plugin install/uninstall/list commands
- package.json exports ./registry for TS plugin peerDep support
- 17 new/updated tests, tsc --noEmit clean
* fix: CDPBridge connect timeout unit mismatch (seconds vs ms)
opts.timeout is passed in seconds from runtime.ts but CDPBridge
was using it as milliseconds, causing instant timeout (30ms).
* feat: add registry-api public entry point for TS plugin peerDep support
- Add src/registry-api.ts: re-exports core registration API (cli, Strategy,
getRegistry) without transitive side-effects, safe for plugin imports
- Update package.json exports: './registry' -> './dist/registry-api.js'
- Update src/registry.ts: use globalThis shared registry to ensure single
instance across npm-linked plugin modules
- Update .gitignore for plugin-related artifacts
* fix: symlink host opencli into plugin node_modules on install
After npm install, replace the npm-installed @jackwener/opencli
with a symlink to the running host's package root. This ensures
TS plugins always resolve '@jackwener/opencli/registry' against
the host installation, avoiding version mismatches when the
published npm package lags behind.
* fix: transpile TS plugins to JS on install, deduplicate .ts/.js discovery
- installPlugin: after symlinking host opencli, transpile any .ts files
to .js using esbuild from the host's node_modules/.bin/
- discoverPluginDir: skip .ts files when a .js sibling exists (production
node cannot load .ts directly)
- scanPluginCommands: deduplicate basenames via Set to avoid showing
'aggregate, aggregate' when both .ts and .js exist
* docs: add plugin system user guide
- New docs/guide/plugins.md covering:
- Installation/uninstallation commands
- Creating YAML plugins (zero-dep)
- Creating TS plugins (with peerDep)
- TS plugin install lifecycle (clone → deps → symlink → transpile)
- Example plugins and troubleshooting
- Add Plugins to VitePress sidebar (EN + ZH)
- Link from getting-started.md Next Steps
* fix: address review issues in plugin system
- Security: replace execSync with execFileSync to prevent shell injection
- Replace deprecated npm --production with --omit=dev
- Tighten parseSource regex to [\w.-]+ to reject special chars
- Fix ZH sidebar plugin link (/guide/plugins → /zh/guide/plugins)
- Return plugin name from installPlugin() to avoid duplicated logic
- Use execFileSync for esbuild transpilation
- Fix misleading comment in linkHostOpencli
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(twitter): rewrite trending from YAML to TS with DOM scraping fallback
The old REST API /i/api/2/guide.json returns 503. Replace with a TS
adapter that:
- Tries legacy guide.json API first (with proper auth headers)
- Falls back to DOM scraping via [data-testid='trend'] elements
- Filters out promoted content
- Follows the same Strategy.COOKIE pattern as timeline.ts
* fix: use 'help' instead of 'description' in Arg (matches Arg interface)
* docs(steam): add adapter documentation, update READMEs
- Create docs/adapters/browser/steam.md
- Add steam entry to README.md and README.zh-CN.md
- Fixes doc-coverage CI check (44/44)
Add three new Twitter/X UI-strategy commands:
- `block` / `unblock` — block or unblock a user by username
- `hide-reply` — hide a bot/spam reply on your own tweet thread
* feat: add `opencli describe` command for unified CLI capability discovery
Add a new `describe` command that helps AI agents discover and understand
both built-in site commands and external CLI tools through a single entry point.
- Built-in commands: reads structured data from CliCommand registry
(args with type/choices/default, columns, strategy, domain)
- External CLIs: collects help text via `binary --help`, extracts
subcommand names + summaries, passes through raw help text
- Supports `--format json` for programmatic consumption by AI agents
- Graceful degradation: parse failures return raw help text, uninstalled
CLIs show install instructions without triggering auto-install
Closes#141
* fix: address code review findings for describe command
- Strip trailing colons from Cobra-style subcommand names (browse: → browse)
- Use CliError instead of bare Error for consistent error handling with hints
- Remove decorative section separators to match project comment style
- Validate --format flag (text/json only) with clear error message
- Truncate raw help output to 50 lines to prevent excessive output
- Add deduplication test for multi-section command groups
* refactor: replace describe command with enhanced --help and list --json
Per maintainer feedback, remove the standalone `describe` command and instead:
1. Enhance --help for all built-in commands:
- Show argument choices (from registry, not shown by Commander)
- Show execution metadata: Strategy / Browser / Domain
- Show output columns
2. Enhance `list -f json/yaml` with full argument schema:
- args field now includes type, required, positional, choices, default, help
- Added columns and domain fields for structured formats
- Table/csv/md formats unchanged (args remain comma-joined names)
This follows the principle that --help is the standard CLI discovery
mechanism and AI models already know to use it.
* fix: stabilize JSON schema and fix positional choices rendering
- Always output columns/domain in json/yaml ([] and null when empty)
- Use <name> instead of --name for positional args with choices
- Remove extra blank line when no choices args present
* docs: add missing adapter docs, fix sidebar 404s, add doc-check CI
- Add doc pages for 11 undocumented adapters: arxiv, barchart,
chaoxing, grok, hf, jike, jimeng, linux-do, sinafinance,
stackoverflow, weread, wikipedia
- Update adapters/index.md with all new adapter entries
- Update VitePress sidebar config with 12 new entries
- Remove broken zh/ sidebar refs (troubleshooting, testing)
- Add doc-check CI workflow (adapter coverage + build + link check)
- Add scripts/check-doc-coverage.sh for adapter doc enforcement
- Enhance PR template with adapter doc checklist
* fix(ci): use --root-dir instead of --base for lychee link checker
lychee v0.23 requires --base to be a URL or absolute path.
Use --root-dir for resolving root-relative links in local files.
* fix(ci): remove lychee link-check job, rely on VitePress build
VitePress links use extension-less paths (e.g. /adapters/browser/twitter)
which lychee cannot resolve. The docs-build job already catches all
broken internal links via VitePress dead link detection during build.
- Auto-click New Conversation if session has only 1 message
- Map Anthropic models (claude-3-7-sonnet) to Antigravity UI models
- Refactor waitForReply to check for Cancel/Stop button presence to
detect generation completion reliably, with text stability fallback
- Replace document.execCommand (deprecated) with CDP Input.insertText
- Use Input.dispatchMouseEvent to physically click + focus the Lexical editor
before text injection (fixes focus issues with JS-only .focus())
- Improve getLastAssistantReply: strip echoed user message, thinking blocks,
Copy button text, and de-duplicate repeated content artifacts
- New command: opencli antigravity serve --port 8082
- Starts HTTP server compatible with Anthropic /v1/messages API
- Connects to Antigravity via CDP (OPENCLI_CDP_ENDPOINT)
- Uses Input.dispatchKeyEvent for reliable Enter key submission
- Polls for reply with text-change detection + 3s stability check
- Precise DOM walker for extracting last assistant reply
- Lazy CDP connection (connects on first request)
- Auto-reconnect on CDP connection loss
- CORS headers for Claude Code compatibility
Usage:
OPENCLI_CDP_ENDPOINT=http://127.0.0.1:9224 opencli antigravity serve
ANTHROPIC_BASE_URL=http://localhost:8082 claude
formatPostTime() used local timezone methods, causing test failure
on UTC CI servers. XHS API timestamps are Beijing time (UTC+8),
so use explicit UTC offset with getUTC*() methods.
- wikiFetch return Promise<unknown> instead of Promise<any>
- Add WikiSearchResult type, remove r: any
- Type wikiFetch responses with inline type assertions
- Only append ... to abstract when actually truncated
Add arXiv (search, paper) and Wikipedia (search, summary) public API adapters.
- arxiv/search: search papers by keyword
- arxiv/paper: get paper details by ID
- wikipedia/search: search articles with lang support
- wikipedia/summary: get article summary
Type safety fixes applied: wikiFetch returns unknown, typed search results.
Co-authored-by: BruceLoveDecimal <39156883+BruceLoveDecimal@users.noreply.github.com>
- Remove fetchCreatorNotesByCdp() and captureNoteDetailApiPayload() raw
WebSocket code (~240 lines) — adapters should use IPage, not raw CDP
- Replace direct CDP WebSocket with IPage.evaluate() in-page fetch
- Fix page: any → IPage in all function signatures
- Simplify to two-tier fallback: API+interceptor → DOM parse
- Rebase onto latest main (resolves cdp.ts/daemon.ts conflicts)
- Change VitePress base from '/opencli/' to '/' for custom domain opencli.info
- Add docs/public/CNAME so GitHub Pages preserves custom domain on re-deploy
* docs: deduplicate documentation — single source of truth in docs/
- Remove root CDP.md, CDP.zh-CN.md, CLI-ELECTRON.md (now in docs/advanced/)
- Slim adapter READMEs to one-liner + link to docs/ (11 files)
- Update README.md adapter table links to point to docs/
* docs: set VitePress base path for GitHub Pages deployment
URLSearchParams.toString() encodes spaces as +, but Bilibili's WBI
signature verification expects %20. This mismatch causes search
queries with spaces (e.g. "亚马逊 滞销产品") to fail with
TypeError: Failed to fetch due to CORS-blocked error responses.
Fixes#125
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The build manifest includes antigravity/serve which collides with the
hardcoded antigravity serve in cli.ts. Add a guard to skip registry
entries whose subcommand already exists in the site group.
* feat(hf): add top command for hf papers (daily, weekly, monthly)
* feat(footer): add footerExtra support and derive dates from API response
Add footerExtra callback to CliCommand for custom table footer content.
For weekly/monthly periods, derive date range from API response publishedAt
field with local clock fallback.
* fix: truncate long paper titles
* refactor(hf): remove comments column for consistent output
* feat(hf): add --all flag to return all papers
* feat(hf): add paper id column to output
* fix: restore main.ts as bootstrap, sync footerExtra + CDPBridge + domain pre-nav to cli.ts
- main.ts should remain a lightweight entry point delegating to cli.ts
- Preserve CDPBridge fallback (OPENCLI_CDP_ENDPOINT) — PR had hardcoded BrowserBridge only
- Add domain pre-navigation for cookie/header strategies to cli.ts
- footerExtra feature from PR is properly integrated
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
The old adapter called `search.smzdm.com/ajax/?c=<channel>&s=<q>` which
now returns 404. This caused opencli smzdm search to always return empty
results regardless of keyword.
Fix: navigate directly to `search.smzdm.com/?c=home&s=<keyword>&v=b`
and scrape the rendered DOM via querySelectorAll('li.feed-row-wide').
Also switched from async IIFE to sync IIFE since all data is already in
the DOM after page load — no fetch needed.
Tested: opencli smzdm search --keyword A7M5 returns correct results
with prices and mall names.
Adds 'opencli boss resume --uid <uid>' command that scrapes the chat page
right panel to display candidate resume information including:
- Basic info: name, gender, age, experience, degree, active status
- Work history: time period + company + position
- Education: time period + school + major + degree
- Job being discussed and candidate expectations
Uses UI scraping approach since BOSS Zhipin does not expose a public API
for candidate resume data on the recruiter side.
Add comprehensive Jike (即刻) adapter covering read and write operations.
Read commands:
- user: user posts via m.okjike.com SSR JSON
- topic: topic/circle posts via m.okjike.com SSR JSON
- post: post detail with comments via m.okjike.com SSR JSON
- feed: home timeline via React fiber tree extraction
- search: search posts via React fiber tree extraction
- notifications: notification list via DOM innerText parsing
Write commands (Strategy.UI, browser DOM automation):
- create: publish post via inline compose box
- comment: comment on post via contenteditable paste
- like: like post via _likeButton_ div click
- repost: repost via action bar → popover menu → confirm
Implementation details:
- Three data extraction strategies: SSR JSON, React fiber, DOM manipulation
- Shared JikePost interface and getPostData helper in shared.ts
- All evaluate blocks include try/catch error handling
- Two rounds of parallel Claude + Codex code review applied
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
Replace ad-hoc string escaping with JSON.stringify() for values
interpolated into JavaScript code strings passed to page.evaluate().
- explore.ts: clickLabels were escaped with only single-quote
replacement, which breaks on labels containing backslashes or
newlines. JSON.stringify() handles all edge cases correctly.
- synthesize.ts: buildEvaluateScript() embedded URLs directly inside
single quotes. JSON.stringify() safely handles URLs containing
special characters.
- boss/chatlist: List chat conversations (招聘端聊天列表)
Uses getBossFriendListV2 API with pagination and job filter support.
- boss/chatmsg: Read chat message history with a candidate
Resolves encryptUid to numeric uid/securityId, fetches via historyMsg API.
- boss/send: Send chat message to a candidate via UI automation
BOSS chat uses MQTT protocol (not HTTP), so this command automates the
web chat UI: clicks on user in list → types in contenteditable editor →
clicks the send button.
All three commands use Strategy.COOKIE and require an active BOSS直聘
login session in Chrome.
The previous approach (nativeSetter + Enter keydown on the search input)
does not reliably trigger Twitter's form submission - the synthetic
KeyboardEvent is ignored by React, leaving the page on /explore with
zero API calls captured.
Use history.pushState + PopStateEvent instead, which triggers React
Router's listener and performs a true SPA navigation to /search.
The interceptor survives because no full page reload occurs.
Tested: "opencli", "it's a test" (single quote), "hello" all return
results with correct author attribution.
Add CLI commands to view Chaoxing assignments and exams by reusing
Chrome login session via the Browser Bridge.
Chaoxing has no flat API for listing assignments/exams. The adapter
follows the browser flow: establish session → fetch course list via
backclazzdata API → enter each course via stucoursemiddle redirect →
click tab to capture iframe URL → navigate and parse DOM.
Commands:
opencli chaoxing assignments [--course <name>] [--status] [--limit]
opencli chaoxing exams [--course <name>] [--status] [--limit]
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(intercept): use evaluate() for IIFE wrapping in installInterceptor/getInterceptedRequests
Root cause: daemon migration changed these methods from this.evaluate()
to direct sendCommand('exec'), losing the wrapForEval() IIFE wrapping.
CDP received bare arrow functions that were never invoked.
Fixes#98
* fix(twitter): SPA navigation, data path, and author resolution for INTERCEPT commands
- followers/following: install interceptor on profile page, then click
followers/following link (SPA navigation preserves JS context).
Use JSON.stringify for targetUser to prevent injection. Throw on
navigation failure. Update selector: /verified_followers.
- notifications: install interceptor on home, then pushState+popstate
to /notifications. Validate navigation URL.
- search: fix author resolution (core.screen_name, not legacy).
- All: fix GraphQL data path (remove extra .data level), update author
resolution to try core.screen_name before legacy.screen_name.
- followers: remove erroneous .filter(r => r?.url) — interceptor stores
response body JSON, URL filtering happens at capture time.
Replace eager close-window (which caused race conditions when
parallel commands shared the window) with an idle-based timer:
- Window auto-closes 30s after the last command completes
- Each incoming command resets the idle timer
- Consecutive commands reuse the same window (faster)
- No race conditions with parallel execution
- Close-window action kept for explicit cleanup if needed
- Add 'close-window' action to extension protocol and background.ts
- Add Page.closeWindow() method to send close-window command
- browserSession() now closes automation window in cleanup
- Remove domain pre-navigation + 2s wait from main.ts (CDP handles
cross-domain cookies natively, no same-origin workaround needed)
- Net effect: commands run faster, no stale windows left behind
- Delete unused extension/src/executor.ts (chrome.scripting experiment)
- Remove 15 no-op backward-compat exports from doctor.ts
- Remove getTokenFingerprint no-op from browser/index.ts
- Rename PlaywrightMCP → BrowserBridge across all source files
(backward-compat alias kept in mcp.ts and browser/index.ts)
- Remove unnecessary host_permissions from extension manifest
- Sync extension package.json version to 0.2.0
- All 14 tests pass
All opencli operations now run in a dedicated Chrome window instead
of hijacking the user's active tab. The automation window:
- Created on first command via chrome.windows.create({ focused: false })
- 1280x900 viewport, auto-cleaned up when closed
- All tabs resolved within this window only
- User's main browsing session is never touched
Tested: twitter trending ✅, zhihu hot ✅
Both commands now scroll the conversation list to load more items
before processing. Scrolls up to 20-30 times, stops after 3
consecutive scrolls with no new items loaded.
Previously limited to ~14 visible conversations, now loads as many
as needed (up to --max).
- Rewrite accept.ts: use [data-testid=conversation] click-based approach
instead of extracting href links (requests page has no /messages/xxx links)
- Support comma-separated keywords for OR matching (e.g. '群,微信')
- Add timeoutSeconds: 600 (10 min) for batch DM operations
- Bump default OPENCLI_BROWSER_COMMAND_TIMEOUT from 45s to 60s
- Track visited conversations to avoid infinite loops
Usage:
opencli twitter accept --keyword '微信' --max 20
Workflow:
1. Navigate to /messages/requests
2. Click into each conversation
3. If message contains keyword, click Accept
4. After accept (auto-redirects to /messages), go back to requests
5. Repeat until --max reached or no more matches
process.execPath is always plain 'node' even under tsx,
so .ts files could not be executed. Use --import tsx/esm
flag to enable TypeScript loading in spawned daemon.
Add weread adapter for issue #82, covering search, rankings, book details,
bookshelf, notebooks, highlights, and notes.
Public commands (no login required):
- weread search <keyword> — search books
- weread ranking [category] — book rankings (all/rising/category ID)
Private commands (cookie auth via browser):
- weread book <bookId> — book details
- weread shelf — personal bookshelf
- weread notebooks — books with highlights/notes
- weread highlights <bookId> — underlines in a book
- weread notes <bookId> — personal notes on a book
Closes#82
goto() triggers a full page navigation that resets the JS execution
context, wiping any previously injected fetch/XHR monkey-patches.
The old code installed the interceptor on x.com then navigated away,
so the interceptor was always destroyed before it could capture data.
Fix: navigate directly to the target page, install interceptor after
page load, then scroll to trigger API calls via pagination.
Also fixes the same bug in notifications.ts.
Closes#86
Exponential backoff:
- Reconnect delay: 2s, 4s, 8s, 16s, ..., capped at 60s
- Resets to base delay on successful connection
- Reduces idle CPU waste vs fixed 3s reconnect
Screenshot via CDP Page.captureScreenshot:
- New 'screenshot' action in protocol (5th action)
- Supports format (png/jpeg), quality, fullPage
- Full-page: uses Emulation.setDeviceMetricsOverride for scroll height
- CLI-side: page.screenshot() with optional file save
- Extension build: 9.81KB (+1.7KB from 8.11KB)
Inspired by bb-browser's architecture patterns.
When using CDP mode (OPENCLI_CDP_ENDPOINT), the browser page context is
the user's active tab which may be on an unrelated domain. Cookie/header
strategy commands that use fetch() with credentials: 'include' then fail
with "Failed to fetch" due to the browser's same-origin policy.
Fix: before executing cookie/header strategy commands, navigate to the
command's declared domain so the fetch runs in same-origin context.
This mirrors the pre-navigation already done in the cascade command.
Affects all cookie-strategy adapters (bilibili, twitter, zhihu, xueqiu,
etc.) when OPENCLI_CDP_ENDPOINT is enabled and the active Chrome tab is
on a different site.
Co-authored-by: kensei <backtime1993@gmail.com>
* chore(ci): add Dependabot for npm and GitHub Actions updates
- Weekly npm dependency updates with PR limit of 10
- Weekly GitHub Actions version updates with PR limit of 5
- Conventional commit prefixes (chore(deps), chore(ci))
* ci: add security audit workflow
- Run npm audit on push/PR and weekly schedule
- Fail on high-severity vulnerabilities using audit-ci
- Only audit production dependencies
* ci: add release-please for automated changelog and versioning
- Auto-generate CHANGELOG.md from Conventional Commits
- Create version bump PRs on push to main
- Works alongside existing release.yml for npm publish
* ci: add concurrency controls and Node.js version matrix
- Add concurrency groups to ci, e2e-headed, security workflows
to cancel duplicate runs on the same branch
- Test unit tests across Node 18/20/22 with fail-fast: false
- Update test step name to show Node version
* chore: bump minimum Node.js version from 18 to 20
- Update engines.node in package.json to >=20.0.0
- Update prerequisites in README.md and README.zh-CN.md
- Remove Node 18 from CI test matrix
* review: fix release token and prod-only audit scope
* docs: align Node 20 troubleshooting guidance
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
Feishu uses custom 'Lark Framework' (Chromium-based but NOT Electron).
CDP port test failed — --remote-debugging-port has no effect.
Uses AppleScript + clipboard approach (same as WeChat/ChatGPT).
Commands: status, send, read, search (Cmd+K), new (Cmd+N)
Includes adapter READMEs (EN+ZH).
- Remove feishu and wechat adapters (not tested yet, will re-add later)
- Remove their rows from README.md and README.zh-CN.md
- Significantly polish CLI-ELECTRON.md skill guide:
- Add Electron detection guide (check for Electron Framework)
- Add Non-Electron AppleScript pattern section
- Add port assignment table for all CDP adapters
- Improve code examples with real working TypeScript
* feat(xiaohongshu): add 4 creator analytics commands
Add creator backend support for Xiaohongshu (小红书), enabling
creators to access their analytics data from the command line.
New commands:
- creator-profile: account info (followers, likes, creator level)
- creator-stats: 7-day/30-day overview (views, likes, collects,
comments, shares, new followers) with daily trend data
- creator-notes: note list with per-note metrics from note manager
- creator-note-detail: single note analytics breakdown
(organic vs promoted vs video traffic)
API discovery:
- /api/galaxy/creator/home/personal_info (cookie auth, 200 OK)
- /api/galaxy/creator/data/note_detail_new (cookie auth, 200 OK)
- /api/galaxy/creator/data/note_detail?note_id=xxx (cookie auth, 200 OK)
- Note manager DOM extraction for note list (bypasses v2 signature)
All endpoints verified working with real creator account.
Screenshots (redacted) included in docs/screenshots/.
Requires: Chrome logged into creator.xiaohongshu.com
* chore: remove screenshots from repo (will host externally for PR)
* review: fix creator analytics CLI integration
Co-authored-by: stone16 <stone2paul@gmail.com>
* test: add site-scoped test runner
Co-authored-by: stone16 <stone2paul@gmail.com>
* review: ignore publish timestamps in creator note metrics
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat: add download support for images, videos, and articles
Add comprehensive download functionality to OpenCLI with support for
multiple platforms and content types.
- Add `src/download/index.ts`: HTTP download with progress, yt-dlp
wrapper for video platforms, cookie export to Netscape format for
authenticated downloads
- Add `src/download/progress.ts`: Terminal progress bars, multi-file
download tracker with status summary
- Add `src/pipeline/steps/download.ts`: New `download` pipeline step
for declarative YAML pipelines
- Register `download` step in executor.ts
- Add template filters: `slugify`, `sanitize`, `ext`, `basename` for
filename templating
- `xiaohongshu download`: Download images and videos from notes
- `bilibili download`: Download videos using yt-dlp with cookie auth
- `twitter download`: Download media from user timeline or single tweet
- `zhihu download`: Export articles to Markdown with optional image
download
```yaml
pipeline:
- download:
url: ${{ item.imageUrl }}
dir: ./downloads
filename: ${{ item.title | sanitize }}.jpg
concurrency: 5
skip_existing: true
use_ytdlp: false
type: auto # auto|image|video|document
```
- Concurrent downloads with configurable parallelism
- Progress bars with file size display
- Skip existing files option
- Cookie forwarding for authenticated downloads
- yt-dlp integration for video platforms (YouTube, Bilibili, Twitter)
- HTML to Markdown conversion for article export
- yt-dlp: Required for video downloads from streaming platforms
- ffmpeg: Optional for video format conversion
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* docs: add download support documentation
- Add Download Support section to both README.md and README.zh-CN.md
- Document supported platforms: Xiaohongshu, Bilibili, Twitter, Zhihu
- Include prerequisites (yt-dlp installation)
- Add usage examples for all download commands
- Document the `download` pipeline step for YAML adapters
- Update built-in commands table with new `download` commands
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: preserve zhihu ordered list content
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
Add support for grok.com site with two commands:
- ask: Send a message to Grok and get response
- debug: Debug grok page structure
Implementation uses Playwright CDP protocol with fallback DOM selectors
(div.message-bubble, [data-testid="message-bubble"]) for reliability.
Co-authored-by: xdord <xdord@xdorddeMac-mini.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
WeChat Mac is native Cocoa (not Electron), so CDP is not available.
Uses AppleScript + clipboard automation instead:
- status: check if WeChat is running
- send: paste + Enter in active conversation
- new: Cmd+N for new chat
- search: Cmd+F and type query
- read: Cmd+A → Cmd+C to copy chat content
Total: 30 sites · 156 commands
Add linux.do (Discourse-based forum) support with 6 YAML pipeline commands:
- hot: trending topics with period filter (all/daily/weekly/monthly/yearly)
- latest: newest topics
- categories: list all categories with slug/id for further queries
- category: browse topics within a specific category
- topic: post details with replies (first page)
- search: search topics by keyword
All commands use navigate+evaluate pattern with cookie auth
(linux.do enforces login_required on all endpoints).
Security: user inputs sanitized via | json filter + encodeURIComponent.
HTML content stripped with block-tag spacing and full entity decoding.
Add two CLI commands for Jimeng (即梦AI) — ByteDance's AI image generation platform:
- generate: Text-to-image generation with model selection and configurable wait time
- history: View recent generation history with prompt, model, status, and image URLs
Both commands use browser automation with cookie-based authentication on jimeng.jianying.com.
- Remove --remote-allow-origins from antigravity README, README.zh-CN, SKILL.md (not needed for local usage)
- Update ChatGPT README to document both AppleScript and CDP approaches
- Document ChatGPT Electron launch: /Applications/ChatGPT.app/Contents/MacOS/ChatGPT --remote-debugging-port=9224
Three public commands for Xiaoyuzhou (小宇宙) podcast platform:
- podcast <id>: view podcast profile
- podcast-episodes <id> [--limit]: list recent episodes (up to 15)
- episode <id>: view episode details
Uses __NEXT_DATA__ extraction from SSR pages, no auth required.
Includes unit tests (16), E2E tests (3), and README updates.
* feat(browser): add CDP remote connection support for server environments
This feature enables OpenCLI to connect to a Chrome browser running on a
different machine (e.g., your local computer) from a headless server
environment via Chrome DevTools Protocol (CDP).
Server environments (CI, cloud VMs, headless Linux) cannot run Chrome with
a GUI or install the Playwright MCP Bridge extension. This makes it
impossible to use OpenCLI commands that require browser authentication.
Add support for the `OPENCLI_CDP_ENDPOINT` environment variable, which
tells OpenCLI to connect to a remote Chrome instance via CDP instead of
using the local extension mode.
1. Start Chrome with remote debugging on local machine:
```
chrome --remote-debugging-port=9222 --user-data-dir="$HOME/chrome-debug"
```
2. Create SSH tunnel to forward port to server:
```
ssh -R 9222:localhost:9222 your-server
```
3. Run OpenCLI on server:
```
export OPENCLI_CDP_ENDPOINT="http://localhost:9222"
opencli bilibili hot --limit 5
```
- src/browser.ts: Add CDP endpoint detection in buildMcpArgs()
- src/doctor.ts: Show CDP mode status in doctor report
- README.md: Add "Remote Chrome (Server/Headless)" section
- README.zh-CN.md: Add corresponding Chinese documentation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* docs: extract CDP connection guide into separate files
* docs: clarify CDP vs SSH/Proxy distinction in CDP guides
* docs: restructure CDP guides into 3 distinct phases (preparation, tunnel, execution)
---------
Co-authored-by: ByteYue <yj976240184@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The flow command returned no data because:
1. The CSRF token may not be in the DOM yet when Angular is still
initializing — add a polling loop (up to 5s) to wait for it
2. The unusual_activity list is empty outside market hours — fall back
to the mostActive list which always has data
3. Remove the DOM table fallback that never matched (barchart uses
Angular components, not standard <tr> elements)
Add explicit group ordering for Vitest projects so unit tests run before e2e tests, while keeping the e2e ordering fix from PR #38.\n\nCo-authored-by: RbBtSn0w <hamiltonsnow@gmail.com>
2026-03-17 17:46:03 +08:00
866 changed files with 82478 additions and 4487 deletions
* **douyin:** repair creator draft flow — switch from broken API pipeline to UI-driven approach ([#640](https://github.com/jackwener/opencli/issues/640))
* **douyin:** support current creator API response shapes for activities, profile, collections, hashtag, videos ([#618](https://github.com/jackwener/opencli/issues/618))
* **bilibili:** distinguish login-gated subtitles from empty results ([#645](https://github.com/jackwener/opencli/issues/645))
* **facebook:** avoid in-page redirect in search — use navigate step instead of window.location.href ([#642](https://github.com/jackwener/opencli/issues/642))
* **substack:** update selectors for DOM redesign ([#624](https://github.com/jackwener/opencli/issues/624))
* **weread:** recover book details from cached shelf fallback ([#628](https://github.com/jackwener/opencli/issues/628))
* **docs:** use relative links in adapter index ([#629](https://github.com/jackwener/opencli/issues/629))
* **weibo,youtube:** add Weibo commands and YouTube channel/comments ([#418](https://github.com/jackwener/opencli/issues/418))
* **twitter:** add filter option for search ([#410](https://github.com/jackwener/opencli/issues/410))
* **extension:** add popup UI, privacy policy, and CSP for Chrome Web Store ([#415](https://github.com/jackwener/opencli/issues/415))
* add url field to 9 search adapters (67% -> 97% coverage) ([#414](https://github.com/jackwener/opencli/issues/414))
### Bug Fixes
* **extension:** improve UX when daemon is not running — show hint in popup, reduce reconnect noise ([#424](https://github.com/jackwener/opencli/issues/424))
* fix ChatWise Windows connect ([#405](https://github.com/jackwener/opencli/issues/405))
* resolve 6 critical + 11 important bugs from deep code review ([#337](https://github.com/jackwener/opencli/issues/337), [#340](https://github.com/jackwener/opencli/issues/340))
* **stealth:** harden anti-detection against advanced fingerprinting ([#357](https://github.com/jackwener/opencli/issues/357))
### Code Quality
* replace all `catch (err: any)` with typed `getErrorMessage()` across 13 files ([#412](https://github.com/jackwener/opencli/issues/412))
* adopt CliError subclasses in social and desktop adapters ([#367](https://github.com/jackwener/opencli/issues/367), [#372](https://github.com/jackwener/opencli/issues/372), [#375](https://github.com/jackwener/opencli/issues/375))
* simplify codebase with type dedup, shared analysis module, and consistent naming ([#373](https://github.com/jackwener/opencli/issues/373))
* **ci:** add cross-platform CI matrix (Linux/macOS/Windows) ([#402](https://github.com/jackwener/opencli/issues/402))
* **plugin:** add update command, hot reload after install, README section ([#307](https://github.com/jackwener/opencli/issues/307)) ([966f6e5](https://github.com/jackwener/opencli/commit/966f6e5))
* **yollomi:** add new commands and update documentation ([#235](https://github.com/jackwener/opencli/issues/235)) ([ea83242](https://github.com/jackwener/opencli/commit/ea83242))
* **record:** add live recording command for API capture ([#300](https://github.com/jackwener/opencli/issues/300)) ([dff0fe5](https://github.com/jackwener/opencli/commit/dff0fe5))
* ensure standard PATH is available for external CLIs ([#285](https://github.com/jackwener/opencli/issues/285)) ([22f5c7a](https://github.com/jackwener/opencli/commit/22f5c7a))
* **daemon:** harden security against browser CSRF attacks ([#268](https://github.com/jackwener/opencli/issues/268)) ([40bd11d](https://github.com/jackwener/opencli/commit/40bd11d))
### Performance
* smart page settle via DOM stability detection ([#271](https://github.com/jackwener/opencli/issues/271)) ([4b976da](https://github.com/jackwener/opencli/commit/4b976da))
### Refactoring
* doctor defaults to live mode, remove setup command entirely ([#263](https://github.com/jackwener/opencli/issues/263)) ([b4a8089](https://github.com/jackwener/opencli/commit/b4a8089))
* replace all about:blank with data: URI to prevent New Tab Override interception ([#257](https://github.com/jackwener/opencli/issues/257)) ([3e91876](https://github.com/jackwener/opencli/commit/3e91876))
* harden resolveTabId against New Tab Override extension interception ([#255](https://github.com/jackwener/opencli/issues/255)) ([112fdef](https://github.com/jackwener/opencli/commit/112fdef))
* **wikipedia:** fix search arg name + add random and trending commands ([#231](https://github.com/jackwener/opencli/issues/231)) ([1d56dd7](https://github.com/jackwener/opencli/commit/1d56dd7))
* resolve inconsistent doctor --live report (fix [#121](https://github.com/jackwener/opencli/issues/121)) ([#224](https://github.com/jackwener/opencli/issues/224)) ([387aa0d](https://github.com/jackwener/opencli/commit/387aa0d))
* **twitter:** add --type flag to timeline command ([#83](https://github.com/jackwener/opencli/issues/83)) ([e98cf75](https://github.com/jackwener/opencli/commit/e98cf75))
* **tiktok:** add TikTok adapter with 15 commands ([#202](https://github.com/jackwener/opencli/issues/202)) ([4391ccf](https://github.com/jackwener/opencli/commit/4391ccf))
* add Lobste.rs, Instagram, and Facebook adapters ([#199](https://github.com/jackwener/opencli/issues/199)) ([ce484c2](https://github.com/jackwener/opencli/commit/ce484c2))
* **medium:** add medium adapter ([#190](https://github.com/jackwener/opencli/issues/190)) ([06c902a](https://github.com/jackwener/opencli/commit/06c902a))
* plugin system (Stage 0-2) ([1d39295](https://github.com/jackwener/opencli/commit/1d39295))
* make primary args positional across all CLIs ([#242](https://github.com/jackwener/opencli/issues/242)) ([9696db9](https://github.com/jackwener/opencli/commit/9696db9))
* **xueqiu:** make primary args positional ([#213](https://github.com/jackwener/opencli/issues/213)) ([fb2a145](https://github.com/jackwener/opencli/commit/fb2a145))
### Refactoring
* replace hardcoded skipPreNav with declarative navigateBefore field ([#208](https://github.com/jackwener/opencli/issues/208)) ([a228758](https://github.com/jackwener/opencli/commit/a228758))
* type discovery core ([#219](https://github.com/jackwener/opencli/issues/219)) ([bd274ce](https://github.com/jackwener/opencli/commit/bd274ce))
* type browser core ([#218](https://github.com/jackwener/opencli/issues/218)) ([28c393e](https://github.com/jackwener/opencli/commit/28c393e))
* type pipeline core ([#217](https://github.com/jackwener/opencli/issues/217)) ([8a4ea41](https://github.com/jackwener/opencli/commit/8a4ea41))
* reduce core any usage ([#216](https://github.com/jackwener/opencli/issues/216)) ([45cee57](https://github.com/jackwener/opencli/commit/45cee57))
* fail fast on invalid pipeline steps ([#237](https://github.com/jackwener/opencli/issues/237)) ([c76f86c](https://github.com/jackwener/opencli/commit/c76f86c))
* add antigravity serve command — Anthropic API proxy ([35a0fed](https://github.com/jackwener/opencli/commit/35a0fed8a0c1cb714298f672c19f017bbc9a9630))
* add arxiv and wikipedia adapters ([#132](https://github.com/jackwener/opencli/issues/132)) ([3cda14a](https://github.com/jackwener/opencli/commit/3cda14a2ab502e3bebfba6cdd9842c35b2b66b41))
* add external CLI hub for discovery, auto-installation, and execution of external tools. ([b3e32d8](https://github.com/jackwener/opencli/commit/b3e32d8a05744c9bcdfef96f5ff3085ac72bd353))
* **boss:** add 8 new recruitment management commands ([#133](https://github.com/jackwener/opencli/issues/133)) ([7e973ca](https://github.com/jackwener/opencli/commit/7e973ca59270029f33021a483ca4974dc3975d36))
* **serve:** implement auto new conv, model mapping, and precise completion detection ([0e8c96b](https://github.com/jackwener/opencli/commit/0e8c96b6d9baebad5deb90b9e0620af5570b259d))
* **serve:** use CDP mouse click + Input.insertText for reliable message injection ([c63af6d](https://github.com/jackwener/opencli/commit/c63af6d41808dddf6f0f76789aa6c042f391f0b0))
* **docs:** use base '/' for custom domain and add CNAME file ([#129](https://github.com/jackwener/opencli/issues/129)) ([2876750](https://github.com/jackwener/opencli/commit/2876750891bc8a66be577b06ead4db61852c8e81))
* **serve:** update model mappings to match actual Antigravity UI ([36bc57a](https://github.com/jackwener/opencli/commit/36bc57a9624cdfaa50ffb2c1ad7f9c518c5e6c55))
* type safety for wikiFetch and arxiv abstract truncation ([4600b9d](https://github.com/jackwener/opencli/commit/4600b9d46dc7b56ff564c5f100c3a94c6a792c06))
* use UTC+8 for XHS timestamp formatting (CI timezone fix) ([03f067d](https://github.com/jackwener/opencli/commit/03f067d90764487f0439705df36e1a5c969a7f98))
* **xiaohongshu:** use fixed UTC+8 offset in trend timestamp formatting (CI timezone fix) ([593436e](https://github.com/jackwener/opencli/commit/593436e4cb5852f396fbaaa9f87ef1a0b518e76d))
* use %20 instead of + for spaces in Bilibili WBI signed requests ([#126](https://github.com/jackwener/opencli/issues/126)) ([4cabca1](https://github.com/jackwener/opencli/commit/4cabca12dfa6ca027b938b80ee6b940b5e89ea5c)), closes [#125](https://github.com/jackwener/opencli/issues/125)
# 5. Link globally (optional, for testing `opencli` command)
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.
### YAML 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 [CLI-EXPLORER.md](./CLI-EXPLORER.md) if you need the full adapter workflow.
### Validate Your Adapter
```bash
# Validate YAML syntax and schema
opencli validate
# Test your command
opencli <site> <command> --limit 3 -f json
# Verbose mode for debugging
opencli <site> <command> -v
```
## Arg Design Convention
Use **positional** for the primary, required argument of a command (the "what" — query, symbol, id, url, username). Use **named options** (`--flag`) for secondary/optional configuration (limit, format, sort, page, filters, language, date).
**Rule of thumb**: Think about how the user will type the command. `opencli xueqiu stock SH600519` is more natural than `opencli xueqiu stock --symbol SH600519`.
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
The OpenCLI Browser Extension is a bridge between the [OpenCLI](https://github.com/jackwener/opencli) command-line tool and your Chrome browser. It receives commands from a **locally running daemon** process via WebSocket (`localhost` only) and executes them in **isolated Chrome windows** that are separate from your normal browsing session.
## Data collection
The extension does **NOT** collect, store, transmit, or sell any personal data. Specifically:
- **No analytics or telemetry** — no data is sent to any remote server.
- **No user tracking** — no cookies, identifiers, or fingerprints are created.
- **No external network requests** — all communication is strictly `localhost` (WebSocket to `ws://localhost:19825`).
## Permissions explained
| Permission | Why it's needed |
|------------|----------------|
| `debugger` | Required to use Chrome DevTools Protocol (CDP) for browser automation — executing JavaScript, capturing page content, and taking screenshots in isolated windows. |
| `tabs` | Required to create and manage isolated automation windows and tabs, separate from the user's browsing session. |
| `cookies` | Required to read site-specific cookies (scoped by domain) so CLI commands can authenticate with websites the user is already logged into. Cookies are **never written, modified, or transmitted externally**. |
| `activeTab` | Required to identify the currently active tab for context-aware commands. |
| `alarms` | Required to maintain the WebSocket connection to the local daemon via periodic keepalive checks. |
## Data flow
```
User's terminal (opencli CLI)
↓ (spawns)
Local daemon process (localhost:19825)
↓ (WebSocket, localhost only)
Chrome Extension (this extension)
↓ (Chrome APIs)
Isolated Chrome automation window
```
All data stays on the user's machine. No data leaves `localhost`.
## Cookie access
The extension reads cookies **only** when explicitly requested by a CLI command, and **only** for the specific domain the command targets. It cannot and does not dump all cookies. Cookie data is returned to the local daemon process and is never sent to any external server.
## Third-party services
This extension does not integrate with, send data to, or receive data from any third-party service.
## Open source
This extension is fully open source. You can audit the complete source code at:
A CLI tool that turns **any website** into a command-line interface — Bilibili, Zhihu, 小红书, Twitter/X, Reddit, YouTube, and [many more](#built-in-commands) — powered by browser session reuse and AI-native discovery.
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.
---
**Built for AI Agents** — Configure an instruction in your `AGENT.md` or `.cursorrules` to run `opencli list` via Bash. The AI will automatically discover and invoke all available tools.
## Table of Contents
**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).
- [Highlights](#highlights)
- [Prerequisites](#prerequisites)
- [Quick Start](#quick-start)
- [Built-in Commands](#built-in-commands)
- [Output Formats](#output-formats)
- [For AI Agents (Developer Guide)](#for-ai-agents-developer-guide)
- [Testing](#testing)
- [Troubleshooting](#troubleshooting)
- [Releasing New Versions](#releasing-new-versions)
- [License](#license)
**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.
---
## Highlights
- **CLI All Electron** — CLI-ify apps like Antigravity Ultra! Now AI can control itself natively.
- **Account-safe** — Reuses Chrome's 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.
- **Self-healing setup** — `opencli setup` auto-discovers tokens; `opencli doctor` diagnoses config across 10+ tools; `--fix` repairs them all.
- **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.
- **Dual-Engine Architecture** — Supports both YAML declarative data pipelines and robust browser runtime TypeScript injections.
## Prerequisites
## Why opencli?
- **Node.js**: >= 18.0.0
- **Chrome** running **and logged into the target site** (e.g. bilibili.com, zhihu.com, xiaohongshu.com).
There are many great browser automation tools. Here's when opencli is the right choice:
> **⚠️ Important**: Browser commands reuse your Chrome login session. You must be logged into the target website in Chrome before running commands. If you get empty data or errors, check your login status first.
| Your need | Best tool | Why |
|-----------|-----------|-----|
| Scheduled data extraction from specific sites | **opencli** | Pre-built adapters, deterministic JSON, zero LLM cost |
| AI agent needs reliable site operations | **opencli** | Hundreds of commands, structured output, fast deterministic response |
| Explore an unknown website ad-hoc | Browser-Use, Stagehand | LLM-driven general browsing for one-off tasks |
| Large-scale web crawling | Crawl4AI, Scrapy | Purpose-built for throughput and scale |
| Control desktop Electron apps from terminal | **opencli** | CDP + AppleScript — the only CLI tool that does this |
OpenCLI connects to your browser through the Playwright MCP Bridge extension.
**What makes opencli different:**
### Playwright MCP Bridge Extension Setup
- **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** — 50+ sites across global and Chinese platforms (Bilibili, Zhihu, Xiaohongshu, Reddit, HackerNews, and more), plus desktop Electron apps via CDP.
1. Install **[Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm)** extension in Chrome.
2. Run `opencli setup` — discovers the token, distributes it to your tools, and verifies connectivity:
> For a detailed comparison with Browser-Use, Crawl4AI, Firecrawl, and others, see the [Comparison Guide](./docs/comparison.md).
```bash
opencli setup
```
The interactive TUI will:
- 🔍 Auto-discover `PLAYWRIGHT_MCP_EXTENSION_TOKEN` from Chrome (no manual copy needed)
- ☑️ Show all detected tools (Codex, Cursor, Claude Code, Gemini CLI, etc.)
- ✏️ Update only the files you select (Space to toggle, Enter to confirm)
- 🔌 Auto-verify browser connectivity after writing configs
> **Tip**: Use `opencli doctor` for ongoing diagnosis and maintenance:
> ```bash
> opencli doctor # Read-only token & config diagnosis
> opencli doctor --live # Also test live browser connectivity
> opencli doctor --fix # Fix mismatched configs (interactive)
> opencli doctor --fix -y # Fix all configs non-interactively
> ```
<details>
<summary>Manual setup (alternative)</summary>
Add token to your MCP client config (e.g. Claude/Cursor):
opencli hackernews top --limit 5# Public API, no browser
opencli bilibili hot --limit 5# Browser command
opencli zhihu hot -f json # JSON output
opencli zhihu hot -f yaml # YAML output
opencli doctor # Check extension + daemon connectivity
opencli daemon status # Check daemon state (PID, uptime, memory)
```
### Install from source (for developers)
**Try it out:**
```bash
git clone git@github.com:jackwener/opencli.git
cdopencli
npm install
npm run build
npm link # Link binary globally
opencli list # Now you can use it anywhere!
opencli list # See all commands
opencli hackernews top --limit 5# Public API, no browser needed
opencli bilibili hot --limit 5# Browser command (requires Extension)
```
### Update
@@ -132,110 +90,182 @@ opencli list # Now you can use it anywhere!
npm install -g @jackwener/opencli@latest
```
---
### For Developers
**Install from source**
```bash
git clone git@github.com:jackwener/opencli.git &&cd opencli && npm install && npm run build && npm link
```
**Load Source Browser Bridge Extension**
1. Open `chrome://extensions` and enable **Developer mode** (top-right toggle).
2. Click **Load unpacked** and select the `extension/` directory from this repository.
---
## Prerequisites
- **Node.js**: >= 20.0.0 (or **Bun** >= 1.0)
- **Chrome** running **and logged into the target site** (e.g. bilibili.com, zhihu.com, xiaohongshu.com).
> **⚠️ Important**: Browser commands reuse your Chrome login session. You must be logged into the target website in Chrome before running commands. If you get empty data or errors, check your login status first.
## Built-in Commands
**19 sites · 80+ commands** — run `opencli list` for the live registry.
66+ adapters in total — **[→ see all supported sites & commands](./docs/adapters/index.md)**
## 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).
| **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"` |
| [opencli-plugin-juejin](https://github.com/Astro-Han/opencli-plugin-juejin) | YAML | 稀土掘金 (Juejin) hot articles |
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 [CLI-ONESHOT.md](./CLI-ONESHOT.md) — just a URL + one-line goal, 4 steps done.
> **Full mode**: Before writing any adapter code, read [CLI-EXPLORER.md](./CLI-EXPLORER.md). It contains the complete browser exploration workflow, the 5-tier authentication strategy decision tree, and debugging guide.
opencli cascade https://api.example.com/data # Auto-probe: PUBLIC → COOKIE → HEADER
```
Explore outputs to `.opencli/explore/<site>/` (manifest.json, endpoints.json, capabilities.json, auth.json).
## Testing
See **[TESTING.md](./TESTING.md)** for the full testing guide, including:
- Current test coverage (unit + E2E tests across 19 sites)
- How to run tests locally
- How to add tests when creating new adapters
- CI/CD pipeline with sharding
- Headless browser mode (`OPENCLI_HEADLESS=1`)
```bash
# Quick start
npm run build
npx vitest run # All tests
npx vitest run src/ # Unit tests only
npx vitest run tests/e2e/ # E2E tests
```
See **[TESTING.md](./TESTING.md)** for how to run and write tests.
## Troubleshooting
- **"Failed to connect to Playwright MCP Bridge"**
- Ensure the Playwright MCP extension is installed and **enabled** in your running Chrome.
- Restart the Chrome browser if you just installed the extension.
- **Empty data returns or 'Unauthorized' error**
- Your login session in Chrome might have expired. Open a normal Chrome tab, navigate to the target site, and log in or refresh the page to prove you are human.
- **Node API errors**
- Make sure you are using Node.js >= 18. Some dependencies require modern Node APIs.
- **Token issues**
- Run `opencli doctor` to diagnose token configuration across all tools.
- **"Extension not connected"** — Ensure the Browser Bridge extension is installed 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 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.
3.Run `opencli setup` to auto-discover token and configure all tools
2.**opencli Browser Bridge** Chrome extension installed (load `extension/` as unpacked in `chrome://extensions`)
3.No further setup needed — the daemon auto-starts on first browser command
> **Note**: You must be logged into the target website in Chrome before running commands. Tabs opened during command execution are auto-closed afterwards.
Public API commands (`hackernews`,`github search`,`v2ex`) need no browser.
Public API commands (`hackernews`, `v2ex`) need no browser.
## Commands Reference
@@ -48,7 +54,7 @@ Public API commands (`hackernews`, `github search`, `v2ex`) need no browser.
Read posts, comments, and notifications from [Band](https://www.band.us), a private community platform. Authentication uses your logged-in Chrome session (cookie-based).
## Commands
| Command | Description |
|---------|-------------|
| `opencli band bands` | List all Bands you belong to |
| `opencli band posts <band_no>` | List posts from a Band |
| `opencli band post <band_no> <post_no>` | Export full post content including nested comments |
| `opencli band mentions` | Show notifications where you were @mentioned |
## Usage Examples
```bash
# List all your bands (get band_no from here)
opencli band bands
# List recent posts in a band
opencli band posts 12345678 --limit 10
# Export a post with comments
opencli band post 12345678987654321
# Export post body only (skip comments)
opencli band post 12345678987654321 --comments false
# Export post and download attached photos
opencli band post 12345678987654321 --output ./band-photos
# Show recent @mention notifications
opencli band mentions --limit 20
# Show only unread mentions
opencli band mentions --unread true
# Show all notification types
opencli band mentions --filter all
```
### `band mentions` filter options
| Filter | Description |
|--------|-------------|
| `mentioned` | Only notifications where you were @mentioned (default) |
| `all` | All notifications |
| `post` | Post-related notifications |
| `comment` | Comment-related notifications |
## Prerequisites
- Chrome running and **logged into** [band.us](https://www.band.us)
- a Chrome session that can already access the target Bloomberg article page
- the [Browser Bridge extension](/guide/browser-bridge)
## Notes
- RSS commands support `--limit` with a maximum of 20 items.
- If `bloomberg news` fails on a page from RSS, try a different standard story/article link first; not every Bloomberg URL in feeds is a normal article page.
-`publish` requires `--schedule` to be at least 2 hours later and no more than 14 days later
-`draft` and `publish` upload the video through Douyin/ByteDance browser-authenticated APIs, so cookies in the active browser session must be valid
-`hashtag suggest` expects a valid `cover`/`cover_uri` value produced during the publish pipeline; for normal manual use, `hashtag search` and `hashtag hot` are usually more convenient
opencli grok ask --prompt "Write a long essay" --web --timeout 180
```
### Options
| Option | Description |
|--------|-------------|
| `--prompt` | The message to send (required) |
| `--timeout` | Wait timeout in seconds (default: 120) |
| `--new` | Start a new chat before sending (default: false) |
| `--web` | Opt into the explicit grok.com consumer web flow (default: false) |
## Behavior
-`opencli grok ask` keeps the upstream/default behavior intact.
-`opencli grok ask --web` switches to the newer hardened consumer-web implementation.
- The `--web` path adds stricter composer detection, clearer blocked/session-gated hints, and waits for a stabilized assistant bubble before returning.
## Prerequisites
- The Grok adapter still depends on browser-backed access to `grok.com`
- For `--web`, Chrome should already be running with an authenticated Grok consumer session
List forum categories with optional sub-category expansion.
```bash
opencli linux-do categories
opencli linux-do categories --subcategories
opencli linux-do categories --limit 50
```
When `--subcategories` is enabled, sub-categories are rendered as `Parent / Child` so the `name` value can be copied directly into `opencli linux-do feed --category ...`.
- NotebookLM accessible in the current browser session
## Notes
- Notebook-oriented commands assume you already have the target notebook open in Chrome, or that `opencli notebooklm use` can bind an existing notebook tab into `site:notebooklm`.
-`list`, `get`, `source-list`, `history`, `source-fulltext`, and `source-guide` prefer NotebookLM RPC paths and fall back only when the richer path is unavailable.
-`notes-get` currently reads note content only from the visible Studio note editor; if the note is listed but not open, open it in NotebookLM first and then retry.
AI image/video generation and editing on [yollomi.com](https://yollomi.com). Uses the same `/api/ai/*` routes as the web app; authentication is your **logged-in Chrome session** (NextAuth cookies).
🔥 **CLI All Electron Apps! The Most Powerful Update Has Arrived!** 🔥
Turn your local Antigravity desktop application into a programmable AI node via Chrome DevTools Protocol (CDP). This allows you to compose complex LLM workflows entirely through the terminal by manipulating the actual UI natively, bypassing any API restrictions.
## Prerequisites
Start the Antigravity desktop app with the Chrome DevTools `remote-debugging-port` flag:
Check the Chromium CDP connection. Returns the current window title and active internal URL.
### `opencli antigravity send <message>`
Send a text prompt to the AI. Automatically locates the Lexical editor input box, types the prompt securely, and hits Enter.
### `opencli antigravity read`
Scrape the entire current conversation history block as pure text.
### `opencli antigravity new`
Click the "New Conversation" button to instantly clear the UI state and start fresh.
### `opencli antigravity dump`
Dump the current DOM and snapshot artifacts to `/tmp` for reverse-engineering and selector debugging.
### `opencli antigravity extract-code`
Extract any multi-line code blocks from the current conversation view. Ideal for automated script extraction (e.g. `opencli antigravity extract-code > script.sh`).
### `opencli antigravity model <name>`
Quickly target and switch the active LLM engine. Example: `opencli antigravity model claude` or `opencli antigravity model gemini`.
### `opencli antigravity watch`
A long-running, streaming process that continuously polls the Antigravity UI for chat updates and outputs them in real-time to standard output.
> The CDP approach is primarily for advanced automation and future desktop-only commands. The built-in command set above still works in the default AppleScript path unless you explicitly route through `OPENCLI_CDP_ENDPOINT`.
## How It Works
- **AppleScript mode**: Uses `osascript` to control ChatGPT, `pbcopy`/`pbpaste` to paste prompts, and the macOS Accessibility tree to read visible chat messages.
- **CDP mode**: Connects via Chrome DevTools Protocol to the Electron renderer process.
Control the **ChatWise Desktop App** from the terminal via Chrome DevTools Protocol (CDP). ChatWise is an Electron-based multi-LLM client supporting GPT-4, Claude, Gemini, and more.
Control the **OpenAI Codex Desktop App** headless or headfully via Chrome DevTools Protocol (CDP). Because Codex is built on Electron, OpenCLI can directly drive its internal UI, automate slash commands, and manipulate its AI agent threads.
## Prerequisites
1. You must have the official OpenAI Codex app installed.
2. Launch it via the terminal and expose the remote debugging port:
Control the **Cursor IDE** from the terminal via Chrome DevTools Protocol (CDP). Since Cursor is built on Electron (VS Code fork), OpenCLI can drive its internal UI, automate Composer interactions, and manipulate chat sessions.
| `opencli doubao-app ask "message"` | Send a prompt and wait for the reply |
| `opencli doubao-app screenshot` | Capture a screenshot of the app window |
| `opencli doubao-app dump` | Export DOM and snapshot debug info |
## How It Works
Connects to the Doubao Electron app via CDP, injecting JavaScript into the renderer process to control the chat UI — sending messages, reading replies, and capturing screenshots.
## Limitations
- Requires Doubao Desktop to be launched with `--remote-debugging-port`
- macOS / Linux / Windows (Electron-based, platform independent)
| **[hackernews](./browser/hackernews)** | `top``new``best``ask``show``jobs``search``user` | 🌐 Public |
| **[bbc](./browser/bbc)** | `news` | 🌐 Public |
| **[devto](./browser/devto)** | `top``tag``user` | 🌐 Public |
| **[dictionary](./browser/dictionary)** | `search``synonyms``examples` | 🌐 Public |
| **[apple-podcasts](./browser/apple-podcasts)** | `search``episodes``top` | 🌐 Public |
| **[xiaoyuzhou](./browser/xiaoyuzhou)** | `podcast``podcast-episodes``episode` | 🌐 Public |
| **[yahoo-finance](./browser/yahoo-finance)** | `quote` | 🌐 Public |
| **[arxiv](./browser/arxiv)** | `search``paper` | 🌐 Public |
| **[paperreview](./browser/paperreview)** | `submit``review``feedback` | 🌐 Public |
| **[barchart](./browser/barchart)** | `quote``options``greeks``flow` | 🌐 Public |
| **[hf](./browser/hf)** | `top` | 🌐 Public |
| **[sinafinance](./browser/sinafinance)** | `news` | 🌐 Public |
| **[spotify](./browser/spotify)** | `auth``status``play``pause``next``prev``volume``search``queue``shuffle``repeat` | 🔑 OAuth API |
| **[stackoverflow](./browser/stackoverflow)** | `hot``search``bounties``unanswered` | 🌐 Public |
| **[wikipedia](./browser/wikipedia)** | `search``summary``random``trending` | 🌐 Public |
| **[lobsters](./browser/lobsters)** | `hot``newest``active``tag` | 🌐 Public |
| **[steam](./browser/steam)** | `top-sellers` | 🌐 Public |
## Desktop Adapters
| App | Description | Commands |
|-----|-------------|----------|
| **[Cursor](./desktop/cursor)** | Control Cursor IDE | `status``send``read``new``dump``composer``model``extract-code``ask``screenshot``history``export` |
# Connecting OpenCLI via CDP (Remote/Headless Servers)
If you cannot use the opencli Browser Bridge extension (e.g., in a remote headless server environment without a UI), OpenCLI provides an alternative: connecting directly to Chrome via **CDP (Chrome DevTools Protocol)**.
Because CDP binds to `localhost` by default for security reasons, accessing it from a remote server requires an additional networking tunnel.
This guide is broken down into three phases:
1.**Preparation**: Start Chrome with CDP enabled locally.
2.**Network Tunnels**: Expose that CDP port to your remote server using either **SSH Tunnels** or **Reverse Proxies**.
3.**Execution**: Run OpenCLI on your server.
---
## Phase 1: Preparation (Local Machine)
First, you need to start a Chrome browser on your local machine with remote debugging enabled.
> **Note**: The `--remote-allow-origins="*"` flag is often required for modern Chrome versions to accept cross-origin CDP WebSocket connections (e.g. from reverse proxies like ngrok).
Once this browser instance opens, **log into the target websites you want to use** (e.g., bilibili.com, zhihu.com) so that the session contains the correct cookies.
---
## Phase 2: Remote Access Methods
Once CDP is running locally on port `9222`, you must securely expose this port to your remote server. Choose one of the two methods below depending on your network conditions.
### Method A: SSH Tunnel (Recommended)
If your local machine has SSH access to the remote server, this is the most secure and straightforward method.
Run this command on your **Local Machine** to forward the remote server's port `9222` back to your local port `9222`:
> *Tip: If you provide a standard HTTP/HTTPS CDP endpoint, OpenCLI requests the `/json` target list and picks the most likely inspectable app/page target automatically. If multiple app targets exist, you can further narrow selection with `OPENCLI_CDP_TARGET` (for example `antigravity` or `codex`).*
If you plan to use this setup frequently, you can persist the environment variable by adding the `export` line to your `~/.bashrc` or `~/.zshrc` on the server.
The `download` step can be used in YAML pipelines:
::: v-pre
```yaml
pipeline:
- fetch:https://api.example.com/media
- download:
url:${{ item.imageUrl }}
dir:./downloads
filename:${{ item.title | sanitize }}.jpg
concurrency:5
skip_existing:true
```
:::
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.