* feat(hupu): add hupu cli adapter
* fix(hupu): prevent detail from returning the wrong thread
* refactor: deduplicate shared utilities in hupu adapter
- Merge postHupuJson and postHupuReplyJson into single function with mode parameter
- Move stripHtml and decodeHtmlEntities to utils.ts, remove duplicate definitions
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat: 推特新增回复图片能力支持本地路径和网络路径
* fix(twitter/reply): fix image upload fallback, restore execCommand, add size limit
- Fix attachReplyImage fallback: use uploaded flag instead of checking
page.setFileInput existence, so base64 fallback actually runs when
CDP setFileInput throws "Unknown action"
- Restore execCommand('insertText') as primary text input method for
Twitter's Draft.js editor, with paste event as fallback
- Add 20MB size limit for remote image downloads to prevent OOM
- Remove unsafe buttons[0] fallback that could click invisible buttons
* fix(twitter/reply): add local image size check and base64 fallback warning
Local images were not validated for size — a 100MB file would fail only
at upload time. Remote images already had MAX_IMAGE_SIZE_BYTES checks.
Also add a console.warn when using the base64 fallback with large
payloads, consistent with xiaohongshu/publish.ts behavior.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(xiaoe): add 小鹅通 (Xiaoe-tech) student platform adapter
Add 5 YAML adapters for 小鹅通 (xiaoe-tech.com), the leading Chinese
online education platform:
- courses: list purchased courses with URLs and shop names
- detail: course info (name, price, user count, shop)
- catalog: full course outline supporting normal courses (type 50),
columns (type 6), and big columns (type 8)
- play-url: get M3U8 play URL via direct API for video courses,
and Vue component tree search + Performance API polling for
live replay courses
- content: extract rich-text page content as plain text
Technical notes:
- Strategy: cookie (reuses Chrome login session)
- Framework: Vue 2 + Vuex Store (SPA)
- Video courses use a two-step API chain:
detail_info.get → play_sign → getPlayUrl → M3U8
- Live replays use Performance API + Vue data tree polling
- Catalog expands chapters via Vue component method getSecitonList()
- Supports multiple stores (cross-domain cookie sharing via
study.xiaoe-tech.com)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* review: stop truncating xiaoe content
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix: add -v/--verbose to explore, record, generate, cascade
Built-in browser commands were registered directly in cli.ts and
missed the -v/--verbose flag that commanderAdapter.ts wires up for
adapter commands. Also switch explore's lone log.debug() call to
log.verbose() so the flag has visible effect.
Closes#716
* refactor(cli): make builtin command wiring testable
* refactor(cli): simplify verbose wiring, use normal Commander pattern
Replace registerVerboseAction wrapper with simple applyVerbose() helper.
The wrapper broke Commander's builder chain and created awkward
indentation. Now each command uses standard .option().action() with
applyVerbose(opts) as the first line — easier to read and maintain.
* fix(cli): add -v/--verbose to doctor and synthesize commands
These commands were also missing verbose support, same root cause as
explore/record/generate/cascade — registered directly in cli.ts,
bypassing commanderAdapter's automatic -v wiring.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(twitter): add --images flag to post command
Support attaching up to 4 images when posting tweets via
`opencli twitter post "text" --images /path/a.png,/path/b.jpg`.
Uses the existing CDP DOM.setFileInputFiles mechanism (page.setFileInput)
to inject files into Twitter's file input. Includes proper file validation,
graceful error handling for older extensions, and polling-based upload
readiness detection instead of fixed delays.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(twitter): use attachments DOM signal for upload detection, add tests
Replace unreliable tweet-button-only polling with dual-condition check:
wait for [data-testid="attachments"] with correct [role="group"] count
AND button enabled. Increase timeout to 30s. Add 8 unit tests covering
image upload flow, file validation, and error paths.
Addresses PR #666 review feedback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(twitter): use top-level imports, fix test mocks, faster upload poll
- Use top-level fs/path imports instead of dynamic imports inside func
- Fix test statSync mock to return undefined (not null) for missing files
- Fix test path mock to preserve other exports via importOriginal
- Fix null type error in no-browser-session test
- Reduce upload poll interval from 1s to 500ms for faster detection
- Use JSON.stringify for imageCount interpolation for consistency
* refactor(twitter): extract validation, fail-fast, reduce duplication
- Extract validateImagePaths() with extension validation (jpg/png/gif/webp)
matching xiaohongshu publish pattern
- Validate images before browser navigation (fail-fast on bad input)
- Remove try/catch wrapper around setFileInput — let errors propagate
naturally instead of masking the original error
- Deduplicate tweetButton/tweetButtonInline lookups using fallback OR
- Use constants for MAX_IMAGES, UPLOAD_POLL_MS, UPLOAD_TIMEOUT_MS
- Add tests: unsupported format, validates-before-navigating
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(gemini): stabilize ask reply state handling
* fix: use CommandExecutionError for composer failures and clean up formatting
- Replace raw Error with CommandExecutionError for Node-side composer
failures (prepareComposer, insertText) to match adapter error conventions
- Remove extra blank lines after __test__ export
* refactor: remove dead code and add Chinese sign-in label
- Remove unused areGeminiTurnsEqual and areGeminiLinesEqual functions
- Add Chinese sign-in label (登录) to sign-in detection for consistency
with other Chinese labels already added in this PR
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
Move data processing (HTML stripping, answer mapping) from browser-side
evaluate to Node-side, keeping the evaluate minimal: just fetch + status
check. Uses __httpError sentinel consistent with pixivFetch convention.
- Remove hover_price_text as MOQ source in search normalizeSearchCandidate
to prevent price fields from being misinterpreted as MOQ data
- Rename firstLine() to firstWord() to match its actual behavior (splits
by whitespace, not newlines)
- Add missing "单" unit to item.ts extractSalesText regex
- Add test case verifying hover_price_text is not used for MOQ
* fix(zhihu): make question runtime-compatible
* fix: validate questionId is numeric to prevent interpolation issues
* refactor: simplify evaluate string and harden against injection
- Build URL in Node.js, embed via JSON.stringify for safety-by-design
- Remove unnecessary (page as any) cast — IPage already has evaluate
- Simplify error message construction (no nested ternaries)
- Replace implementation-detail test with numeric ID validation test
* refactor: simplify zhihu question — move stripHtml into evaluate, return clean data
* fix: add colon separator in fetch error message for readability
"request failed Failed to fetch" → "request failed: Failed to fetch"
---------
Co-authored-by: Kyrie <kyrie@mallab.world>
Co-authored-by: jackwener <jakevingoo@gmail.com>
1. marks: correct pageSize from 30 to 15 — douban grid mode shows 15
items per page, causing pagination to stop after the first page.
2. subject: split title/originalTitle correctly — v:itemreviewed contains
both Chinese and original titles concatenated.
3. subject: extract country/region from #info as list, split by "/".
4. subject: extract duration as pure number (min) from v:runtime or #info.
5. subject: return casts as list instead of comma-joined string.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- bilibili subtitle/comments tests: use importOriginal to include
resolveBvid in utils mock
- comments test: use valid BV ID format for aid-resolution error test
- launcher test: skip pgrep test on win32 (detectProcess early-returns)
* fix(windows): graceful degradation and manual CDP override for Electron apps
* fix: validate OPENCLI_CDP_ENDPOINT with probeCDP before use
Fail-fast with a clear error if the manual CDP endpoint is not reachable,
instead of passing a bad URL downstream and getting a confusing error.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(bilibili): support b23.tv short URL/short code resolution
Add resolveBvid() in utils.ts to automatically resolve b23.tv short URLs
and short codes to BV IDs. Supports all input formats:
- BV ID: BV1MV9NBtENN (pass through)
- Short code: XYzsqGa
- Short URL: https://b23.tv/XYzsqGa, b23.tv/XYzsqGa
Uses Node.js https.get with 302 redirect only (no body download),
typically ~100-250ms resolution time.
Applied to: subtitle, comments, download commands.
* fix: add timeout, input coercion, and tests for resolveBvid
- 5s timeout on https.get to prevent hanging on unresponsive b23.tv
- Accept unknown input type with String() coercion
- Simplify callers (remove redundant String().trim() wrappers)
- Add unit tests for BV ID passthrough and edge cases
---------
Co-authored-by: chenruinian <chenruinian@Sa1kas-MacBookPro.local>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat: auto-downgrade table output to YAML in non-TTY environments
When stdout is not a TTY (pipes, AI agents, subprocesses), automatically
output YAML instead of table with ANSI colors and box-drawing characters.
This makes opencli output parseable by downstream tools and AI agents.
Behavior:
- TTY: table (default, unchanged)
- Non-TTY: yaml (auto-detected)
- OUTPUT env var: overrides auto-detection (yaml/json/table/etc)
- Explicit -f flag: always respected
* fix: TTY detection now works with commanderAdapter default fmt
- fmt='table' from commanderAdapter now correctly triggers non-TTY downgrade
- Priority: explicit -f (non-table) > OUTPUT env var > TTY auto-detect
- Added test for explicit -f precedence over OUTPUT env var
* fix: explicit -f flag now takes precedence over TTY auto-detection
Use Commander's getOptionValueSource to distinguish explicit -f from
default. Explicit -f table in non-TTY keeps table output. Only auto-
downgrade when user didn't pass -f.
Priority: explicit -f > OUTPUT env var > TTY auto-detect > table default
* fix: explicit -f also skips command defaultFormat override
When user passes -f explicitly, command-level defaultFormat (e.g.
gemini/ask defaultFormat:'plain') no longer overrides their choice.
* feat(amazon): unify ranking adapters for three signal boards
* refactor: simplify bestsellers wrapper and fix pagination detection for all ranking types
1. Remove unnecessary __test__ wrapper from bestsellers.ts — the test
now uses normalizeRankingCandidate directly from rankings.ts,
eliminating a needless indirection layer.
2. Fix isRankingPaginationUrl to detect pagination refs for all ranking
types: zg_bs_pg_ (bestsellers), zg_bsnr_pg_ (new releases),
zg_bsms_pg_ (movers & shakers). Previously only matched the
bestsellers-specific ref pattern.
---------
Co-authored-by: 泽加武 <zejiawu@zejiawudeMac-mini.local>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix doubao image urls in read output
* fix(doubao): derive image selector from messageTextSelectors
Hardcoded image selector only covered the first two text selectors,
so images inside class-based message containers would be missed.
Generate from the shared selector list for consistency.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* refactor(xiaohongshu): replace blind retry with MutationObserver wait
Instead of retrying the entire navigation when search results are empty,
use a MutationObserver to wait for `section.note-item` elements (or login
wall text) to appear in the DOM, with a 5s timeout. This is faster (resolves
as soon as content renders) and more correct (addresses the root cause of
delayed hydration rather than working around it with a full re-navigation).
* simplify: merge login-wall detection into MutationObserver wait
WAIT_FOR_CONTENT_JS now returns 'content', 'login_wall', or 'timeout'
instead of just true/false. This eliminates the separate login-wall
evaluate call and the redundant loginWall field in the extraction payload.
Two evaluate calls total (wait + extract) instead of three.
* fix(doubao-app): connect to correct CDP target instead of background page
Doubao desktop app exposes multiple CDP targets. The scoring logic picked
the background page (doubao-background) over the actual chat page because
its URL-as-title contained "doubao", boosting its score above the real
chat page (title "豆包"). This caused all commands (send, ask, read) to
fail with "No textarea found".
- Add `targetFilter` field to ElectronAppEntry for per-app preferred target
- Set doubao-app targetFilter to 'doubao-chat/chat'
- Penalize background/new-tab-page URLs and URL-like titles in scoring
- Thread cdpTargetFilter through execution → runtime → CDPBridge
Closes#634, closes#506
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(cdp): exclude background targets instead of targetFilter
Replace the targetFilter plumbing (4 files, new interface field) with
a single-line fix: exclude `background_page` and `service_worker`
type targets from CDP selection entirely.
Background pages should never be connection targets — they have no
visible DOM and all selectors will fail. This is the root cause of
#506/#634 (doubao-app connecting to empty background page).
Simpler fix: 1 line added vs 4 files modified. No new interface
fields, no per-app configuration needed.
---------
Co-authored-by: 刘启灏 <liuqihao@liuqihaodeMacBook-Pro.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
PR #712 refactored _ensureDaemon to use a single fetchDaemonStatus() call
instead of separate isDaemonRunning(). The test was still mocking the old
function, causing it to fall through to the spawn-daemon path and throw
the wrong error message.
1. eval retry delay: 1000ms → 200ms for SPA navigation errors, 500ms
for debugger detach. SPA navigations recover within ~100ms, the old
1000ms delay was unnecessarily long.
2. Window creation: replace fixed 200ms sleep with tab-load poll.
Listens for chrome.tabs.onUpdated status=complete with 500ms
fallback cap. about:blank loads in ~20ms, saving ~180ms.
3. bridge.ts _ensureDaemon: single fetchDaemonStatus() call instead of
two sequential calls (isExtensionConnected + isDaemonRunning both
called fetchDaemonStatus independently). Saves one HTTP round-trip.
4. goto() post-navigation: coalesce stealth injection + DOM settle into
a single exec call. Previously two sequential round-trips
(Node→daemon→WS→extension→CDP each). Saves ~60-160ms per goto().
Two changes that eliminate the about:blank → target-domain navigation
on first command execution:
1. Extension: getAutomationWindow() accepts an optional initialUrl.
When creating a new window, uses the target URL directly instead
of about:blank. handleNavigate() passes cmd.url through so the
window starts on the correct domain.
2. CLI: Remove isAlreadyOnDomain() check before pre-nav. Instead,
always call page.goto(preNavUrl) — the extension's handleNavigate
already has a fast-path that skips navigation when the tab is
already at the target URL. This avoids an extra exec round-trip
(getCurrentUrl eval) on first command.
Net effect: first command saves ~1-3s (one fewer page load),
subsequent commands behave the same (navigate fast-path handles
domain matching efficiently via chrome.tabs.get).
Both methods had zero production callers — only test mocks referenced them.
newTab() created about:blank pages via CDP Target.createTarget, but no
adapter or pipeline step ever invoked it. closeTab() was similarly unused.
selectTab() and tabs() are kept as they have active production usage
(e.g. doubao adapter). The scoreTarget about:blank penalty is retained
as a defensive measure against user-opened blank tabs.
* docs: improve operate skill with Browser Use best practices
- Add Critical Rules section (state over screenshot, verify with get value)
- Add Command Cost Guide (free/instant vs expensive vision tokens)
- Add Action Chaining Rules (safe to chain vs page-changing)
- Add Tips section
- Fix Core Workflow to use state/get value for verification, not screenshot
- Mark screenshot as "ONLY for user deliverables"
Inspired by Browser Use's design: DOM-first state representation,
action cost awareness, and multi-action chaining patterns.
* docs: fix operate skill — eval read-only, IIFE, interaction rules
- Add rule: NEVER use eval to click/type — use click/type/select commands
(eval bypasses scrollIntoView + CDP pipeline, fails on off-screen elements)
- Add rule: eval is read-only, always wrap in IIFE to avoid variable conflicts
- Reorder Critical Rules for priority
- Add IIFE example in Extract section
Root cause: Claude Code was using eval("el.click()") instead of
click <index>, and hitting "already declared" errors from repeated
eval calls in the same page context.
* feat: Browser Use best practices — click/type/state improvements
Inspired by deep analysis of Browser Use's design patterns:
1. Framework listener detection (React/Vue/Angular)
- Detect __reactProps$ onClick, Vue _vei, Angular ng-reflect-click
- Catches <div onClick> elements that pure ARIA/tag heuristics miss
2. Click CDP fallback
- clickJs() now returns coordinates on failure
- BasePage.click() falls back to CDP Input.dispatchMouseEvent
- Page.clickWithQuads() uses DOM.getContentQuads for inline elements
3. Type improvements
- React-compatible: use native HTMLInputElement.prototype.value setter
- Contenteditable: selectAll + execCommand('insertText') for rich editors
- Autocomplete: detect role=combobox, wait 400ms for dropdown suggestions
4. getContentQuads precise click
- Page.clickWithQuads() for multi-line inline elements (e.g. wrapped <a>)
- Falls back through getContentQuads → getBoxModel → JS click
* fix: address code review — injection, silent failure, setter prototype
1. clickWithQuads: escape ref with JSON.stringify before inserting into
JS strings and CSS selectors (injection risk)
2. base-page click: throw error when both JS click and CDP fallback fail
instead of silently succeeding
3. typeTextJs: use matching prototype for native setter
(HTMLTextAreaElement for textarea, HTMLInputElement for input)
* fix(twitter): add search input fallback for intermittent SPA navigation failures
The pushState + popstate approach works in most environments but fails
intermittently for some users (see #690), likely due to Twitter A/B
tests or timing race conditions where the pathname hasn't updated when
checked.
This commit adds a fallback strategy: when pushState fails after 2
retries, we type the query into the search input on /explore and press
Enter. This triggers Twitter's own form handler, performing SPA
navigation without a full page reload (keeping the fetch interceptor
alive).
Both strategies use selector-based waiting ([data-testid="primaryColumn"])
rather than fixed delays, with graceful fallthrough on timeout.
Fixes#690
* test(twitter): update search test for fallback evaluate call
The search input fallback adds one extra evaluate() call when pushState
fails. Update the mock chain and assertion count accordingly.
* fix(twitter): guard nativeSetter and add fallback success test
- Add optional chaining on getOwnPropertyDescriptor().set to handle
edge cases where Twitter's sandbox overrides the HTMLInputElement
prototype.
- Add test case covering the full fallback path: pushState fails twice,
search input fallback succeeds, results are returned correctly.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(twitter): resolve article ID to tweet ID before GraphQL query
Article URLs (x.com/i/article/{articleId}) use a different ID than
tweet status URLs. The GraphQL TweetResultByRestId endpoint requires
the parent tweet ID, not the article ID.
Fix: navigate to the article page first, extract the associated tweet
ID from DOM links, then use that for the GraphQL query.
Fixes article fetching returning "Article not found" for all article URLs.
* fix: distinguish article URLs from status URLs, add explicit error handling
The previous commit routed all inputs through the article page, breaking
status URL and bare ID flows. Now only article URLs trigger the
article→tweet ID resolution. Status URLs and bare IDs keep the original
behavior. Also throws an explicit error if resolution fails instead of
silently falling back to the article ID.
---------
Co-authored-by: buruguo <buruguo@lambdafintech.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(xiaohongshu): clarify empty note shell hint
* fix(xiaohongshu): simplify empty shell detection to title+author check
The 7-field conjunction was overly strict — a note that rendered only
placeholder metrics but no title/author was still a valid empty shell.
Since title and author are always present on real notes, checking just
those two fields is a more reliable and simpler signal.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* refactor: deduplicate transient error checks, cache VM contexts, expose tab ID
- Extract shared isTransientBrowserError() into browser/errors.ts, replacing
duplicated string-matching lists in daemon-client.ts and pipeline/executor.ts
- Cache compiled vm.Script objects in template.ts with LRU eviction (max 256),
avoiding per-invocation VM context creation in pipeline loops
- Add getActiveTabId() to IPage interface and Page class for tab state inspection
* refactor: extract BasePage to deduplicate DOM helpers across Page and CDPPage
Both Page (daemon-backed) and CDPPage (direct CDP) had ~200 lines of
identical DOM helper implementations (click, type, scroll, wait, snapshot,
interceptor, etc). Extract shared logic into abstract BasePage class.
Subclasses now only implement transport-specific methods.
* refactor: rename mcp.ts to bridge.ts and clean up Playwright MCP references
The file browser/mcp.ts contained BrowserBridge (daemon session manager),
not MCP functionality. Renamed to bridge.ts for clarity. Also removed all
stale "Playwright MCP" references from comments and variable names across
the codebase — Playwright was removed long ago.
* fix(notebooklm): remove bind-current workflow
* fix: relax notebook ID check in open.ts and clean up idle timeout test
- open.ts: only throw when page kind is not 'notebook'; log a warning
instead of throwing when the notebook ID doesn't match exactly
- background.test.ts: remove unused tabs[1] setup in idle timeout test
that was leftover from borrowed-session era
* build: rebuild extension dist after bind-current removal
* 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.