#2081 tried to make the real-browser AX smoke run everywhere via
--headless=new. That fixed headed macOS's Mach-port crash but exposed a
second environment property: hosted runners don't reliably start the MV3
extension service worker in headless, so main went red on macOS anyway.
Chasing headless reliability across runner images is the wrong axis.
First principles: gate each check on the environment that can run it
deterministically, and make sure every OS has a real blocking gate.
- Real-browser extension smoke (AX tree + cross-frame CDP): Linux under
xvfb is the one hosted environment where a real Chrome reliably starts
an MV3 extension. It runs there, headed, release-blocking. It is not
scheduled on macOS/Windows because neither can run it deterministically
(headed macOS crashes on Mach port rendezvous outside an Aqua session;
headless connects no SW).
- Daemon transport contracts: no browser, deterministic, so they run
blocking on all three OSes including Windows — macOS/Windows now have a
real gate over the exact layer our recent bugs lived in (#2067/#2070/
#2073), not a skipped test that proves nothing.
- Windows joins the matrix for the first time (transport gate); the
setup-chrome action is skipped there since it hangs on the MSI path and
Windows needs no browser.
Local Chrome launch stays headed by default; OPENCLI_E2E_HEADLESS=1 opts
into headless for display-less local runs.
The AX real-Chrome smoke's contract is "the extension bridge works in a
real Chrome", not "a window appears" — Linux already admitted that by
faking a display with xvfb. Hosted macOS runners fail headed Chrome at
the OS level (child processes lose the Mach port rendezvous with the
browser process because CI jobs run outside a regular Aqua session), and
PR #2079 papered over that by skipping the platform. Running the smoke
with --headless=new removes the display/GUI-session dependency entirely:
new headless is a full browser (MV3 service worker, chrome.debugger,
--load-extension), verified locally against the same Chrome for Testing
build CI uses. The smoke is release-blocking on every OS again; headed
mode stays available locally via OPENCLI_E2E_HEADED=1.
New daemon-transport contract E2E: the real dist/src/daemon.js process
with a scripted fake extension, pinning the cross-layer contracts end to
end — duplicate ids attach to the pending command without re-dispatch,
deadlines produce a structured 408 command_result_unknown, extension
death after dispatch yields command_result_unknown, a stale
preferredContextId falls back to the only connected profile while an
explicit contextId fails loud, and graceful shutdown flushes structured
daemon_shutting_down 503s with exit code 0. No browser required; runs in
the fixed-port project on every OS.
* 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>
* fix(browser): stale default profile must not veto live connections
A persisted default profile (browser-profiles.json defaultContextId) has a
lifetime that routinely exceeds the extension instance it names — reinstalling
the extension or resetting Chrome regenerates the contextId. Since #1235 that
stale preference was folded together with --profile/OPENCLI_PROFILE into one
hard contextId on every command, so the daemon refused to serve it
(profile_disconnected) even when exactly one live profile was connected,
breaking the documented promise "with only one connected profile, OpenCLI
uses it automatically" — and making doctor hang waiting for a dead profile.
First-principles fix: distinguish REQUIREMENT from PREFERENCE end to end and
let the component that knows live state arbitrate.
- profile.ts resolves a ProfileSelection tagged 'explicit' (--profile arg,
OPENCLI_PROFILE env — fail loud when offline) or 'preferred' (config
default); profileRouteParams() maps it to the wire fields.
- New wire field preferredContextId (both protocol copies); contextId keeps
its strict semantics. Old daemons ignore the new field, which degrades to
the no-contextId single-profile auto-use — exactly the documented behavior.
- The daemon arbitrates via a pure, tested resolveProfileRoute(): requested →
strict; preferred → use when connected, fall back to the only connected
profile when not (logged once per stale id), ask with a stale-default hint
when multiple are connected.
- bridge/ensure only pin readiness to a profile for explicit requirements —
a stale preference no longer makes connect()/doctor wait for a dead
profile.
- doctor surfaces the stale default with the fallback status and the
recovery command (opencli profile use).
* fix(cli): key saved-tab scope by the selected profile in getPageScope
From adversarial review: getBrowserPage computed the target scope from the
profile SELECTION (explicit or preferred), but getPageScope read only the
explicit Page.contextId — so with a config-default profile the remembered
tab was saved under "<session>" and looked up under "<contextId>:<session>",
silently forgetting the selection on every command. Both sites now key the
scope by the selected profile.
npm-installed CLIs on Windows are .cmd shims: `where` finds them (so the
installed-check passes), but Node refuses to spawn them directly since the
CVE-2024-27980 hardening — spawnSync fails with EINVAL/ENOENT and every
CLI-hub passthrough to an npm-installed tool breaks (#1958). On that
specific failure the passthrough now retries through the shell with each
token quoted for cmd.exe.
Also: a child killed by a signal left status null and opencli exited 0,
reporting success to the calling shell/agent; signal death now maps to a
non-zero exit code.
OpenCLIApp injects OPENCLI_DAEMON_PORT=19825 into the environment of every
CLI it manages. The CLI hard-rejected the variable regardless of its value,
so fresh OpenCLIApp installs failed on every command — including --version
and doctor — with EX_CONFIG, and the daemon never started (#2068, #2072).
A value equal to the default port carries no configuration at all; only a
NON-default value is a genuine misconfiguration worth failing on. All three
rejection points (main.ts entry, daemon startup, transport assert) now share
isIgnorableDaemonPortEnv().
Also fixes the README multi-profile example that was missing the required
browser <session> positional (#1893).
The "Create a file like clis/<site>/<command>.ts" example predates #928, which
converted the entire adapter layer from TypeScript to JavaScript. The repo now
ships 0 .ts and 1259 .js built-in adapters, so a contributor following the doc
creates a file in the wrong format. Update the example to JavaScript (keeping a
pointer to the still-supported TypeScript path), and fix the adapter test
command, which pointed at src/ rather than the adapter's own clis/ test file.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(transport): exactly-once command transport — journal, waiters, absolute deadlines
Rebuilds the CLI→daemon→extension command transport around one principle:
exactly-once = at-least-once retry + an idempotent executor. This replaces
the accumulated per-layer compensation (three client retry flags, cause-code
walking, duplicate-id 409s, an inner extension retry loop, a phased
reconnect state machine) with three small primitives:
1. Command journal (extension/src/journal.ts, chrome.storage.session).
Every command id executes exactly once: duplicates attach to the
in-flight promise, completed ids replay the recorded result, and ids
whose worker died mid-execution report `command_lost` honestly.
storage.session survives service-worker restarts and clears on browser
exit — precisely the lifetime a retry cares about.
2. Stable ids + daemon waiters. Transport retries keep the SAME id; the
daemon attaches duplicate ids to the pending command instead of 409ing.
With the executor idempotent, every transport failure becomes safely
retryable (gated on extension >= 1.0.22; legacy extensions keep the old
conservative pre-connect-only retry). Semantic retries (attach_failed /
tab_gone — failures BEFORE any page code ran) are the only place a new
id is minted, once.
3. Absolute deadline (`deadlineAt`, epoch ms) instead of per-hop durations.
Same machine, one clock: every layer computes remaining = deadlineAt -
now, so daemon queueing and service-worker wake latency no longer
silently shrink the innermost budget or invert the layering.
Error classification now happens once, at the failure site: the extension
tags results with machine-readable codes (attach_failed, tab_gone,
target_navigated, detached_mid_command, cdp_timeout) and errors.ts prefers
codes over the legacy message-pattern tables (kept only for old
extensions). detached_mid_command / cdp_timeout are now correctly
non-retryable — they die mid-execution, so a blind re-run could
double-apply a write.
Deletions and stability fixes riding the same contract:
- extension evaluate()'s inner retry loop (the client owns semantic
retries now); the fast/slow reconnect window + notifyDaemonReachable
rescheduling (plain exponential backoff with jitter, reset on success);
the three parallel session-override Maps (one record per lease).
- WS application-level keepalive ({type:'ping'} every 20s): Chrome 116+
only extends the service worker's lifetime on WS *activity*, so an idle
socket lived on a knife-edge between the 30s idle kill and the 30s
keepalive alarm.
- idle-lease release is deferred while a command is executing on the
lease (refcount) — a 30s idle timer can no longer tear the tab down
mid-command; completion re-arms the timer.
- daemon shutdown flushes structured 503s to waiting clients before
exiting instead of process.exit() killing the queued responses.
- results are delivered on the freshest open socket after a reconnect
instead of being dropped when the executing socket was superseded.
* fix(transport): gate daemon_shutting_down resend on journal capability; bound ensure by deadline
From adversarial review of the transport-v2 work:
- daemon_shutting_down was resent with the same id regardless of the
extension's journal capability. The daemon fires it for DISPATCHED
commands too, so on a pre-journal extension the resend re-executes a
write. The daemon now returns the pre-dispatch contract for commands
that never reached the extension (safe to resend anywhere) and
daemon_shutting_down only for dispatched ones; the client resends
those only when the extension journals ids, else surfaces
command_result_unknown.
- ensureBridge's connect wait was a fixed 45s regardless of the
command's remaining budget — repeated daemon failures could stretch a
30s --timeout command past two minutes. The wait is now clamped to
the remaining deadline.
* fix(browser): end-to-end command deadlines, safe transport retries, CDP timeouts
Three connectivity/stability fixes that share one root cause: the timeout
and retry contracts between CLI, daemon, and extension were disconnected.
1. Plumb one command deadline through all three layers. The client HTTP
request was hardcoded to 30s while the daemon default was 120s and no
caller ever set body.timeout — every command slower than 30s died with
an opaque client-side AbortError while still running in the browser.
Now the transport computes an effective timeout (user --timeout via
setDaemonCommandTimeoutSeconds, or timeoutMs + margin for extension-side
waits like wait-download), sends it as body.timeout, and aborts the HTTP
request only after the daemon's structured 408 should have arrived.
The daemon timer now rejects with the command_result_unknown contract
instead of a bare Error the client cannot classify.
2. Stop replaying possibly-dispatched commands on fetch TypeError. Any
`TypeError: fetch failed` used to trigger ensure + resend with a fresh
id, bypassing the daemon's duplicate-id guard — a daemon crash mid-click
could double-submit a form. Only pre-connect failures (ECONNREFUSED and
friends, checked via err.cause) are retried now; post-connect drops
surface as command_result_unknown per the existing contract.
3. Give chrome.debugger commands a real deadline. The extension's CDP
calls had none (sendCommandInFrameTarget declared _timeoutMs and never
used it), so a page-blocking native dialog (alert/confirm/beforeunload)
hung Runtime.evaluate forever and wedged every later command on the tab.
All sendCommand calls now race a timer; exec/cdp commands derive their
deadline from the transport's body.timeout, undercut by 5s so the more
specific extension error beats the daemon's generic timer.
* fix(browser): swallow post-timeout CDP rejections; short deadline for doctor probe
Two issues found in self-review of the deadline work:
- sendDebuggerCommand raced the command promise against a timer but left
the losing command promise unobserved — if it rejected later (debugger
detach on tab close long after the timeout fired) it surfaced as an
unhandled rejection in the service worker. Swallow it on a side branch.
- doctor's checkConnectivity probe inherited the default 120s transport
deadline, so a daemon that accepts requests but never answers made
doctor hang for 2 minutes before reporting FAIL. A health probe wants
the opposite: shrink the per-command deadline to the probe budget (8s)
and restore it afterwards.
* fix(extension): honor derived CDP deadline in evaluateInFrame warm-up; pin deadline tests
From adversarial review of the deadline work:
- evaluateInFrame's Runtime.enable warm-up on the frame-target fallback
path dropped the caller's derived deadline and fell back to the 60s
default — a blocked iframe could burn the whole daemon budget in the
warm-up alone, so the daemon's generic 408 always beat the extension's
specific error on the cross-frame path.
- Two untested links in the deadline chain are now pinned by regression
tests: the client HTTP abort fires exactly at timeout*1000 + 10s (not
before the daemon's structured 408 can arrive), and handleExec derives
115s from a 120s transport timeout (10s floor for tiny timeouts).
* 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>
Three independent CDP-layer correctness fixes:
1. Redirect wiped the captured POST body. On an HTTP 30x, CDP re-fires
Network.requestWillBeSent with the SAME requestId (the prior hop in
`redirectResponse`) for the redirect target — usually a GET with no
postData. The handler overwrote the entry's request-body fields
unconditionally, destroying the original POST body. Now the body is
only populated on the initial send (guarded on `!redirectResponse`).
2. responseReceived created orphan entries. If readNetworkCapture()
drained the entries (clearing requestToIndex) while a request was in
flight, the later Network.responseReceived ran getOrCreate and made a
new half-entry with a defaulted method ('GET') and no request data.
Now it is lookup-only, mirroring loadingFinished.
3. evaluateInFrame had no retry on a stale cached context. A navigated/
reloaded frame invalidates its cached execution-context id, but the
executionContextDestroyed event may not be processed yet, so
Runtime.evaluate rejects with "Cannot find context with specified id".
Now that rejection drops the stale id and falls through to the
frame-target path (mirrors evaluate()'s re-resolution); genuine page
errors still propagate.
Tests: redirect-body preservation, orphan-entry prevention, and
stale-context fallback — all reverse-validated. cdp suite 15/15, tsc
clean, extension/dist rebuilt.
A forced detach inside ensureAttached's re-attach loop fires
chrome.debugger.onDetach, whose handler deletes the tab's armed
networkCaptures state; the detach also disables the CDP Network domain,
and re-attach only re-issued Runtime.enable. So any non-navigate command
that triggered a re-attach (a stale-attach health-check failure during SPA
navigation, or third-party debugger interference) left
network-capture-read returning [] even though requests fired — the
recorded "0 captures" symptom.
Snapshot the capture before the re-attach and, on success, re-enable the
Network domain and restore the accumulated state (restored last so it
wins over the onDetach handler's delete). Adds a regression test that
fails without the restore.
reconcileTargetLeaseRegistry computed each lease's remaining lifetime
(stored.idleDeadlineAt - now) but used it only to decide expire-vs-keep;
the keep branch called resetWindowIdleTimer(leaseKey), which always
schedules a fresh FULL idle timeout, discarding the remaining time.
Under MV3 service-worker churn (the SW is evicted/restarted routinely),
a lease's idle deadline was refreshed to the full timeout on every
restart, so an owned adapter tab/placeholder that should auto-release
could linger far past its idle timeout — effectively indefinitely.
Add an optional remainingMs override to resetWindowIdleTimer and pass the
computed remaining from reconcile, clamped to [0, timeout]. Adds a
regression test (5s-remaining lease must schedule a ~5s alarm, not 30s);
reverse-validated.
Two unrelated try/catch blocks were eating errors with no observable
signal, both reachable from production paths:
1) `src/pipeline/template.ts:215` `sanitizeContext` (the JSON round-trip
that severs prototype chains before handing pipeline context to the VM
sandbox) caught any `JSON.stringify` failure and returned `{}`. The
most common cause is a BigInt anywhere in `data` / `args` / `item` /
`root` (e.g. GraphQL 64-bit IDs). After collapse, every template
expression referencing that branch resolved to `undefined`, producing
silent column-drops downstream with no warning.
Fix:
- Add a JSON.stringify replacer that coerces BigInt to string, so the
common BigInt-in-context case survives the sandbox copy.
- For everything else (circular references, Symbol, etc.), the
fallback is still `{}` but now log.warn so the failure shows up in
`~/.opencli` logs and doctor output instead of silently producing
blank rows.
2) `src/daemon.ts:445` the WS message handler from the extension caught
`JSON.parse` failures and ran the `// Ignore malformed messages`
comment. A malformed message presents downstream as a generic command
timeout (`pending` never resolves), so the actual protocol drift /
version skew between daemon and extension never surfaced in the log.
Fix: log.warn the parse error plus the first 200 chars of the offending
frame so the root cause is visible during triage.
Both changes are observability-only: no successful path changes behavior;
only previously-silent failure paths get a log line, plus BigInt now
serializes to a string instead of nuking its containing branch.
Tests:
- `src/pipeline/template.test.ts`: two new cases covering the BigInt
preservation path (forces the VM sandbox via `String(args.id)`, not the
resolvePath fast path) and the circular-ref no-crash invariant.
- daemon WS handler change is log-only; existing daemon tests cover the
message dispatch path.
Unify Browser Bridge active daemon ensure and per-command pre-dispatch recovery, harden MV3 extension reconnect cadence, and increase connect timeout headroom for Chrome alarm wake floor.
* feat(adapters): add 懂车帝 (dongchedi) + 瓜子二手车 (guazi) car adapters
Two no-login PUBLIC adapters for Chinese car platforms. Both read
server-rendered data (no cookies, no signature, no browser) and ship
pure parsers unit-tested against frozen real-data fixtures.
dongchedi (6 commands) — parses __NEXT_DATA__ SSR JSON:
search 车系搜索 + 指导价/经销商价
series 车系概览(品牌/价格/懂车分/销量排名/款型数)
models 款型列表 + 价格
specs 配置概览(尺寸/动力/四驱/悬挂/气囊)
score 懂车分 8 维评分 + 同级对比
koubei 车主口碑/评价正文
(Dongchedi's /motor XHR APIs are ByteDance-signature gated; the SSR
pages expose the same data unsigned, so the adapter reads those.)
guazi (2 commands) — parses m.guazi.com mobile SSR HTML:
browse 分城市在售二手车列表(售价/里程/年份)
car 车源详情(售价/上牌/里程/过户/配置/车况)
(Desktop www.guazi.com is signature-locked; mobile SSR is open. Deep
pagination/filtering uses the signed API and is intentionally omitted.)
Gates green: tsc, doc-coverage --strict, silent-column-drop (new=0),
typed-error-lint (no new), 24 adapter tests passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(adapters): add 汽车之家 (autohome) — brand catalog + 口碑 ratings
Third no-login PUBLIC car adapter (search by brand, not free text).
autohome (2 commands):
brand 按品牌列出全部车系 + 厂商指导价(grade/carhtml/<INITIAL>.html,
中文品牌名→拼音首字母目录页,DL 块按品牌定位)
score 车系口碑评分:总分 + 各维度 + 故障率PPH + 竞品对比
(k.autohome.com.cn/<id> 的 __NEXT_DATA__.baseData,免登录免签名)
Deliberately omitted (would be silently-wrong without a browser running
Autohome's signing/anti-scrape code): free-text keyword search (signature
gated) and full per-trim 参数配置 (rotating CSS font-glyph obfuscation).
Use dongchedi search/specs/koubei for those. Documented in the adapter doc.
Gates green: tsc, doc-coverage --strict (170/170), silent-column-drop
(new=0), typed-error-lint (no new); 31 adapter tests passing across the
three car adapters.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(car-adapters): fail closed on parser drift
* fix(guazi): fail closed on empty SSR listings
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(cli): split `opencli list` table into App vs Site sections
Per @WAWQAQ: `opencli list` (default table format) grouped every adapter
under a flat `site:` heading, so desktop-app adapters like `trae-cn`,
`cursor`, `codex` looked the same as web-site adapters like `bilibili`
or `twitter` — a user couldn't tell which entries drove a real browser
session vs an Electron app via CDP.
`opencli --help` already classified adapters via `classifyAdapter(domain)`
(from `src/help.ts`), grouping them into "App adapters" and "Site adapters"
sections; `opencli list` was the lone outlier still using the flat layout.
Mirror that classification in `list`:
- Walk commands once, partition each command's `site` group into
`appsBySite` or `sitesBySite` based on `classifyAdapter(cmd.domain)`.
- Render section headers ("App adapters" / "Site adapters") before each
group, then keep the existing per-site layout untouched.
- Update the summary footer from
`... across N sites, M external CLIs` to
`... across X apps + Y sites, M external CLIs`
so the split is visible numerically too.
- Skip empty sections — a user with only sites (no Electron apps) won't
see a stray "App adapters" header.
Non-table formats (json / yaml / md / csv) are unchanged; structured
consumers already get the `domain` field per row and can re-classify
themselves if they care.
`npx tsc --noEmit` clean; `npx vitest run --project unit src/cli.test.ts`
shows the same 157/163 pre-existing pass/fail counts as `main` (the 6
failing browser-tab targeting tests are unrelated, pre-existing on
`7af50abd`).
* fix(cli): classify loopback adapters as apps
* feat(github): add `github trending` adapter
Add a PUBLIC adapter that lists repositories from
https://github.com/trending — the trending view is a public HTML page with
no official REST API, so the data was previously unreachable through opencli.
The adapter fetches the page server-side (no browser, no auth) and parses
each repo's full name, description, primary language, total stars, forks,
and stars gained in the period.
Flags:
- `--since` daily | weekly | monthly (default daily)
- `--language` filter by language slug, e.g. python, rust, "c++"
- `--limit` 1..25 (GitHub lists at most 25)
Typed errors: ArgumentError for bad --since / --limit, CommandExecutionError
on request/HTTP failure, EmptyResultError when the page yields no repos.
Tests cover parsing (stars/forks/language/description/url, missing language),
limit truncation, language+since URL building, argument validation, the
empty-result path, and non-ok HTTP.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(github-trending): rename to standalone `github-trending repos`
Move the trending scraper out of the `github` site namespace into a
dedicated `github-trending` adapter to avoid confusion with the bundled
`gh` external CLI. Command is now `opencli github-trending repos`
(site=github-trending, name=repos), leaving room for a future
`developers` subcommand. The `github` site retains only login/whoami.
Regenerated cli-manifest.json; 8 fixture tests + typecheck pass.
* fix(github-trending): fail closed on parser drift
---------
Co-authored-by: minh <claude@ttfy.cc>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(12306): handle endpoint rotation via 302 redirects
Three related bugs caused the trains command to fail with
"non-JSON body" when 12306 rotated its query endpoint:
1. Node.js fetch defaulted to redirect: 'follow', silently
following 12306's HTTP 302 to error.html and returning
HTML instead of JSON.
2. The 302 response body contained rotation info
(e.g. {"c_url":"leftTicket/queryB"}) but was never read
because resp.status === 302 triggered continue before
consuming the body.
3. Mutating QUERY_ENDPOINTS via unshift() during a for...of
loop caused infinite iteration when the new endpoint was
skipped by the iterator.
Changes:
- Set redirect: 'manual' on fetch to capture 302 responses
- Parse c_url from 302 body and enqueue the rotated endpoint
- Replace for...of with a while queue + Set-based dedup to
safely handle dynamic endpoint discovery
Fixes the trains command against the current 12306 wire
protocol (queryG → 302 → queryB rotation).
* fix(12306): bound leftTicket endpoint rotation
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(gemini/history): expand collapsed Recents sidebar before extraction
opencli gemini history returned EMPTY_RESULT ("No conversation links were
visible in the sidebar") even when logged in, because Gemini collapses the
sidebar "最近"/Recents section by default — the /app/<id> conversation
anchors are absent from the DOM until that section is expanded.
getGeminiConversationList now retries extraction (up to 3 times) and, while
empty, clicks the sidebar-open button plus the Recents expand/collapse toggle
(matched by aria-label in both zh and en) before waiting for the React
sidebar to render and re-extracting.
Verified: opencli gemini history --limit 5 returns 5 conversations (2.5s);
opencli gemini detail <id> reads full conversation content.
Co-Authored-By: Claude <noreply@anthropic.com>
Generated on: cmcc-i5
Generated by: home-cc
* test(gemini): cover collapsed recents history extraction
* fix(gemini): avoid collapsing expanded recents
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat(chatgpt): add project commands
Add ChatGPT project management adapters for listing visible projects and uploading local files into project knowledge.
The new project-list command extracts project links from stable sidebar anchors first, with a React Fiber fallback for sidebar builds that do not expose hrefs. The project-file-add command uploads through the project knowledge flow, validates local files before browser interaction, and waits for filename confirmation before reporting success.
For the current ChatGPT project UI, project knowledge uploads live behind the Sources tab rather than the older Add files dialog. The upload helper now prefers that Sources surface, avoids mistaking the chat composer plus button for project knowledge upload, and dispatches a browser-like pointer/mouse sequence so Radix-powered tabs activate reliably in live sessions before setting the source file input.
This intentionally avoids command-level system proxy mutation. Users who need a proxy should configure the browser or network environment outside this adapter, rather than letting a single command toggle OS proxy settings.
Also update the generated CLI manifest and focused adapter tests for command registration, argument contracts, project id parsing, project link extraction, upload confirmation, live Sources-tab upload behavior, and failure wrapping.
Validation:
- pnpm exec tsx src/main.ts chatgpt project-list -f json --trace retain-on-failure --window foreground --keep-tab true (live authenticated local session; returned visible projects)
- pnpm exec tsx src/main.ts chatgpt project-file-add /tmp/opencli-chatgpt-project-upload-validation-pointer-20260621215147.txt --id 6a1791df8fa88191afb5a016ce1f497e -f json --trace retain-on-failure --window foreground --keep-tab true (live authenticated local session; uploaded one text file to project knowledge)
- pnpm exec vitest run --project adapter clis/chatgpt/commands.test.js clis/chatgpt/envelope.test.js clis/chatgpt/image.test.js clis/chatgpt/model.test.js clis/chatgpt/utils.test.js
- pnpm exec tsc --noEmit
- pnpm run build-manifest
* feat(chatgpt): support project chat routing
Add --project to chatgpt ask/send so messages can start a new chat inside a specified ChatGPT project. Reject --project with --conversation before navigation, and parse project-scoped /g/g-p-.../c/<id> conversation URLs so ask can report the created conversation id.
Validation:
- pnpm exec vitest run --project adapter clis/chatgpt/commands.test.js clis/chatgpt/envelope.test.js clis/chatgpt/image.test.js clis/chatgpt/model.test.js clis/chatgpt/utils.test.js
- pnpm exec tsc --noEmit
- pnpm run build-manifest
* feat(chatgpt): extend project routing
Add --project routing to chatgpt new, image, and model so project-scoped work is available beyond ask/send. New and image open the specified project before preparing the composer; model opens the project before switching the intelligence level.
Validation:
- pnpm exec vitest run --project adapter clis/chatgpt/commands.test.js clis/chatgpt/envelope.test.js clis/chatgpt/image.test.js clis/chatgpt/model.test.js clis/chatgpt/utils.test.js
- pnpm exec tsc --noEmit
- pnpm run build-manifest
- pnpm exec tsx src/main.ts chatgpt new --help / image --help / model --help
* fix(chatgpt): harden project command boundaries
* fix(chatgpt): require stable project ids
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(adapters): surface empty results as EmptyResultError, not sentinel rows
Four adapters returned a fabricated row (exit 0) on the not-found/empty
path instead of throwing a typed error, so an agent reading the exit code
or rows could not tell "no results" from success — the framework assigns
EMPTY_RESULT its own exit code precisely so this is detectable:
- maimai/search-talents: returned [{error, query}] on zero candidates.
Worse, `error`/`query` are not in `columns`, so the message was dropped
by column projection — the user saw an empty/garbage row, never the
reason. Now throws EmptyResultError (fixes the silent-column-drop too).
- discord-app/search: returned a synthetic "System" row on no matches.
- pixiv/download: returned a failed sentinel when an illust had 0 pages
(the file already throws typed errors elsewhere). Test updated to
assert the throw.
- xiaohongshu/download: returned a failed sentinel when a note had no
media (the file already throws CliError for the security-block branch).
Per-image partial-failure status rows in the download loops are left
as-is (legitimate batch reporting). Auth-path conversions
(tiktok/facebook string-prefix + maimai in-page throw) are a separate
follow-up since they involve in-page-throw handling.
Adapter suites green; typed-error-lint and silent-column-drop audits
report no new violations.
* fix(adapters): fail closed on malformed empty payloads
* fix(audit): bump undici in lockfile
* fix(pixiv): fail closed on missing pages payload
---------
Co-authored-by: codex-mini0 <codex-mini0@slock.local>