findAppProcessPids intentionally returns [] on win32; five of the six app-scoped tests from #2232 fail on the Windows CI shard and the sixth passes only vacuously.
* fix(codex): resolve the ChatGPT executable inside Codex.app
* fix(codex): try the ChatGPT executable first and sync the launch doc
* fix(codex): scope executable process detection to app bundle
* fix(codex): resolve symlinked app process identity
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(discovery): warn when yaml adapters are skipped instead of dropping them silently
* fix(discovery): stay quiet for yaml adapters that already have a .js replacement
* fix(discovery): audit skipped yaml adapters in manifest path
* fix(discovery): require loadable js replacements for yaml warning suppression
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* 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(autoresearch): pass Claude prompt via stdin to prevent shell injection
The modify() function in autoresearch/commands/run.ts interpolated a prompt
string — built from git log messages and scope file names — directly into
a shell command executed by execSync. The double-quote escaping only handled
literal quotes, leaving $(...), backticks and backslashes able to trigger
command substitution.
Switch to the same pattern already used in autoresearch/commands/fix.ts:
pass the prompt via the execSync 'input' option so it is delivered on stdin
and never parsed by the shell.
* fix(autoresearch): invoke Claude without a shell
---------
Co-authored-by: OpenCLI-sol <opencli-sol@users.noreply.github.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>
markdown table cells with | in them were breaking the table layout —
added escaping so pipes get rendered as \| properly.
also fixed a few things in CONTRIBUTING.md:
- the page.evaluate example had a template injection issue where user
input could break out of the template string. switched to passing
args through the function parameter instead.
- pipeline adapter example was missing the required access field, so
anyone following the guide would get a crash on registration
- removed a pointless .map(h => h) identity copy on table headers
- fixed consoleMessages('error') filter that was also returning warnings
* 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.