* fix(twitter): read the profile link until it settles in whoami
* fix(twitter): harden whoami identity settling
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(instagram): download through the media info endpoint and expand ~ in --path
* fix(instagram): harden media info downloads
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(instagram): like and unlike posts through the post page controls
* fix(instagram): verify post like persistence
* fix(instagram): confirm already-like state in feed
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat: port twitter full-sync and close-window hardening onto 1.8.6
Rebase our xfetch-oriented OpenCLI mods onto upstream main organically:
keep the 1.8.x likes/bookmarks media metadata and auth hardening, then
add --all/--resume-file/--output-file JSONL streaming with U+2028/U+2029
escaping, raise the full-archive page budget, retry browser lease close
failures, and expose browser tab current-window diagnostics.
* fix(twitter): preserve resume state when max-pages stops early
--max-pages is a safety budget, not archive exhaustion. Keep the resume
file and report complete=false so full-sync can continue instead of
restarting from the top.
* test(cli): expect browser tab current-window in structured help
The full-sync branch adds `browser tab current-window`, so the nested
tab help snapshot must count 5 commands instead of 4.
* fix(twitter): make archive resume state fail closed
* fix(twitter): reject mismatched archive resume output
---------
Co-authored-by: OpenCLI-sol <opencli-sol@users.noreply.github.com>
* feat(pinterest): add Pinterest adapter suite
Adds `opencli pinterest` with 19 commands over Pinterest's internal resource API
(`POST /resource/<Name>Resource/<action>/`, form-urlencoded `source_url` + `data`,
`X-CSRFToken` + `X-Pinterest-PWS-Handler` headers, `bookmark` paging).
- Read: search-pins / search-boards / search-users, pin, user, user-pins,
user-boards, board-pins, board-sections, download. Reads work anonymously
because Pinterest issues a csrftoken to logged-out sessions too.
- Write: save (boardless repin lands in "Quick saves"), pin-create /
pin-update / pin-delete, board-create / board-update / board-delete,
board-section-create / board-section-delete.
- Deletes require `--confirm`; without it the command resolves and names the
target, then exits non-zero via ArgumentError (pin title + board for
pin-delete, pin count for board-delete, section title for
board-section-delete).
- Boards are addressed by `<username>/<slug>`, a board URL, or the numeric
`boardId` (BoardResource accepts `board_id` and reports the board's url, which
is reused so the id path costs no extra round trip); sections by id or slug.
Display names are not accepted — a name alone cannot say which account a board
belongs to. A Pinterest site route such as a `/pin/<id>/` URL is rejected as
such instead of being parsed as the board `pin/<id>`.
- Board URLs are percent-decoded before use: Pinterest hands out encoded slugs
for non-ASCII board names, and posting those verbatim answers HTTP 404. Slugs
are compared Unicode-normalized so an NFD-composed accent still matches.
- Sections can only be set by a follow-up move. PinResource/create and
RepinResource/create accept a section key, answer HTTP 200, and file the pin
at the board root anyway; only PinResource/update honours it (under
`board_section_id`, not `section_id`). So `save --section` and
`pin-create --section` create then move, and report the created pin id if the
move fails rather than claiming success.
- Omitting an optional text flag leaves the field alone; passing an empty string
clears it. Pinterest refuses link edits on pins it scraped, and answers 401
for that, so its own message is surfaced rather than only "log in".
- Typed errors throughout: ArgumentError for bad refs, unknown sections,
`--section` without `--board`, and limits (validated before any request, with
no silent clamp); AuthRequiredError on 401 (and 403 on writes only, since
reads are anonymous); CommandExecutionError for malformed payloads and
unresolvable targets.
Live-verified end-to-end against a logged-in account: all 10 read commands, and
the full write cycle (board-create → board-section-create → board-update →
pin-create → pin-update → save → the three deletes, preview and confirmed),
including section placement checked on each pin's own `section` field, non-ASCII
board/section slugs, board-id addressing, and clearing a description. 131 tests;
full suite 6258 passed; `tsc --noEmit` clean; `opencli validate` 0 errors;
typed-error-lint and silent-column-drop both new=0; doc coverage 174/174.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(pinterest): drop board-update --privacy empty default
`coerceAndValidateArgs` applies an arg's default and then enforces `choices`
against it, so `default: ''` on a public|secret flag rejected every run that
omitted `--privacy`:
$ opencli pinterest board-update janedoe/my-board --name Foo
error: ARGUMENT Argument "privacy" must be one of: public, secret. Received: ""
Leaving the default off keeps the flag optional; the command already reads it
as `String(kwargs.privacy ?? '')`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(amazon): honor the input marketplace instead of rewriting to amazon.com
* fix(amazon): reject amazon.<label>.<tld> look-alikes and localize the auth hint
* fix(amazon): allow only known marketplace domains
---------
Co-authored-by: OpenCLI-sol <opencli-sol@users.noreply.github.com>
* fix(twitter article): include images, canonicalize URLs, add metadata fields
The article adapter skipped atomic blocks entirely, silently dropping all
images from Twitter article markdown output.
This patch:
1. Resolves atomic blocks -> entity -> mediaId -> media_entities -> image URL
and emits images as  inline.
2. Canonicalizes pbs.twimg.com URLs to the ?format=<ext>&name=large form
used by the standard Twitter media CDN (matches reference clipping format).
3. Adds two new output columns: published_at (from tweet.legacy.created_at)
and preview_text (from articleResults.preview_text) to support building
Obsidian-style frontmatter at save time.
Tested with https://x.com/0xblacklight/status/2069503920918106370
10 images with captions, canonical URLs, all metadata populated.
* fix(twitter/article): resolve media by Draft entity key
---------
Co-authored-by: OpenCLI-sol <opencli-sol@users.noreply.github.com>
* fix(twitter/profile): recover counts + bio after X relocates them out of legacy (#2188)
`twitter profile` returned followers/following/tweets/likes = 0 and an empty
bio while name/screen_name/created_at/verified stayed correct. X moved the count
fields (followers_count/friends_count/statuses_count/favourites_count) and the
bio (description) out of `result.legacy` into a new container — the same drift
#1745 handled for name/created_at by reading `result.core`.
Rather than hard-code the (unknown) new path, resolve each field from its known
homes first (legacy → core → top-level result), then fall back to a bounded
breadth-first search that returns the shallowest match. The BFS refuses to cross
into containers describing a *different* entity (pinned_tweet, entities, media,
…) so it can never report an embedded tweet's favourites_count as the user's
likes or its text as the bio — a wrong-but-confident value would be worse than
0 / ''. This restores the counts/bio today and stays robust if X relocates them
again.
Legacy-path responses resolve identically (existing test unchanged). Adds
offline regression tests for the relocated-field case, legacy precedence over a
deeper decoy, the embedded-tweet guard, all-missing fallbacks, and resolver
type/empty handling.
* fix(twitter/profile): map observed current schema
---------
Co-authored-by: OpenCLI-sol <opencli-sol@users.noreply.github.com>
whoami and login-gated commands reported AUTH_REQUIRED for logged-in users
because the identity probe only matched the legacy Discuz member panel
(`#um .vwmy h4 a`), which the site no longer renders.
- Match the current header username link (`a[title="访问我的空间"]`) while
keeping the legacy selectors as fallbacks; the existing uid regex already
handles the `space-uid-<uid>.html` href.
- Also accept the logged-in header menu ids (`#g_upmine`, `#extcreditmenu`)
as a login signal, so a future wording/markup change of the username link
does not reintroduce a false AUTH_REQUIRED.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(facebook/search): preserve query identity + drop redirect shims (#2090)
The #2126 extractor deduped and reported result URLs as `origin + pathname`,
dropping the query string. But `permalink.php?story_fbid=…`, `story.php?…` and
`watch/?v=…` carry their identity in the query — so two *different* posts or
videos collapsed into a single row and only the first survived dedup.
Add `entityKey(u)` that keeps only the identity params (story_fbid, fbid, id, v,
story_id) and strips FB's per-render tracking nonces (__cft__, __tn__, ref).
Distinct posts now stay distinct, while the same post rendered twice with
different nonces still dedupes to one row. Vanity paths without identity params
keep collapsing to the bare pathname (unchanged).
Also reject `l.` / `lm.` `facebook.com` hosts: their `/l.php?u=…` outbound-link
wrappers passed the host regex and the vanity path catch-all, leaking external
redirect shims into the results.
Adds offline regression tests for distinct permalink/watch identities, nonce
dedup, and the redirect-shim guard.
* fix(facebook): scope query identity by destination
---------
Co-authored-by: OpenCLI-sol <opencli-sol@users.noreply.github.com>
* enrich(ctrip): add train ticket search command
ctrip search already suggests railway stations but there was no way to query the
actual departures. ctrip train <from> <to> --date fills that gap on the public
trains.ctrip.com list page, browser-mode + cookie like flight/hotel-search. Rows
are read by stable class-keyed fields rather than positional innerText;
incomplete cards are dropped, not sentinel-filled.
* enrich(ctrip): add hotel detail command
Single-hotel profile from the detail-page SSR: rating sub-scores, hot facilities, check-in/out policy.
* enrich(ctrip): add bus ticket search command
Intercity coach search via the newbus results deep link (landing SPA does not hydrate under the bridge).
* enrich(ctrip): add ferry ticket search command
Passenger ferry sailings via the ship.ctrip.com results deep link, sibling of bus.
* enrich(ctrip): add cruise package search command
Resolves a departure port name to its legacy per-port code, then reads the .route_info cards.
* enrich(ctrip): add tour package search command
Group and self-guided tour search via the vacations sv=<destination> deep link, stable-class cards.
* enrich(ctrip): add flight+hotel package search command
Shares the vacations product extractor with tour (freetravel section); folds a 万 count multiplier into the shared parser.
* enrich(ctrip): raise CommandExecutionError on rendered-but-unparsed results
Matches the drift handling bus/ferry/train use, so genuine-empty stays EmptyResultError.
* enrich(ctrip): generalize shared list helpers, drop dead train constants
parseListLimit / parsePlaceName replace the train-named helpers now reused across bus/ferry/cruise/tour/package with neutral hints; ferry ship-name/duration read by pattern, not position.
* enrich(ctrip): add attraction listing command
* enrich(ctrip): add round-trip flight search command
* enrich(ctrip): scope attraction to city id and harden flight-round
* fix(ctrip): repoint one-way flight to Ctrip's migrated .flight-item cards
* fix(ctrip): harden travel adapter boundaries
* fix(ctrip): preserve raw limit strings
* test(ctrip): avoid adapter src import
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(trip): add Trip.com international flight search adapter
Trip.com is the English-facing sibling of the ctrip adapter. trip flight
searches worldwide one-way flights, browser-mode + cookie like ctrip flight.
Results are read from .result-item cards by stable data-testid anchors rather
than positional innerText; incomplete cards are dropped, not sentinel-filled.
Closes#2157
* enrich(trip): add hotel-search command
* enrich(trip): add hotel detail command
Single-hotel profile from the detail-page SSR (same shape ctrip hotel uses); also documents the existing hotel-search command.
* enrich(trip): add round-trip flight search command
Reuses the shared .result-item flight extractor against a triptype=rt search URL.
* enrich(trip): rename parseFlightLimit to parseListLimit
The 1-50 limit parser is shared by hotel-search and both flight commands, so a neutral name reads truer than the flight-specific one.
* enrich(trip): add attractions and experiences search command
Anchors on each things-to-do card's stable detail link (name + per-row url) and reads rating/reviews/booked/price by data-format pattern, since the cards use hashed CSS-module classes.
* enrich(trip): add train route timetable command
Reads the per-country SEO route timetable (departure/arrival times, stations, duration, changes) by stable class fields; per-journey fares sit behind the booking step.
* enrich(trip): add car-rental listing command
* enrich(trip): add airport-transfer listing command
* enrich(trip): add tour-package search command
* enrich(trip): add public destination-suggest command
* enrich(trip): add flight+hotel package search command
* docs(trip): note eSIM plans surface via attraction search
* enrich(trip): add live-promotions deals command
* enrich(trip): treat empty deals parse as drift, not empty result
* enrich(trip): split tour no-match (empty) from schema drift
* fix(trip): type public fetch drift failures
* fix(trip): require package flight identity
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(kimi/usage): read quota from membership subscription page
Replace the /code/console page with /membership/subscription?tab=quota so the command surfaces the total usage percentage plus 5h/7h rate limits, gift quota, and booster balance.
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(deepseek/usage): add usage command for platform.deepseek.com
Reads DeepSeek platform usage data from https://platform.deepseek.com/usage
via internal API (get_user_summary) for account-level data and DOM extraction
for time-dimension summary cards.
Output columns:
- balance / bonusBalance (充值/赠送余额)
- cumulativeSpend (累计消费金额)
- monthlySpend / monthlyApiCalls / monthlyTokens (本月数据)
- currentTokenEstimation (当前可用 Tokens 预估)
- timePeriod / periodSpend / periodApiCalls / periodTokens (时间维度)
* test(usage): harden kimi and deepseek usage contracts
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(eastmoney): correct mislabeled convertible ytm/remainingYears columns (#2109)
eastmoney convertible emitted systematically impossible ytm / remainingYears
(20/20 wrong). Cross-verification (12/12 fingerprint) shows the clist fields
were mislabeled: f239 is the putback trigger price (= convPrice × 0.7), not YTM,
and f238 is the pure-bond premium %, not the remaining term.
Relabel to the true semantics (pureBondPremiumPct / putTriggerPrice) and drop
the known-wrong ytm / remainingYears columns rather than keep emitting garbage.
Rename SORTS.ytm -> 'put-trigger' so --sort no longer claims to order by a value
it doesn't hold. Extract mapConvertibleRows() and add JSON-fixture tests.
Real YTM / remaining term aren't in this response's fields; adding the correct
f-codes needs a live push2 field dump cross-checked against jisilu — left as a
follow-up.
* fix(eastmoney): harden convertible field output
* fix(eastmoney): require convertible identity strings
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(toutiao): add recommend channel feed, fix hot --limit being ignored
hot declared func(_page, kwargs) while browser:false commands receive a single
args object, so kwargs was always undefined and --limit silently fell back to
30. Its unit tests passed only because they called func(null, kwargs) by hand.
* fix(toutiao): require recommend article identity
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(xiaohongshu): extract comment images and improve scroll-loading robustness
Add an images field to xiaohongshu/rednote comments (top-level and nested
replies), scraped from .comment-picture galleries while excluding avatars
and inline note-content-emoji stickers. Also make the comment-loading
scroll loop keep going until --limit is satisfied or growth stalls for
several rounds (instead of bailing after one stalled round), and drive
scroll through the scroller element, scrollIntoView, and window.scrollTo
together since the actual scrollable ancestor varies by layout.
* fix(xiaohongshu): validate comment image payloads
* fix(xiaohongshu): scope comment image extraction
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(browser): hit-test the click point and retarget handler-less nodes (#2076#2071)
click() reported {clicked:true} whenever a CDP Input.dispatchMouseEvent
didn't throw, even when the synthetic click silently landed on an overlay
(#2076) or on a handler-less child like an <svg> icon whose click handler
lives on the wrapping <div> (#2071).
Now boundingRectResolvedJs (in a click-only mode) hit-tests the centre via
elementFromPoint and classifies it: target (element/descendant) and ancestor
(open shadow-DOM host or own wrapper — a CDP click still reaches the target)
are trusted; an unrelated overlay ('other') forces a direct DOM-click fallback.
On a miss it probes inset points for a hitting one. If the resolved node owns
no click handler, the click retargets to a nearby clickable ancestor so the
handler fires — cursor:pointer is excluded from that decision because it is
inherited. The result now surfaces click_method (cdp|js|ax), hit, and
retargeted so agents can tell a trusted click from the fallback. hover() and
dblClick() keep their original plain-centre behaviour (click-only opt-in).
Runtime tests execute the generated JS against a fake DOM (with cursor
inheritance modelled) covering target/ancestor/other, retarget, and probe.
* fix(facebook): extract modern feed posts via the action-menu anchor (#2089)
Modern facebook.com no longer wraps feed posts in [role="article"] nor
exposes the Like/Comment/Share aria-labels the fallback keyed on, so feed
extracted 0 rows. Add a container source that anchors on each post's
"Actions for this post" menu and walks up to the highest ancestor holding
exactly one such menu (stopping before page landmarks), a bounded
scroll-to-load loop so lazily-streamed posts render, all-digit decoy author
rejection, and hidden-char / Reels-carousel decoy filtering. jsdom fixtures
cover the modern shape and keep the legacy [role="article"] path working.
* fix(facebook): extract search results from role=feed entity links (#2090)
Modern /search/top renders results inside [role="feed"] as entity/content
links (people, pages, groups, posts) rather than [role="article"]/[role=
"listitem"], and seeds hidden-char decoy links back to /search. Rewrite the
adapter (pipeline -> func, so the extractor is unit-testable) to collect
anchors inside the feed, keep only real facebook.com entity/content hrefs,
and drop /search decoys, chrome links, off-domain spam, and obfuscated text.
Preserves the #625 navigate-before-extract guard. jsdom fixtures included.
* fix(plugin): pass --ignore-scripts to plugin npm install (#1753)
Plugin repos are cloned from untrusted third-party Git URLs. Without
--ignore-scripts, `npm install` runs preinstall/install/postinstall
lifecycle scripts (of the plugin and every transitive dep) at install
time with the user's privileges. Adapter plugins don't need lifecycle
scripts — adapter code is loaded later by the discovery path — so deny
that execution vector unconditionally. Adds a test asserting the flag.
* fix(chatgpt): verify whoami via /api/auth/session, not legacy cookie (#2087)
verifyChatgptIdentity hard-gated on the legacy
`__Secure-next-auth.session-token` cookie before probing
/api/auth/session, so logged-in users on cookie-less sessions got a
false AUTH_REQUIRED. The session endpoint (200 + user.id) is
authoritative; drop the cookie precondition from verify. The login
`poll` keeps its cheap non-navigating cookie gate so verify (which
navigates) doesn't run every ~2s and yank the user off the OAuth form.
Also prefix-match the session cookie so the quickCheck/status/refresh
fast paths stop false-negativing on NextAuth chunked (.0/.1) cookies.
* fix(instagram): collect explore_grid media across nested layouts (#2091)
Instagram stopped populating the flat layout_content.medias[] path;
media now nest across mixed layout shapes (one_by_two_item.clips.items[]
.media, fill_items[].media, ...), so explore returned []. Recursively
walk each sectional item collecting every distinct node.media, dedupe by
pk/id/code (skipping descent into a collected media so carousel children
aren't counted as separate posts), and fall back to play_count for
clips/reels engagement.
* fix(extension): upload files via file-chooser interception (#2108)
DOM.setFileInputFiles with a nodeId/backendNodeId is rejected "-32000 Not
allowed" when the debugger is attached via chrome.debugger (crbug
928255), breaking file upload on every site. Switch setFileInputFiles to
the file-chooser interception flow: enable Page.setInterceptFileChooser-
Dialog, programmatically open the chooser, and use the backendNodeId from
the intercepted Page.fileChooserOpened event (which Chrome accepts). The
event listener is registered before the click and settles on any matching
event so a malformed one rejects fast. Includes the rebuilt bundle.
* fix(chatgpt): use page.sleep in the poll loops #2099 missed (#2095)
#2099 converted the main streaming loops to page.sleep but did not touch
image.js, deep-research-result.js, or the image-poll re-navigation waits
in utils.js. Those still called page.wait(n>=1), which injects a whole-
subtree+attributes MutationObserver DOM-stability wait rather than a
sleep — during ChatGPT streaming the observer never goes quiet and pegs
the renderer. Convert the remaining poll-loop sleeps to page.sleep;
one-shot post-navigation settles are left as-is.
* Improve ChatGPT Deep Research progress reporting
* fix(chatgpt): preserve deep research progress rows
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(linkedin): add company command to read a company page
Adds `opencli linkedin company <name>` — reads a LinkedIn company's About
page: industry, size, headquarters, founded, website, specialties, follower
count, and about text.
- Accepts a bare universal name (`nvidia`), a `/company/<name>` path, or a
full company URL; navigates to the About page and scrapes the dt/dd fact
list + follower count (same DOM-extraction style as profile-read).
- Typed errors: AuthRequiredError via assertLinkedInAuthenticated,
CommandExecutionError on malformed payload / missing company name.
Live-verified end-to-end (NVIDIA: 42M followers, Computer Hardware
Manufacturing, founded 1993; Databricks via full URL). 4 tests; audits new=0.
* fix(linkedin): harden company identity output
* fix(chatgpt): use pure sleeps and cheap generation checks in polling loops (#2095)
During a long `chatgpt ask` (10-20 min answers) the chatgpt.com renderer hit
~700% CPU and >4GB RSS. Root causes, all in the poll loops that run for the
whole generation:
- `page.wait(n>=1)` does not sleep client-side; it injects a whole-body
MutationObserver (DOM-stable probe) that never goes quiet while the answer
streams, so it re-arms and fires on every mutation for the full interval.
Add `page.sleep(seconds)` (bare setTimeout, no page evaluation) to BasePage
and IPage, and switch the poll-interval waits to it: waitForChatGPTResponse,
waitForChatGPTDetailRows, waitForChatGPTDeepResearchResult,
waitForChatGPTImages, waitForChatGPTUploadPreview, and the ask pre-send
settle loop. One-shot post-navigation settle waits keep `page.wait` for its
DOM-stable early return.
- `isGenerating` read `document.body.innerText` every poll, forcing a full-page
reflow and a conversation-sized string allocation. Rewrite it to cheap
signals: stop-button test id, control aria-labels, and a `textContent`
(no reflow) scan scoped to the composer + last turn.
- `getVisibleMessages` read both innerHTML and innerText per turn. Add a
`textOnly` option that skips innerHTML and use it from the response poll
loop, whose output is text-only; read/detail markdown paths are unchanged.
* fix(chatgpt): cover both message shapes in the scoped isGenerating scan (#2095)
The scoped text fallback only looked at article conversation turns, but
CONVERSATION_MESSAGE_SELECTOR supports bare [data-message-author-role]
nodes too. On that DOM shape a plain-text Thinking pill (no stop button,
no aria-label) would read as idle and waitForChatGPTResponse could
return a truncated answer. Prefer the article turn (wider container),
fall back to the last role-attribute node when articles are absent.
Addresses the P2 from external review of PR #2099.
* fix(chatgpt): only leaf pills outside message content count as generating (#2095)
Scanning whole-scope textContent flags any finished answer that merely
mentions "Thinking" / "正在思考" (prose or backticked code spans) as
still generating — e.g. a conversation reviewing this very code —
permanently blocking follow-up sends. Count only short leaf elements
outside .markdown/pre/code as status pills.
Verified against a live conversation whose messages discuss isGenerating:
detail reported Generating=true before, false after; a real streaming
pill still matches (leaf, short, outside rendered content).
* fix(chatgpt): don't read the Thinking model label as a generating state (#2095)
'Thinking' is a supported idle model label (CHATGPT_MODEL_TARGETS.advanced)
rendered as a composer-form button, so both the page-wide aria-label match
and the composer-scope leaf scan flagged an idle conversation with that
model selected as generating forever, blocking sends. Drop bare 'Thinking'
from aria-label matching (the stop button covers English streaming states)
and only count it inside the last conversation turn.
Addresses the round-2 P2 from external review of PR #2099.
* fix(chatgpt): keep text-only polls off innerText
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(twitter): pass user args through JSON.stringify in page.evaluate
The `tweet-id` (article) and `username` (profile) arguments are
interpolated raw into the page.evaluate script string, while `ct0` and
the bearer token in the same functions already go through JSON.stringify.
A `tweet-id` that is not a status/article URL is used verbatim, so a
value containing a double quote escapes the string literal and injects
executable code into the evaluated page context. Route both arguments
through JSON.stringify, matching the existing handling of ct0/bearer.
* test(twitter): cover article evaluate arg escaping
* test(twitter): avoid article test ordering conflict
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(twitter): harden article API response handling
Two failure modes on the article command's GraphQL response were
unhandled and surfaced as opaque page.evaluate crashes:
- A 2xx response with a non-JSON body (logged-out HTML page, block or
challenge page) made `await resp.json()` throw. Wrap it in try/catch
and return a structured {error, hint}, mirroring the guard profile.js
already has. The raw parser message is not surfaced, since V8's JSON
SyntaxError echoes a fragment of the response body.
- A valid JSON `null` body made `d.data?.` throw a TypeError, bypassing
the structured-error path. Guard the root with `d?.data?.`.
Both paths are turned into a clear CommandExecutionError by the existing
outer handler.
* fix(twitter): harden article response handling
* fix(twitter): fail closed on malformed article payloads
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(twitter): match localized delete menu and poll for late-hydrating article (#2001)
twitter delete failed on a Simplified-Chinese X detail page: (1) the More
caret was matched by aria-label === 'More', which X localizes (zh-Hans 更多),
and (2) findTargetArticle() ran before the article's self-referential
/status/<id> link hydrated on slow networks. Inside buildDeleteScript:
- Prefer the language-agnostic [data-testid="caret"] (scoped to the matched
article), falling back to a multilingual /^(More|更多)/ aria-label match.
- Poll findTargetArticle() for ~5s (20 x 250ms) before giving up.
- Broaden the Delete menu item to Delete/删除 and exclude the Lists item in
both languages (List/列表).
* fix(twitter): harden delete menu result handling
* fix(twitter): scope delete menu items to opened menu
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(weibo): resolve uid before the full auth probe to avoid HTTP 400 (#2047)
`auth status --site weibo --full` failed with `HTTP 400 from /ajax/profile/info`
even on a logged-in session: verifyWeiboIdentity fetched the bare
/ajax/profile/info, which the current Weibo web app rejects without a uid.
`weibo me` already works because it resolves the current uid first.
Mirror that path: call getSelfUid(page) (which throws AuthRequiredError when no
logged-in uid resolves), then probe /ajax/profile/info?uid=<uid>. Extract the
probe into buildWeiboIdentityProbe(uid) and add clis/weibo/auth.test.js.
* fix(weibo): unwrap auth identity probes
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* Add ChatGPT Deep Research result extraction
* fix(chatgpt): bind deep research results to requested conversation
* fix(chatgpt): preserve deep research payload failures
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>