The original opencli-usage skill was deleted in PR #1094 as part of the
skill consolidation, but its role (top-level orientation to what opencli
is, how to discover adapters, what flags/env/formats are universal, and
which specialized skill to load next) was not covered elsewhere. Restore
it, but deliberately NOT as a verbatim copy:
- Drop the hand-maintained 100-adapter table. There are 100+ adapters
and the list moves every week — `opencli list -f json` is the source
of truth agents should call at the start of a task.
- Replace it with the meta-structure agents actually need: the three
pillars (adapters / browser driving / external CLI passthrough), the
strategy tags (PUBLIC | COOKIE | HEADER | INTERCEPT | UI | LOCAL)
and what each implies for prerequisites, universal flags (-f, -v),
output formats, env vars, self-repair hook, adapter authoring paths,
plugins, external CLI passthrough.
- Explicitly list the commands PR #1094 removed (`explore`, `record`,
`web` / `desktop` top-level groups) so agents don't attempt them.
- Cross-link to the four post-consolidation skills: opencli-browser
(ad-hoc driving), opencli-adapter-author (writing adapters),
opencli-autofix (repair flow), smart-search (search routing).
Adapter-author description updated to stop claiming it replaces
opencli-usage.
Backfill the 1.7.6 section that was missing from the release PR.
Covers window lifecycle flags, selector-first browser interactions,
agent-native payload, compound form fields, three new adapter commands,
four fixes, skill doc updates, and extension 1.0.2 body-truncation
contract unification.
--live (OPENCLI_LIVE=1) keeps the automation window open after an adapter
command finishes, so agents or humans can inspect the page state. Default
behavior (immediate closeWindow) is unchanged.
--focus (OPENCLI_WINDOW_FOCUSED=1) surfaces the existing env-var toggle as a
CLI flag so users don't need to shell-export to see the window in foreground.
Both flags are parsed early in main.ts and stripped from argv, so they can be
placed anywhere on the command line and work on any subcommand (adapter or
browser).
Restore `skills/opencli-browser/SKILL.md`, deleted in #1094, rewritten for
the post-#1116 browser CLI surface: selector-first target contract,
`match_level { exact | stable | reidentified }`, compound fields for
date/time/select/file, structured error codes with `available` vs
`candidates`, new `find` / `extract` / `network --filter` commands,
html tree budgets, tabs/frames, cost guide, recipes, pitfalls.
Review tightened two contract-drift bugs before merge:
- `browser tab list` envelope field is `page`, not `targetId`
- `network --ttl` default is `24h`, not `~5min`
2 reviewers green (codex-mini1, First-principles-1); CI all-green.
* feat(browser): compound expansion + cascading stale-ref + bbox 0.99 dedup
Three agent-native upgrades inspired by browser-use, landed as one PR
because they share the same target / snapshot / find surface.
1. Compound expansion (compound.ts)
Date/time/datetime-local/month/week, select, and file inputs now
emit a `compound` JSON field on `browser find --css` entries with
format, current value, min/max (date family), full options list
+ selected (select), accept / multiple / files[] (file). Kills
the three biggest form-page failure modes (wrong date format,
guessed options, re-uploaded files) without extra round-trips.
2. Cascading stale-ref (target-resolver.ts)
Numeric ref resolution now walks three tiers before giving up:
exact → stable (tag + strong id match, soft signals drifted) →
reidentified (original ref lost, fingerprint uniquely found a
live element; re-tag + refresh identity). Every success envelope
carries `match_level` so callers can tell which tier matched.
SPA re-renders / i18n label swaps no longer stall agents.
3. BBox 0.99 containment for interactive descendants (dom-snapshot.ts)
Adds a second dedup tier on top of the existing 0.95 non-interactive
one. When a parent is a propagator (tag a/button OR role button/
link/menuitem/tab/option) and a child is interactive but
undistinctive (no aria-label/id/testid/name/form-control), fold
it into the parent — removes `[1]<button> [2]<svg> [3]<span>`
noise on icon buttons.
Tests: 287/287 pass (src/browser + src/cli.test.ts). Typecheck clean.
* fix(browser): address reviewer blockers on PR #1116
- compound select: walk ALL options to collect selected labels, not just
the first 50 we serialize. Fixes dropdowns where the selected entry
sits past COMPOUND_SELECT_OPTIONS_CAP (e.g. country lists, timezones)
reporting current: "" even though the user picked a valid option.
- match_level: propagate the cascading match tier
(exact / stable / reidentified) through IPage.click/typeText/scrollTo,
BasePage, and the cli command envelopes (click / type / select /
get text|value|html|attributes). Agents now see in JSON that the
resolver had to fall back, instead of the tier being swallowed.
- compound contract is now also emitted by `browser state`
(per-ref compounds: sidecar) and by `browser get html --as json`
(compound field on each node), not only by `browser find --css`.
Closes the gap where agents using the default snapshot still
round-tripped `find` for every date / select / file control.
Adds targeted regression tests for each blocker + updates cli.test.ts
mocks to the new envelope shape.
The resolveTwitterQueryId() function in shared.js fetches an external JSON
file from GitHub without a timeout. If the network request stalls, the
function never resolves and the twitter article command hangs indefinitely.
Add a 5-second AbortController timeout so the fetch fails fast and falls
back to the local script-scanning strategy. This fixes the reported hang
when opencli twitter article loses network connectivity.
* feat(bilibili): add video command
Add `opencli bilibili video <bvid|url|short-link>` to fetch one
video's metadata via the public /x/web-interface/view endpoint.
Returns title, author, category, publish time, duration, view /
danmaku / reply / like / coin / favorite / share counts, parts,
thumbnail, and description as a key/value table.
Reuses `resolveBvid` and `apiGet` from clis/bilibili/utils.js to
stay consistent with the existing bilibili adapters
(subtitle/search/etc. all follow the same navigate + apiGet
pattern). Non-zero API codes surface as CommandExecutionError.
Fills a visible gap: existing bilibili commands cover search,
hot, subtitle, ranking, user-videos etc., but nothing returned
metadata for a single video — `web read` only gets a DOM shell.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(manifest): register bilibili video command
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* WIP: deepseek file upload (blocked by 30s idle timeout)
* feat(deepseek): add file upload support via --file flag
Closes#1092
* fix(deepseek): use native file input path for --file
---------
Co-authored-by: Benjamin Liu <beneecs@Benjamins-Mac-mini.local>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(youtube): fall back to Videos tab when Home tab has no videos (#1108)
Some channels have no video shelves on their Home tab, causing
`opencli youtube channel <id>` to return an empty `recent_videos` list
even though the channel has videos visible in the browser.
When the Home tab extraction finds zero videos, the command now makes
a second InnerTube browse request to the Videos tab and extracts from
its richGridRenderer format.
* fix(youtube): make Videos tab fallback locale-safe
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(browser): selector-first find + get/click/type/select with JSON envelope
A2: new `browser find --css <sel>` — structured JSON (matches_n + entries[]) so
agents can go from semantic selector directly to a list of candidates without
parsing free-text snapshot output. Per-entry shape: nth/ref/tag/role/text/attrs/
visible. Attr whitelist kept small (11 high-signal fields), invisible elements
still returned so agents can reason about offscreen vs missing.
A3: get text/value/attributes now accept a selector-first <target> (numeric ref
OR CSS) and emit `{value, matches_n}`. Bonus scope (approved by reviewers):
click/type/select share the same contract with `--nth <n>`, emitting
`{clicked|typed|selected, target, matches_n, ...}` on success.
Unified structured error envelope across all selector-first commands:
{ error: { code, message, hint?, candidates?, matches_n? } }
with codes invalid_selector / selector_not_found / selector_ambiguous /
selector_nth_out_of_range (CSS) plus not_found / stale_ref (numeric ref).
Write commands reject multi-match CSS without `--nth` as selector_ambiguous;
reads default to "first match wins" but always expose matches_n so agents
notice ambiguity. `resolveTargetJs` is the single source of truth; click /
typeText / scrollTo share a `runResolve` helper in BasePage.
No back-compat shims per design directive.
125 targeted tests green; tsc clean.
* fix(browser): unify selector surface + allocate fresh refs in find
Two blockers from PR #1112 review:
1. First-principles-1 (blocker): `browser find --css` now allocates fresh
numeric refs for untagged matches. It scans `window.__opencli_ref_identity`
(and any stray `data-opencli-ref` attrs) for the current max, allocates
`max+1` upward, writes `data-opencli-ref` on the element, and populates
the identity map with the same fingerprint shape snapshot uses (tag,
role, text, ariaLabel, id, testId). `find -> click <ref>` now works on
fresh pages without requiring `browser state` first. Type changed from
`ref: number | null` to `ref: number`.
2. codex-mini1 (blocker): removed the `isCssLike` regex
(`^[a-zA-Z#.\[]`) in `resolveTargetJs`. Valid selectors like `:root`,
`:has(...)`, `*` used to short-circuit to "Cannot parse target" before
reaching `querySelectorAll`, so `find --css` accepted them but
`get/click/type/select` did not. Now: numeric → ref path, everything
else → querySelectorAll, and the browser parser decides. Same selector
surface across all selector-first commands.
Tests added:
- target-resolver: pseudo-selectors flow into CSS branch (not rejection)
- find: ref allocation writes attribute + identity map; fingerprint shape matches resolver
- cli: find envelope now expects numeric refs
127 targeted tests green; tsc clean.
* feat(browser): agent-native payload — network bodies, html tree budgets, extract command
Three fixes/additions driven by agent-usage gaps, as one complete change:
- network (P0 fix): lift silent 4000-char body truncation in CDP + extension
paths to an 8MB memory-guard cap, and surface body_truncated / body_full_size
/ body_truncation_reason in the --detail envelope so the agent sees when a
body was cut. List view also exposes body_truncated_count and per-entry flag.
Adds --max-body flag for explicit caller-side capping.
- get html --as json (P1): add --depth / --children-max / --text-max budget
knobs on the tree serializer, plus a truncated={depth,children_dropped,
text_truncated} envelope that only appears when a budget is hit. Lets the
agent narrow DOM output without walking away empty-handed.
- extract (P2 new command): agent-native article/content channel. Scope →
denoise (strip nav/header/footer/scripts/forms/etc.) → HTML→markdown via
existing htmlToMarkdown → paragraph-boundary-aware chunk with stateless
next_start_char resume cursor. Agents no longer misuse `get html` to read.
* fix(browser): unify body-truncation signal contract across raw/detail/fallback
Addresses review blockers on #1104:
- NETWORK_INTERCEPTOR_JS fallback no longer silently drops bodies above the
per-entry cap. Raised cap to 1 MiB (ring stays at 200 entries), and on
overflow keeps the string prefix + sets `bodyTruncated` / `bodyFullSize`
so `browser network` propagates the same agent-visible signal the CDP /
extension paths emit.
- `CachedNetworkEntry` schema switches from internal camelCase
`bodyTruncated` to the user-facing `body_truncated` / `body_full_size`
fields. `--raw` emits cache entries verbatim, so this removes the
snake_case/camelCase split across list / --detail / --raw.
- Adds a `--raw` truncation-contract test that also asserts the camelCase
fields do not leak through.
Agents often know what fields a target request's body should contain
but not which captured request carries it. --filter lets them declare
the field set and get back only matching entries.
Matching is "any-segment": a field matches when it equals any segment
name of any inferShape() path (ignoring root $, array indices, and
bracket-quoted key syntax). Multiple fields AND together. Case-sensitive.
- invalid_filter for empty / commas-only values
- invalid_args when combined with --detail (mutually exclusive)
- 0 matches is a valid empty result, not an error
- persisted cache stays unfiltered so later --detail lookups still resolve
Envelope gains `filter` (echo) and `filter_dropped` (count of entries
passing the static-resource filter but not --filter). Existing --raw
and --all compose normally.
* feat(browser): remove silent html truncation, add --as json tree output
`browser get html` had two agent-hostile defaults:
1. A silent 50000-char cap on the returned HTML — agents that got a
truncated page had no signal they were looking at half the DOM.
2. Only raw HTML string output, forcing agents to re-parse for
structured extraction.
Changes:
- Default output is now the full outerHTML, no truncation
- `--max <n>` opts in to a character cap; when the cap actually
trips, the HTML is prepended with
`<!-- opencli: truncated N of M chars; re-run without --max ... -->`
so agents always see the signal
- `--as json` returns `{selector, matched, tree}` where `tree` is
`{tag, attrs, text, children}` recursively. `matched` is the full
count of selector matches so agents know when more elements exist
beyond the first. `text` is the node's own direct text children,
whitespace-collapsed; child elements live in `children`.
- `--selector` not matching any element now emits structured
`{error:{code:"selector_not_found", ...}}` with a non-zero exit
code, in both raw and json modes (was `(empty)` stdout previously,
indistinguishable from empty element)
- Invalid `--as` / negative `--max` emit structured
`invalid_format` / `invalid_max` error codes
Extracted the tree serializer as `src/browser/html-tree.ts` so the
JS expression can be unit-tested against a DOM stub.
* fix(browser get html): structured errors for invalid selector & strict --max
Both edges previously bypassed the structured-error contract introduced in
#1102, which agents rely on for branching:
- Invalid CSS selector: querySelector(All) would throw SyntaxError through
page.evaluate into the generic exception path. Wrap the lookup in try/catch
inside page context for both raw and --as json paths; surface as
{error:{code:"invalid_selector", message}} + non-zero exit.
- --max validation: parseInt silently accepted "1.5" -> 1 and "10abc" -> 10.
Switch to a strict /^\\d+$/ check so fractional, negative, and non-numeric
values all return {error:{code:"invalid_max"}}; validation runs up front so
bad values never reach the page.
Covered by new unit tests in cli.test.ts (fractional, non-numeric, invalid
selector on raw + json) and html-tree.test.ts (SyntaxError -> invalidSelector
envelope).
Co-authored-by: freemandealer <freeman.zhang1992@gmail.com>
---------
Co-authored-by: freemandealer <freeman.zhang1992@gmail.com>
* feat(browser): rewrite network command for agent-native discovery
Replace the index-based list + pretty-printed --detail flow with a
structured JSON interface built around stable keys, body-shape previews,
and a persistent capture cache. Agents can now reference captured
requests by operationName (GraphQL) or `METHOD host+pathname` (REST)
instead of array indexes that shift on every rerun.
- `browser network` now emits JSON: `{workspace, captured_at, count,
filtered_out, entries: [{key, method, status, url, ct, size, shape}],
detail_hint}` — no body payloads by default
- Shape inference (src/browser/shape.ts) walks response JSON into a
flat path -> descriptor map with depth cap 6 and a 2KB budget per
entry, so agents see structure without paying body tokens
- Stable key generator (src/browser/network-key.ts) derives
`operationName` from graphql URLs and `METHOD host+pathname`
elsewhere, disambiguating collisions with `#N` suffixes
- Persistent cache (src/browser/network-cache.ts) snapshots every
capture to `~/.opencli/cache/browser-network/<workspace>.json` with
a 24h TTL, so `--detail <key>` survives later commands
- `--detail <key>` returns `{key, url, method, status, ct, size, shape,
body}` with structured error codes (cache_missing / cache_expired /
cache_corrupt / key_not_found, the latter including available_keys)
- Add `--raw` for agents that want every full body inline, `--ttl` for
cache lookups
- Update opencli-adapter-author + opencli-autofix skill docs to
reference `--detail <key>` and the shape-first discovery flow
Supersedes the cache prototype in #1051.
Co-authored-by: freemandealer <freeman.zhang1992@gmail.com>
* fix(browser): structured errors for capture/save, shape budget guard
Self-review findings on the network refactor:
- captureNetworkItems throwing (browser crashed / CDP dropped) now emits
`error.code: capture_failed` on stdout rather than leaking a bare
stderr line from browserAction's generic handler — agents get a
parseable JSON blob on every failure path, matching the design goal.
- saveNetworkCache throwing (disk full, read-only path) is a soft
failure: the captured data is already in hand, so surface a
`cache_warning` field in the envelope and keep going instead of
aborting. `--detail` lookups on that run will miss the cache but the
listing still reaches the agent.
- shape.ts: guard the sub-walk on `add()`'s return value so the
"budget hits on the array/object descriptor itself" path can never
emit a stray child without its parent marker.
- network-key.ts: document that `#N` suffixes start at `#2` — the first
occurrence stays bare, there is no `#1`. Matches test + code.
Added regression tests: `capture_failed` on readNetworkCapture throw,
`cache_warning` on persistence failure, shape budget hit on array descriptor.
Co-authored-by: freemandealer <freeman.zhang1992@gmail.com>
---------
Co-authored-by: freemandealer <freeman.zhang1992@gmail.com>
* feat(twitter): add tweets command for fetching a user's recent posts
Adds `opencli twitter tweets <username> [--limit N]` to pull a user's
most recent chronological tweets via the UserTweets GraphQL endpoint.
Long posts resolve via note_tweet, pinned entries are skipped, and
retweets are flagged. QueryIds resolve dynamically through
`resolveTwitterQueryId` with hardcoded fallbacks.
* fix(twitter): expose retweet flag in tweets output
* refactor: consolidate 6 skills into 3, remove mechanical commands
Replaces opencli-oneshot / opencli-explorer / opencli-browser /
opencli-usage with a single opencli-adapter-author skill that takes
the AI agent end-to-end: site recon, API discovery, field decoding,
adapter coding, and `opencli browser verify`.
Removes the mechanical commands (`explore`, `synthesize`, `generate`,
`cascade`, `record`) and their src/tests — they were codegen scaffolding
meant for agents, which the new skill handles more flexibly via
`opencli browser` primitives.
Skill highlights:
- Top-level decision tree + 12-step runbook
- 5 site patterns (SPA / SSR / JSONP / Token / Streaming)
- 5-layer API discovery (network → initial state → bundle → token → interceptor)
- Field decode playbook (self-explanatory → codes → sort-key comparison)
- Output design guide (columns, types, order, ≤15 per adapter)
- Two-layer site memory: in-repo seeds for eastmoney/xueqiu/bilibili/tonghuashun
plus local `~/.opencli/sites/<site>/` runtime workspace
Kept skills: opencli-autofix (now points to adapter-author for rewrites),
smart-search. Kept primitives: `browser *`, `doctor`, `list`, `validate`,
`verify`, `<site> <cmd>`, `plugin *`, `completion`.
No backward compatibility shims. Full test suite (1605 tests) passes.
* review fixes: honest coverage, hard memory-hit path, typo, stale docs
- site-memory hit path no longer jumps to writing adapter; forces Step 5
endpoint re-verification + Step 7 field check, and 30-day expiry
- site-memory.md now specifies exact schemas for endpoints.json /
field-map.json / notes.md / fixtures + write-back timing rules
- coverage-matrix.md marks unverified patterns as 🟡 with an evidence
section citing coingecko dry run + PR #1091 eastmoney + bilibili
- eastmoney seed typo: resolveSecids -> resolveSecid (and splitSymbols)
- docs/developer/ai-workflow.md rewritten to teach the adapter-author
skill + opencli browser * primitives (dropped generate/synthesize/
cascade/explore references)
- ts-adapter.md, getting-started.md, CHANGELOG.md:87 updated to point
at opencli-adapter-author
* fix(ci): resync package-lock + drop stale built-in list reference
- Regenerate package-lock.json to restore @emnapi/core + @emnapi/runtime
entries that got dropped during the rebase — `npm ci` was failing on all
CI jobs (build / audit / docs-build / bun-test / unit-test)
- docs/guide/getting-started.md: built-in list dropped `explore`, now
reads (list, validate, verify, browser, doctor, plugin...)
* fix(ci): restore package-lock.json from main (unrelated lockfile churn)
Mirror the remaining 13 read-oriented adapters and the shared _secid.js
helper from the author's local workspace into the repo, so that
clis/eastmoney/ becomes the full Phase A codegen regression oracle
described in OpenCLI Improvement Spec v1.1 §B.10.
Total repo oracle after this PR: 14 adapters under clis/eastmoney/
(hot-rank.js already exists; this PR adds the other 13) plus the
_secid.js normalize helper.
Covers the two schema-expressiveness gaps discovered during prep:
- CSV row_format: kline.js decodes "YYYYMMDD,open,close,..." strings
- :row_index source: convertible.js derives rank = i + 1
_secid.js is the canonical example of the v1.1 §B.7 helper contract
(pure normalize/derive function, serializable I/O, no env/fs/net/session
access, does not drive pagination/retry/fallback).
This PR is oracle-only, carries no framework changes. Phase A framework
PR depends on this merging first so the codegen diff target is stable.
Refs: task #177 / spec v1.1 §B.10
* feat(download): show saved file path in web read and weixin download output
Closes#1038
* test(download): cover saved article path
---------
Co-authored-by: Benjamin Liu <beneecs@Benjamins-Mac-mini.local>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(browser): add cross-origin iframe support via CDP execution contexts
Enable interaction with cross-origin iframes through CDP's execution
context mechanism, without requiring content scripts or all_frames.
- Track frame execution contexts via Runtime.executionContextCreated events
- Add 'frames' action to list all child frames (including cross-origin)
- Support frameIndex in 'exec' action to evaluate JS in specific frames
- Add Page.frames() and Page.evaluateInFrame() APIs for CLI consumers
- Tag cross-origin iframes with [F0]/[F1] indices in DOM snapshots
- Add Page.getFrameTree to CDP allowlist
Closes#1077
Change-Id: Id03361ddb616912dff3bfa8e59e8b68716de590b
* fix(browser): align cross-origin iframe routing contract
* fix(browser): unify iframe frame-index routing
---------
Co-authored-by: xuezhangying <xuezhangying@bytedance.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
From first principles, `opencli browser` commands exist for AI Agents to
operate websites through the browser skill. Reframe both READMEs to reflect
this: show users how to install the skill into their AI agent and describe
tasks in natural language, rather than listing raw CLI commands.
* feat(twitter): rewrite lists via GraphQL + add list-tweets
The DOM-scraping / detail-click approach in PR #1053 remained fragile
against X's frequent overview-page rendering changes and slow (N+1 page
loads per list). Rewrite `twitter lists` to call
`ListsManagementPageTimeline` GraphQL directly — one request returns all
owned + subscribed lists with id/name/member_count/subscriber_count/mode.
Also add `twitter list-tweets <listId>` for pulling the tweet stream from
a list, completing the read-side chain (lists → pick an id → list-tweets).
- lists: drop positional `user` arg (GraphQL returns only logged-in
user's lists), add `id` column, change followers to exact integer from
subscriber_count.
- list-tweets: same GraphQL pattern as bookmarks/likes (BEARER + ct0 +
dynamic queryId with static fallback + cursor pagination).
- Delete obsolete lists-parser.js and lists.d.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(twitter): add list-add / list-remove with Save-button commit
Two new commands to toggle list membership. X's list dialog uses a
"click-to-stage, Save-to-commit" pattern — clicking a row only updates
optimistic UI; the actual POST fires when the user clicks the top-right
"Save" button. Pressing ESC or the close-X silently cancels the change.
Implementation:
- Resolve listId → name via ListsManagementPageTimeline GraphQL, so we
match the dialog row by name (dialog rows have no data-testid listId).
- Open profile page → DOM click "…" menu → "Add/remove from Lists".
- Scroll dialog to locate target row (virtualized list).
- page.nativeClick on row — trusted CDP Input.dispatchMouseEvent fires
React's onclick, flips aria-checked (.click() alone does not suffice;
X ignores non-trusted events for list mutations).
- page.nativeClick on the Save button — commits to server.
- Verify by re-fetching ListsManagementPageTimeline and diffing
member_count: success only if N→N±1. No silent successes.
This fixes the pattern where batch `list-add` calls returned success for
every user but committed zero to the server (optimistic UI lied).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: stabilize twitter list manifest and query ids
* docs: add twitter list command discoverability
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(zsxq): separate content field from title, remove title truncation
- Split getTopicText to return only title, add getTopicContent for body text
- Remove .slice(0, 120) that was truncating titles
- content field now contains full body text instead of duplicating title
* fix(zsxq): preserve title fallback for body-only topics
---------
Co-authored-by: huzekang <huzekang@opencode.ai>
Co-authored-by: jackwener <jakevingoo@gmail.com>
PR #1059 migrated xiaoyuzhou from SSR scraping to authenticated API.
The E2E tests run without credentials, producing exit code 78
(CONFIG_ERROR). The existing `isExpectedChineseSiteRestriction` guard
only caught FETCH_ERROR, PARSE_ERROR, and NOT_FOUND — not config
errors from missing auth credentials.
Three improvements from the design debt audit:
1. Remove deprecated `tabId` field and `getActiveTabId()` method
- Delete `tabId` from DaemonCommand (daemon-client.ts) and Command (protocol.ts)
- Delete `getActiveTabId()` from IPage interface (types.ts) and Page class (page.ts)
- Update extension resolveCommandTabId() to remove legacy fallback
- Update handleTabs select case to remove tabId check
- The tab→page migration is now complete
2. Unify argument validation into single code path
- Remove `normalizeArgValue()` from commanderAdapter.ts
- Commander adapter now passes raw values to prepareCommandArgs()
- All coercion (bool, int, number) and validation (required, choices)
happens once in coerceAndValidateArgs() in execution.ts
- Eliminates duplicated boolean normalization
3. Remove dead plugin filesystem wrappers
- Delete `promoteDir()` — never called in production code
- Delete `replaceDir()` — thin wrapper over beginReplaceDir, never called
- Remove corresponding test-only exports and tests
- Rename PromoteDirFsOps → ReplaceDirFsOps to match remaining usage
- Transaction infrastructure (runTransaction, beginReplaceDir,
beginReplaceSymlink) retained — used by publishStandalonePlugin
and publishMonorepoPlugins for atomic multi-step operations
* fix(extension): per-workspace idle timeout for browser sessions (#1058)
The global 30s WINDOW_IDLE_TIMEOUT was too aggressive for interactive
`opencli browser` commands where users type manually between invocations.
- browser:*/operate:* workspaces now default to 10 min idle timeout
- Adapter workspaces keep the existing 30s timeout
- Support custom timeout via OPENCLI_BROWSER_TIMEOUT env var (seconds)
or command-level idleTimeout parameter
- Surface sessionExpired warning when a new window is created after
the previous session timed out
- Fix stale comment (said 120s, actual was 30s)
Closes#1058
* fix: resolve sessionExpired double-delete race and timeout override lifecycle
Addresses @codex-coder review blockers:
1. sessionExpired flag was never set because getAutomationWindow()
consumed expiredWorkspaces before handleCommand() could check it.
Fix: use .has() in getAutomationWindow, only .delete() in handleCommand.
2. workspaceTimeoutOverrides was never cleaned up — once set, it
persisted until extension restart. Fix: clear override on idle
timeout expiry, explicit close-window, and borrowed-session detach.
Adds 5 tests covering:
- browser:* uses 10min timeout (not 30s)
- sessionExpired flag is set and consumed correctly
- workspaceTimeoutOverrides cleared on idle expiry
- workspaceTimeoutOverrides cleared on explicit close
- idleTimeout from command applies to workspace override
* refactor: remove sessionExpired warning per product decision
@WAWQAQ decided session-expired warning is not needed.
Remove expiredWorkspaces tracking, sessionExpired flag from protocol,
and related CLI-side warning code. Keep per-workspace timeout and
override lifecycle cleanup.
* fix: clean up workspaceTimeoutOverrides on user-initiated window close
The windows.onRemoved listener was missing workspaceTimeoutOverrides
cleanup, causing stale overrides to persist across sessions when users
manually close the automation window.
* fix(xiaoyuzhou): migrate from broken SSR scraping to authenticated API (fixes#1023)
Xiaoyuzhou removed SSR rendering — /podcast/<id> and /episode/<id> pages
now return 404, breaking fetchPageProps() which scraped __NEXT_DATA__.
Migrate podcast, podcast-episodes, episode, and download commands to use
the existing authenticated API client (requestXiaoyuzhouJson) that
transcript.js already uses successfully.
Changes:
- podcast.js: use /v1/podcast/get API endpoint
- podcast-episodes.js: use /v1/podcast/listEpisode API endpoint
- episode.js: use /v1/episode/get API endpoint
- download.js: use /v1/episode/get API endpoint
- utils.js: remove unused fetchPageProps, keep format helpers
- Update all affected tests (download.test.js, utils.test.js)
- Change strategy from PUBLIC to LOCAL (requires credentials)
* fix(xiaoyuzhou): align local strategy contract
* fix(xiaoyuzhou): align local api metadata
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
#1025 merged hot-rank adapters for eastmoney/tdx/ths but the
accompanying docs were missing. This breaks the Doc Check CI
workflow on every PR ('--strict' mode, exits non-zero when
`scripts/check-doc-coverage.sh` finds adapters without docs),
blocking merges across the board.
Adds a doc page per adapter, registers them in the adapters
index table, and adds sidebar entries in the VitePress config.
* feat: add hot stock ranking adapters for eastmoney, tdx, ths
Add three new site adapters for Chinese stock hot rankings:
- eastmoney/hot-rank: 东方财富热股榜
- tdx/hot-rank: 通达信热搜榜
- ths/hot-rank: 同花顺热股榜
All use Strategy.COOKIE browser mode with page.evaluate() DOM scraping.
Each includes co-located tests (13 tests total, all passing).
* fix(tdx,ths): add symbol validation and deduplication in evaluate()
Add seen Set for deduplication and skip entries with empty symbol/name,
matching the pattern already used in eastmoney/hot-rank.js.
* fix: refine hot-rank selectors based on browser inspection
- eastmoney: use table.rank_table tbody tr with td index-based extraction,
fix name from a[title] to avoid post content contamination
- tdx: use div.top-cell[data-code] data attributes for reliable extraction,
add tags column from div.tips-item.gnbk
- ths: use card-based layout selectors, remove price column (not in UI),
extract tags from div.tag.PFSC-R
* fix(hot-rank): align tdx and ths columns with actual output
* fix: register hot stock ranking adapters
---------
Co-authored-by: dengjingren <dengjingren@cn.wilmar-intl.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>