Commit Graph

380 Commits

Author SHA1 Message Date
jackwener a57c030c81 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.
2026-03-30 12:48:26 +08:00
AstroHan cf79ec5c23 feat(xueqiu): add comments command (#587)
* feat: add xueqiu comments command

* docs(xueqiu): add comments command docs

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-03-30 01:22:44 +08:00
Zhangchen 8c00ad9f02 feat(browser): add ONES adapter support for tasks and worklog commands (#386)
* 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>
2026-03-30 01:06:38 +08:00
Inori333 3eb2e88c85 fix: normalize boolean arg aliases (#585) 2026-03-30 00:44:45 +08:00
Haoyue Bai b280f19321 feat(youtube): mute and pause watch pages for read commands (#578)
* Mute and pause YouTube watch pages for read commands

* fix(youtube): quiet watch pages earlier

* refactor(youtube): avoid watch ui for read commands

* test(youtube): cover html bootstrap parser

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-03-29 23:22:53 +08:00
AstroHan a32b65be4a feat: add Tieba browser adapters in TypeScript (#581)
* 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>
2026-03-29 22:39:26 +08:00
Ron ab7eca35e4 feat(doubao): add history, detail, meeting-summary (#566)
* 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>
2026-03-29 22:36:24 +08:00
jakevin 107ed28449 refactor(douyin): share user video public api (#580) 2026-03-29 17:50:46 +08:00
Howard 79b4e069f0 feat(douyin): add user-videos command with top comments (#554)
* 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>
2026-03-29 17:45:11 +08:00
PXLZJ d2b563e55b feat(xiaohongshu): add cover image URL to user notes output (#572)
* 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>
2026-03-29 17:40:33 +08:00
jakevin f8e9b08223 fix(zsxq): require active group context (#579)
* fix(zsxq): require active group context

* docs(zsxq): add adapter guide
2026-03-29 17:29:36 +08:00
bhutano 1ae1c82c4a feat(spotify): add Spotify playback adapter (#560)
* 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>
2026-03-29 17:28:37 +08:00
康厚超 440c001a20 feat(band): add Band.us adapter — bands, posts, mentions, post commands (#532)
* 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>
2026-03-29 17:19:26 +08:00
James 5925849414 feat(xiaohongshu): use CDP DOM.setFileInputFiles for image upload (#574)
* 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>
2026-03-29 17:16:55 +08:00
xtftbwvfp d8d9643e89 feat: add 知识星球(zsxq) site adapter (#571)
* 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>
2026-03-29 17:15:27 +08:00
AstroHan bb5c2b1fc6 fix(weread): harden reader fallback and search mapping (#562)
* fix(weread): harden reader fallback and search mapping

* fix(ci): remove stale weread regression test duplicates

* refactor(weread): simplify search fetch and eliminate redundant getCookies

- Parallelize search API + HTML fetch with Promise.all
- Add generic numeric entity decoding (decimal + hex) in decodeHtmlText
- Extract loadWebShelfSnapshotWithVid to pass currentVid downstream,
  avoiding a redundant getCookies call in waitForTrustedWebShelfSnapshot
- Split mixed early-return conditions with individual comments
- Add mirror comments between browser/Node trusted-index logic
2026-03-29 17:02:02 +08:00
jakevin ab0af2de5c feat(exit-codes): Unix-standard process exit codes for all error types (#564)
* 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
2026-03-28 22:16:42 +08:00
jakevin 5c655ee3c8 feat(sinafinance): rewrite stock as public API, no browser required (#563)
* 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
2026-03-28 21:59:44 +08:00
yichuanzhao99-ctrl 0b15561025 添加新浪财经行情及滚动新闻抓取 (#546)
* 添加新浪财经行情及滚动新闻抓取

* review: fix injection vuln, dead code, typos, hardcoded waits

rolling-news:
- Remove dead dateToTimestampParams function and unused CliError import
- Fix column field name typo: clomn → column
- Replace page.wait(5) with selector-based wait
- Remove all commented-out code

stock:
- Fix P0 JS injection: use JSON.stringify() to safely embed args.key/market
- Add null guard for inputEl before calling .focus()
- waitForElement returns null instead of throwing on timeout
- Replace page.wait(5) with selector-based wait
- Extract MARKET_CN/HK/US as named constants
- Throw CliError on NOT_FOUND instead of silent empty return

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-03-28 21:46:33 +08:00
AstroHan 75ddb6319b fix(extension): probe daemon before WebSocket to eliminate console noise (#534)
* 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>
2026-03-28 20:15:51 +08:00
jakevin 5ec34ebc53 feat(hub): add vercel CLI to external CLI hub (#556) 2026-03-28 19:57:49 +08:00
jakevin 959ec5fe1c feat(hub): add lark-cli to external CLI hub (#555) 2026-03-28 19:51:30 +08:00
pi-dal 5ae9658a21 fix(manifest): preserve dynamic TS arg metadata in help output (#536)
* fix(manifest): preserve runtime arg metadata

* refactor(manifest): build TS metadata from runtime commands

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-03-28 14:23:16 +08:00
jakevin 55c3259f28 refactor: slim CI matrix, shared utils, unified logging, remove __test__ leak (#525)
* 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
2026-03-28 02:19:07 +08:00
jakevin 70bd87b98c perf: smart-wait — waitForCapture, wait({ selector }), daemon backoff
- waitForCapture(): polls window.__opencli_xhr instead of DOM-stable; fixes INTERCEPT adapters returning empty after smart-wait refactor
- wait({ selector }): MutationObserver-based wait; resolves instantly on element insertion
- CDPPage.wait(N): smart DOM-stable wait (matches Page.wait behavior)
- Daemon cold-start: exponential backoff [50..3000ms]
- README: simplified to 50-line overview
2026-03-28 02:16:13 +08:00
wangsl ea0cf4d0b0 fix(network): honor proxy env for node requests (#512)
* fix(network): honor proxy env for node requests

* fix(network): honor default ports in NO_PROXY

* refactor(network): normalize proxy config handling

* refactor(network): delegate proxy env handling to undici

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-03-28 01:20:39 +08:00
AstroHan 53b06db53d fix(browser): retry settle probe after SPA client-side redirect (#517)
* 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>
2026-03-28 00:29:45 +08:00
jakevin f79d5ab838 fix(ci): stabilize public command and plugin e2e checks (#522)
* 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>
2026-03-28 00:19:33 +08:00
AstroHan 0085d63fb8 fix(weread): resolve shelf auth fallback (#518)
* fix(weread): resolve shelf auth fallback

* chore(docs): move local issue notes out of pr

* fix(weread): classify session expiry as auth required

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-03-28 00:09:28 +08:00
jakevin 0f3021a086 fix: relax extension version check, enable all adapter tests (#520)
* 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
2026-03-28 00:08:10 +08:00
jakevin 5bd0497244 refactor(plugin): make plugin installs transactional (#509)
* feat(plugin): stage installs before promote

* feat(plugin): make remote updates transactional

* fix(plugin): rollback relink failures

* refactor(plugin): unify transactional publish flow

* refactor(plugin): extract publish pipeline helpers

* refactor(plugin): add structured source model

* refactor(plugin): promote lockfile to structured sources

* fix(plugin): preserve lock reads when migration rewrite fails

* fix(plugin): write lockfiles atomically

* test(plugin): make source helper assertions cross-platform
2026-03-27 18:25:56 +08:00
Guyue a2d1199b50 fix(v2ex): fetch hot topics through browser context (#493)
* Update hot.yaml

* review: fetch v2ex hot data within browser context

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-03-27 17:50:44 +08:00
jakevin cf99c61df5 perf: smart pre-navigation — skip redundant nav + remove 2s wait (#507)
* 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.
2026-03-27 17:42:48 +08:00
AlexYue 70ad5700c9 feat(plugin): support multi-source plugin install (ssh, git@, generic https) (#504)
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
2026-03-27 17:04:57 +08:00
AlexYue 2ad1215ac2 fix(plugin): prevent raw .ts import crash when esbuild transpilation fails (#500) (#503)
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
2026-03-27 16:57:59 +08:00
AstroHan ee59750ddb fix(execution): apply timeout to non-browser commands (#383)
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.
2026-03-27 14:54:28 +08:00
sline 9a9e078462 feat(bluesky): add Bluesky adapter with 9 commands (#215)
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.
2026-03-27 14:39:43 +08:00
AlexYue 55d0473bcf fix(plugin): handle EXDEV cross-filesystem rename during install (#488)
* 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>
2026-03-27 14:22:03 +08:00
AstroHan 1a44d8ccff feat(xiaohongshu): add published_at to search results (#484) (#485)
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
2026-03-27 14:20:22 +08:00
jakevin 31cb2291c5 perf: parallel file discovery, plugin scanning, and external CLI caching (#501)
- 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)
2026-03-27 14:19:55 +08:00
AstroHan fb5b608607 fix(twitter): use DOM-only scraping for trending to match page results (#486)
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.
2026-03-27 14:15:36 +08:00
AlexYue 5e2e1dfe60 fix(plugin): detect symlinked monorepo sub-plugins in discoverPlugins (#487)
* 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>
2026-03-27 14:12:54 +08:00
AstroHan 39eec0da82 fix(xiaohongshu): adapt publish to new two-step creator center UI (#490)
* 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>
2026-03-27 14:03:10 +08:00
AlexYue 384419f4f5 feat(plugin): support local path install via file:// and absolute path (#491)
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
2026-03-27 13:45:42 +08:00
AlexYue fa4c44a0c4 feat(plugin): add 'plugin create <name>' scaffold command (#494)
* feat(plugin): add 'plugin create <name>' scaffold command

Generate a ready-to-develop plugin directory with all required files:
- opencli-plugin.json (manifest with name, version, compatibility)
- package.json (ESM, peer dependency on @jackwener/opencli)
- hello.yaml (sample YAML command using httpbin)
- greet.ts (sample TS command using cli() API)
- README.md (install, usage, and development instructions)

Usage:
  opencli plugin create my-plugin
  opencli plugin create my-plugin --dir /path/to/dir
  opencli plugin create my-plugin --description 'My awesome plugin'

Includes 5 test cases for scaffold generation and error handling.

* fix(plugin): align scaffold with local install flow

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-03-27 13:34:36 +08:00
AlexYue d74263523f fix(plugin): remove legacy LOCK_FILE/MONOREPOS_DIR constants (#495)
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.
2026-03-27 13:27:47 +08:00
jakevin f9f018d7f4 fix(doctor): remove unused fix option and add release URL to extension install hint (#498)
* 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
2026-03-27 13:26:27 +08:00
jakevin 40b923778f feat: smart error dispatch with inline Browser Bridge diagnosis (#481)
* 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
2026-03-27 03:04:09 +08:00
jakevin 15c6d0d508 refactor: deduplicate code, improve type safety, simplify error classes (#480)
- Extract shared parseYamlArgs() to yaml-schema.ts, eliminating duplicate
  YAML args parsing in discovery.ts and build-manifest.ts
- Unify BROWSER_ONLY_STEPS: export from capabilityRouting.ts, reuse in
  pipeline executor (fixes missing intercept/tap in retry set)
- Remove dead normalizeArgValue from commanderAdapter; bool coercion now
  handled solely by coerceAndValidateArgs in execution.ts
- Add closeWindow?() to IPage interface, replacing unsafe casts in executor
- BrowserBridge/CDPBridge implement IBrowserFactory, removing double cast
  in getBrowserFactory()
- Simplify CliError subclasses with new.target.name (9 redundant this.name
  assignments removed)
- Add hook dedup in addHook() to prevent duplicate registrations
- Fix normalizeRows to safely handle primitive values
- Unify CommandArgs type: execution.ts now imports from registry.ts
- Cache strategyLabel() call in cli.ts list command
2026-03-27 02:45:42 +08:00
jakevin 7617dff262 feat: zero onboarding, extension version check, and update notifier (#479)
* 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
2026-03-27 02:14:37 +08:00