Commit Graph

1434 Commits

Author SHA1 Message Date
jackwener ce99dc31b1 fix(linkedin): harden connections identity handling 2026-07-12 01:11:36 +08:00
jackwener 7684438fcf feat(linkedin): add connections command to list first-degree connections
Adds `opencli linkedin connections` — lists your first-degree LinkedIn
connections (name, headline/occupation, public id, connected-at, profile URL)
via the voyager `/voyager/api/relationships/connections` REST endpoint.

- Reuses the shared JSESSIONID csrf-token + x-restli-protocol-version voyager
  fetch pattern; paginates start/count up to --limit (max 500).
- Typed errors: AuthRequiredError (missing session / 401 / 403),
  CommandExecutionError (malformed payload / missing miniProfile),
  EmptyResultError (no connections).

Live-verified end-to-end against a logged-in account (real connections with
occupation + profile URLs). 7 new tests; full suite 5959 passed; audits new=0.
2026-07-04 02:19:34 +08:00
jakevin 6129bb3953 ci(e2e): place each gate where its runner can run it deterministically (#2082)
#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.
2026-07-04 02:12:22 +08:00
jakevin 67344d5e36 test(e2e): run AX smoke headless on all platforms; add daemon transport contract E2E (#2081)
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.
2026-07-04 01:37:38 +08:00
jakevin cad35e7a6a chore(release): bump version to 1.8.6
Bump @jackwener/opencli to 1.8.6.
v1.8.6
2026-07-04 01:10:55 +08:00
jakevin 8de1184da6 test(e2e): treat mac AX bridge startup as optional
Keep Linux AX smoke release-blocking while allowing hosted macOS to skip when command-line unpacked extension startup is unavailable.
2026-07-04 00:55:09 +08:00
Zhongyue Lin 1db7b5f1e8 fix(twitter): match localized delete menu and poll for late-hydrating article (#2001) (#2026)
* 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>
2026-07-04 00:46:58 +08:00
jakevin f7ad36ba12 ci(e2e): pin macOS headed runner
Pin headed E2E macOS coverage to macos-15 while macos-latest migrates to macOS 26.
2026-07-04 00:44:03 +08:00
Zhongyue Lin 63db56d07b fix(weibo): resolve uid before the full auth probe to avoid HTTP 400 (#2047) (#2055)
* 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>
2026-07-04 00:42:23 +08:00
jakevin 338dc794a7 test(e2e): stabilize headed Chrome gate
Use stable Chrome for Testing for headed E2E and keep e2e child-process timeouts below Vitest's framework timeout.
2026-07-04 00:32:46 +08:00
jakevin 9387cd9262 fix(browser): stale default profile must not veto live connections (#2073)
* 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.
2026-07-03 22:13:32 +08:00
jakevin 18dce783da fix(external): run Windows .cmd shims through the shell; non-zero exit on signal death (#2075)
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.
2026-07-03 22:11:34 +08:00
jakevin 08d50d9b24 fix(cli): tolerate OPENCLI_DAEMON_PORT when it equals the default port (#2074)
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).
2026-07-03 22:10:09 +08:00
Adong 928b1e548d feat(hltv): add HLTV adapters (#2028)
* feat(hltv): add HLTV adapters

* fix(hltv): harden row identity contracts

---------

Co-authored-by: Adong <jhdong8855@gmail.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-07-03 20:50:22 +08:00
Louie 96cbeb4f65 docs: align adapter contribution example with the JavaScript adapter layer (#2017)
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>
2026-07-03 20:21:47 +08:00
jakevin bab040985a refactor(transport): exactly-once command transport — journal, waiters, absolute deadlines (#2070)
* 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.
2026-07-03 14:25:38 +08:00
jakevin 23cf6e5239 fix(browser): end-to-end command deadlines, safe transport retries, CDP timeouts (#2067)
* 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).
2026-07-03 13:35:09 +08:00
Marvin d6a7011454 Add ChatGPT Deep Research result extraction (#2023)
* 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>
2026-07-01 02:12:06 +08:00
Marvin e7bdad4783 Fix ChatGPT intelligence level selection (#2022)
* Fix ChatGPT intelligence level selection

* Document ChatGPT model adapter usage in browser skill

* fix(chatgpt): verify model config selection

* fix(chatgpt): classify model preference api drift

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-07-01 02:01:22 +08:00
jakevin 0a90179322 fix(extension): cdp network-capture + frame-eval robustness (#1984)
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.
2026-07-01 01:35:33 +08:00
lizkaiman 215a73dc56 feat(gemini): add model and thinking selection (#2044)
* feat(gemini): add model and thinking selection

Co-authored-by: multica-agent <github@multica.ai>

* fix(gemini): harden model selection contracts

---------

Co-authored-by: coder-SOTA-hm <coder-SOTA-hm@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-07-01 01:33:43 +08:00
jakevin 1f8b09bd1c fix(extension): preserve network capture across ensureAttached re-attach (#1978)
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.
2026-07-01 01:31:25 +08:00
jakevin 9a4e11d3fa fix(extension): honor persisted remaining idle lifetime on reconcile (#1980)
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.
2026-07-01 01:26:25 +08:00
iynewz d174f724ec feat(adapter): add Mercury reimbursement helpers (#2052)
* Add Mercury reimbursement helpers

* fix(mercury): fail closed reimbursement drafts

* fix(mercury): harden reimbursement draft safety checks

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-07-01 01:25:09 +08:00
jakevin 244ec45278 fix(core): stop silently swallowing pipeline context + daemon WS errors (#1979)
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.
2026-07-01 01:22:06 +08:00
陈家名 5d2e87ad16 test(slock): cover trimmed task status filters (#2041) 2026-07-01 01:05:06 +08:00
jakevin b3695a2468 chore(browser): share bind command handling (#2043) 2026-07-01 00:59:34 +08:00
jakevin 01022d9c09 chore(daemon): read status through transport layer (#2040) 2026-07-01 00:59:18 +08:00
jakevin 52396b2da2 chore(browser): extract network interceptor script (#2042) 2026-07-01 00:59:03 +08:00
jakevin 046712ab42 chore(extension): bump version to 1.0.21 (#2039) ext-v1.0.21 2026-06-28 10:30:20 +08:00
jakevin 9161d99d96 chore(release): bump version to 1.8.5 (#2038) v1.8.5 2026-06-28 02:12:13 +08:00
jakevin 616cc88603 test(e2e): isolate fixed-port browser tab tests (#2037) 2026-06-28 01:54:40 +08:00
jakevin cbb9e23394 test(e2e): align browser tests with fixed bridge port (#2036) 2026-06-28 01:22:51 +08:00
jakevin fc4c7c151f test(extension): isolate background reconnect timers (#2035) 2026-06-28 00:49:37 +08:00
jakevin 6b99276a8d fix(twitter): default tweet page delay to two seconds
Change twitter tweets pagination delay default from 1s to 2s and update manifest/tests.
2026-06-27 23:52:31 +08:00
jakevin b056e420b1 fix(twitter): allow paginated tweet backfills
Remove the silent 200-row cap from twitter tweets, add paginated request delay support, and cover >200 cursor pagination.
2026-06-27 23:24:29 +08:00
jakevin 70629fb9b0 fix(bridge): unify active ensure and reconnect recovery
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.
2026-06-27 21:55:03 +08:00
jakevin ba2dcc0dcc fix(browser): enforce fixed daemon bridge port (#2031) 2026-06-27 21:51:54 +08:00
Louie df8ca8d440 feat(xianyu): search 服务端价格区间 / 地区筛选 (mtop API) (#2013)
* feat(xianyu): search 支持服务端价格区间 / 地区筛选(--min-price/--max-price/--province/--city)

闲鱼 search 之前只有 query + limit。本次改成直接调 goofish 自己的搜索接口
`mtop.taobao.idlemtopsearch.pc.search`(沿用 item.js 里 window.lib.mtop.request
的用法,签名由页面自带,无需手搓),把价格区间和地区交给服务端筛选 + 分页,
而不是抓一屏 DOM 再在本地过滤。

筛选编码是在登录态浏览器里 hook window.lib.mtop.request、实际操作筛选面板抓到
真实请求后逆出来的,并逐条用接口返回 + 详情接口 publishCity 做了 ground-truth 校验:

- --min-price / --max-price → propValueStr.searchFilter = "priceRange:<min>,<max>;"
  (元;单边区间用 0 / 99999999 兜底)。实测 priceRange:100000,150000 返回价格
  全部落在 112999–149900。
- --province / --city → extraFilterValue = JSON({divisionList:[{province,city}],
  excludeMultiPlacesSellers:"0",extraDivision:""})。city 可单独使用(province 留空)。
  实测「广东」「北京」结果集完全不相交;--city 深圳 / 湛江 的结果用详情接口校验
  publishCity 全部命中。
- 任一筛选生效时 fromFilter=true。limit 最多 60,按需翻页(rowsPerPage=30)。

返回字段改为结构化解析(item_id / title / price / location / want / url),
鉴权与风控错误处理沿用 item.js 的成熟逻辑(AuthRequired / Empty / mtop-not-ready)。

已在登录态浏览器里真跑验证(深圳+10–20万、湛江、无筛选基线均正确),
clis/xianyu 45 个单测全绿,tsc --noEmit 通过,cli-manifest.json 同步重建。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(xianyu): harden search filter result handling

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-06-27 18:28:47 +08:00
Bo Liu 1dd712d609 feat(juejin): add Juejin (掘金) read-only adapter (#2007)
* feat(juejin): add Juejin (掘金) read-only adapter

Two PUBLIC commands for the Juejin developer community: `recommend` (homepage feed) and `hot` (article ranking by category). Native fetch against api.juejin.cn; no browser, no auth. Category aliases (`backend`, `frontend`, `android`, `ios`, `ai`) resolve to Juejin's stable numeric ids.

Closes #1711

* fix(juejin): fail closed on API shape drift

* fix(juejin): expose recommendation cursor

* fix(juejin): classify response cursor drift

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-06-24 06:18:24 +08:00
Louie ee4820ef59 feat(adapters): add Chinese car-platform adapters — 懂车帝 / 瓜子二手车 / 汽车之家 (no-login) (#2009)
* 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>
2026-06-24 06:07:01 +08:00
Ocean 55b053a6f6 feat(bilibili): 给 video / subtitle / download 加 --page 支持分P选集 (#2003)
* feat(bilibili): 给 video / subtitle / download 加 --page 支持分P选集

多P视频(视频选集)此前 video / subtitle / download 都丢弃 `?p=` 参数、永远解析到 P1 的 cid / 标题 / 字幕 / 视频流。新增可选 `--page N`:

- **video**:从 view API 的 `data.pages` 取第 N 集,`title` 换成分集标题,额外透出 `page` / `cid` / `series_title` 字段;缺省不加,保持整集旧行为
- **subtitle**:用 `pages[N-1].cid` 取代默认 P1 cid,拿该集字幕
- **download**:拼 `?p=N` 给 yt-dlp 原生定位该集
- **utils**:共享 `parsePageArg` / `selectVideoPart`(越界结构化报错)

向后兼容:不传 `--page` 时三命令行为完全不变。新增 9 个测试,bilibili 全量 109 通过;`tsc --noEmit` 干净;`check:silent-column-drop` new=0。

* fix(bilibili): harden multipart page selection

---------

Co-authored-by: ml-scout <ml-scout@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-06-24 05:47:25 +08:00
jakevin 80548c7de9 feat(cli): split opencli list table into App vs Site sections (#2000)
* 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
2026-06-23 15:47:46 +08:00
leo1in88 7af50abd04 feat(github): add github trending adapter (#1953)
* 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>
2026-06-22 21:27:57 +08:00
songsp77 e5bbce4fe2 fix(12306): handle endpoint rotation via 302 redirects (#1999)
* 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>
2026-06-22 18:00:13 +08:00
Mingming Lou 5848754943 fix(gemini/history): expand collapsed Recents sidebar before extraction (#1962)
* 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>
2026-06-22 16:53:42 +08:00
pi-dal 19e64da565 feat(chatgpt): add project commands (#1992)
* 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>
2026-06-22 16:26:59 +08:00
Bo Liu acfab26a52 feat(semanticscholar): add Semantic Scholar academic graph adapter (#1994)
* feat(semanticscholar): add Semantic Scholar academic graph adapter

Native fetch against api.semanticscholar.org. Four read commands cover paper detail (with influentialCitationCount and tldr), citation list, AI-curated recommendations, and free-text search. Optional SEMANTIC_SCHOLAR_API_KEY env var lifts the anonymous rate limit.

Closes #1993

* fix(semanticscholar): fail closed on malformed paper rows

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-06-22 16:25:11 +08:00
jakevin 22bedf21eb docs(readme): recommend OpenCLIApp install path 2026-06-22 03:01:19 +08:00
jakevin 6557622156 fix(adapters): surface empty results as EmptyResultError, not sentinel rows (#1981)
* 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>
2026-06-21 15:44:19 +08:00