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