mirror of
https://github.com/jackwener/OpenCLI.git
synced 2026-09-14 18:25:42 +08:00
feat/evaluate-function-overload
259 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
14b1dc86fc | feat(browser): add function form page evaluate | ||
|
|
0e168d570e |
refactor(browser): replace --session flag with <sessionname> positional (#1505)
* refactor(browser): replace --session flag with <sessionname> positional The `--session <name>` flag was semantically required but syntactically optional, which is an anti-pattern. Required + flag is a contradiction: flag form implies "optional", required is a runtime patch on top. Session is OpenCLI's "operation target" identifier — the natural form for that is a positional argument, like `docker exec <container> <cmd>` or `git checkout <branch>`. New surface: opencli browser <sessionname> open https://x.com opencli browser <sessionname> click 12 opencli browser <sessionname> bind opencli browser <sessionname> unbind Commander 14 cannot natively combine a parent positional with subcommand dispatch — the parent's positional is shadowed by subcommand matching. To bridge that, main.ts now pre-processes argv: when the token after `browser` is non-flag and not a known subcommand name, it is treated as the sessionname and rewritten to the internal `--session <name>` flag form before commander parses it. Help text on the `browser` command is overridden via `.usage('<sessionname> <command> [options]')` so users see the positional form. Reserved subcommand names (33) are listed in cli-argv-preprocess.ts and tested for parity with cli.ts subcommand registrations. If a future subcommand is added, the test fails loudly. Synced surfaces: - README.md / README.zh-CN.md — all examples - docs/guide/browser-bridge.md (+ zh) - skills/opencli-browser/SKILL.md (bind/unbind, examples, table) - skills/opencli-usage/SKILL.md - tests/e2e/browser-tabs.test.ts - CHANGELOG.md (Unreleased BREAKING) The internal `--session` flag and the unit tests calling `program.parseAsync(['...', 'browser', '--session', 'foo', ...])` are preserved as a stable internal API: tests bypass main.ts pre-processing and exercise commander directly. The pre-processor has its own targeted test file (cli-argv-preprocess.test.ts, 10 tests, all green). Verification: - npx tsc --noEmit — pass - npx vitest run --project unit — 1073/1074 pass (1 unrelated skip) - npx vitest run --project extension — 61/61 pass - npm run check:typed-error-lint — baseline 189 - npm run check:silent-column-drop — baseline 103 * fix(cli-argv): only rewrite when `browser` is the root command The preprocessor was looping through every argv slot and would mis-rewrite occurrences of the literal word `browser` deeper in argv (e.g. `opencli adapter init browser/x` or arg values containing `browser`). Now the preprocessor walks past leading root flags + their values to identify the root command token, and only acts when that token is `browser`. The full set of root value-consuming flags (`ROOT_VALUE_FLAGS`) is documented inline and kept in sync with the `program.option()` calls in cli.ts. Adds regression tests: - `opencli adapter init browser x` not rewritten - URL/path values containing `browser` not rewritten - `list browser state` (different root command) not rewritten - `--profile work browser foo state` correctly identifies `foo` as sessionname (not as --profile's value) - `--profile=work` long-form-with-equals consumes one slot only - boolean flags (`-v`) don't consume the next value 12/12 preprocessor tests pass. * fix(cli-argv): hide --session flag, fail-fast on retired form, rename to <session> Three blockers in #1505 review: 1. `--session` flag was still visible in `opencli browser --help` and could be used as a public entrance, contradicting "positional only" UX. Fix: switch from `.requiredOption()` to `.addOption(new Option(...).hideHelp())`. The flag is preserved as an internal API for the daemon protocol and direct `program.parseAsync` callers (tests), but is no longer documented or surfaced in structured help. 2. `opencli browser --session foo state` still succeeded. Now the argv preprocessor throws `BrowserSessionArgvError` when root `browser` is followed by `--session`, and main.ts catches it and exits with a user-facing usage error pointing to the positional form. 3. Missing-session error message exposed the internal flag: `required option '--session <name>' not specified`. Now `getBrowserSession()` in the action body throws `<session> is a required positional argument: opencli browser <session> <command>`, and commander no longer guards the hidden option. Also (per @WAWQAQ) rename placeholder `<sessionname>` -> `<session>` everywhere user-facing — shorter, matches CLI convention. The help text "<session> is a required positional: pass the name of the browser session..." carries the "name" semantics in description, not in the placeholder itself. Sync surfaces: - src/cli.ts — usage line, addOption with hideHelp, descriptions - src/cli-argv-preprocess.ts — throw on --session form - src/cli-argv-preprocess.test.ts — refusal test for old form - src/cli.test.ts — assertions updated for hidden option + new error path - src/help.ts — read `_usage` private field to respect `.usage()` override (commander's `.usage()` getter returns auto-generated form if not set, which would otherwise pollute every namespace's usage string) - src/main.ts — catch BrowserSessionArgvError, stderr + exit - README.md / README.zh-CN.md - docs/guide/browser-bridge.md / docs/zh/guide/browser-bridge.md - skills/opencli-browser/SKILL.md / skills/opencli-usage/SKILL.md - CHANGELOG.md Manual smoke tests (against built dist): - `opencli browser --help` shows `Usage: opencli browser <session> <command> [options]` - `opencli browser --help` Options block does NOT show `--session` - `opencli browser --session foo state` → friendly error, no commander stacktrace - `opencli browser state` → `<session> is a required positional argument: opencli browser <session> <command>` - `opencli browser foo state` → parses correctly * fix: inject <session> into subcommand help paths and drop stale sessions ref Two follow-up blockers from #1505 review: 1. Subcommand help and structured help still rendered the command path without the parent's positional. `opencli browser foo state --help` showed `Usage: opencli browser state [options]`, which would lead users (and agents reading structured help) to think `opencli browser state` was a valid invocation. Now: - `commanderPath()` injects an ancestor's leading-positional placeholder (extracted from its `.usage()` override) between the ancestor's name and the next path segment when building paths upward. - `commandPathFromRoot()` strips placeholder segments (e.g. `<session>`) from the relative `name` field so agents can still address subcommands by their leaf name; placeholders remain in the `command` / `usage` display paths. - `program.configureHelp({ commandUsage: ... })` is applied recursively to every descendant of `browser`, because commander does NOT inherit `configureHelp` into subcommands. Result: opencli browser <session> click --help -> Usage: opencli browser <session> click [target] [options] Daemon, plugin, adapter, profile namespaces (no `.usage()` override) are unaffected. 2. `skills/opencli-browser/SKILL.md` still referenced `opencli browser sessions`, which was removed in #1470. Replaced the sentence with the underlying invariant ("Bound sessions have no OpenCLI idle-close timer; the binding lasts until `unbind`, tab close, window close, or daemon restart") without mentioning the deleted command. Tests: - cli.test.ts: structured help expectations updated to include `<session>` in command/usage paths (3 tests) - cli-argv-preprocess.test.ts: 12 tests still green - 1136/1137 unit+extension green (1 unrelated skip) - typed-error-lint baseline 189 - silent-column-drop baseline 103 |
||
|
|
fa9b38cd92 |
feat(reddit): add whoami, home, subreddit-info read commands (#1491)
* feat(reddit): add whoami, home, subreddit-info read commands Closes gap against jackwener/rdt-cli — three commands the existing 17 reddit adapters were missing: - `reddit whoami` — show the currently logged-in identity (fields: Username, ID, Post / Comment / Total Karma, Account Created, Gold, Mod, Verified Email, Has Mail, Inbox Count). Probes `/api/me.json` with two-pronged auth detection (401/403 OR `data.name` missing on 200 — Reddit returns 200 with an empty body for stale anon sessions, see PR #1428). - `reddit home` — personalized Best feed (`/best.json`). Distinct from the public `frontpage`/`r/all` command: enforces login via the same two-pronged auth check rather than silently degrading to the unauthenticated default feed. `--limit` accepts [1, 100] — out-of-range raises `ArgumentError` before navigation, no silent clamp. - `reddit subreddit-info` — subreddit metadata (Name, Title, Subscribers, Active Now, NSFW, Type, Description, Created, URL) from `/r/<X>/about.json`. Banned / private / quarantined / 404 subreddits raise `EmptyResultError` so the output table never holds a silent sentinel row. All three use Strategy.COOKIE + siteSession:'persistent' matching the existing reddit adapters, validate args upfront before `page.goto`, and use the 5-kind discriminated-union pattern (kind: auth/http/missing/ exception/ok) from PR #1428 to map page.evaluate results to typed errors on the Node side. Intermediate object keys deliberately avoid the declared columns (`field`/`value`/`rank`/etc.) per the silent-column-drop audit sediment from PR #1329. Tests: 28 new (whoami 6, home 9, subreddit-info 13); full reddit suite 38/38. Audits: typed-error-lint 189/189 (0 new), silent-column-drop 103/103 (0 new). Manifest 812 → 815. Refs: https://github.com/jackwener/rdt-cli * fix(reddit): tighten new read command failure contracts * fix(reddit): treat inaccessible subreddit info as empty |
||
|
|
eb59b7444d |
feat(ctrip): add hotel-search + flight browser-mode commands (#1481) (#1489)
* feat(ctrip): add hotel-search + flight browser-mode commands Closes #1481. Two new browser-mode commands on top of the existing public `search` / `hotel-suggest` pair: - `ctrip hotel-search <city> --checkin --checkout [--limit]` reads `window.__NEXT_DATA__.props.pageProps.initListData.hotelList` on `hotels.ctrip.com/hotels/list`. SSR-rendered first page ships ~13 entries; the server ignores `&pageSize=N` so limit caps at 30 with default 10. AuthRequiredError surfaces when Ctrip redirects to the captcha gate. - `ctrip flight <from> <to> --date [--limit]` searches one-way flights on `flights.ctrip.com/online/list/oneway-…`. The post-load XHR is not currently captured by the daemon network buffer (per the known daemon_capture_pipeline_bug_2026_05_07 in agent memory), so rows are pulled from `.flight-list > span > div` cards via a position-anchored innerText parser. A generic `buildScrollUntilJs(selector, target)` helper mirrors the PR #1487 xiaohongshu scroll-until pattern with the selector parameterised. Round-trip + airline filters are out of scope for v1. All argument validation (IATA / ISO date / city ID / limit range) fires upfront before any `page.goto`, per the PR #1387 boundary standard. No silent clamps, no sentinel rows: rows missing required fields are dropped, and end-state checks raise `ArgumentError` / `AuthRequiredError` / `EmptyResultError` as appropriate. The new `mapHotelRow` / `pickHotelMapCoords` / `buildFlightExtractJs` / `buildScrollUntilJs` helpers live in `clis/ctrip/utils.js` alongside the existing suggest helpers. Docs at `docs/adapters/browser/ctrip.md` now distinguish the public suggest commands from the browser-mode commands and document each command's columns + caveats. Verified: - 61/61 vitest tests in `clis/ctrip/ctrip.test.js` (including JSDOM exercises of `buildFlightExtractJs` and full `mapHotelRow` shape parity) - `check:typed-error-lint` 189/189 (0 new) - `check:silent-column-drop` 103/103 (0 new) - `build-manifest` clean — 812 entries total (was 810) * fix(ctrip): harden browser search failure contracts * fix(ctrip): tighten browser empty-vs-parser failures |
||
|
|
150551be8c |
feat(reddit): add reply command for replying to comments (#1428)
* feat(reddit): add reply command for replying to comments Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(reddit/reply): replace silent-sentinel rows with typed errors reply.js originally mirror-copied comment.js's failure pattern: returning [{ status: 'failed', message: 'HTTP 403' }] on auth/HTTP/Reddit errors and relying on the caller to inspect the row instead of throwing. That's the 'silent-sentinel' anti-pattern from typed-errors.md — failures should surface as typed errors so an agent can actually branch on them. Round 21 lesson (f) — "grandfathered-not-exempt + helper-refactor boundary is new" — applies: comment.js / upvote.js / save.js can stay grandfathered, but a brand-new file does not inherit that exemption. Changes: - Throw AuthRequiredError when /api/me.json or /api/comment returns 401/403, or when /api/me.json returns 200 but data.name is missing (stale anon session — empty modhash alone isn't a strong enough signal). - Throw CommandExecutionError for non-2xx HTTP and for non-empty data.json.errors (e.g. RATELIMIT, NO_TEXT, TOO_OLD). - Drop the over-defensive `if (!page) throw ...` — registry guarantees a page object when browser:true. - Intermediate result object uses `kind` discriminator + `detail` / `httpStatus` / `where` keys that don't overlap with columns ['status','message'], so the silent-column-drop audit stays quiet (per PR #1329 sediment). Verified: - npx tsc --noEmit clean - node scripts/check-typed-error-lint.mjs → 189/189, 0 new - node scripts/check-silent-column-drop.mjs → 103/103, 0 new - npx vitest run clis/reddit src/convention-audit → 11/11 pass - node ./dist/src/main.js validate → 0 errors Success path is unchanged: still returns [{ status: 'success', message: 'Reply posted on t1_<id>' }]. * fix(reddit): harden reply command contract * fix(reddit): reject suffixed reply urls --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: jackwener <jakevingoo@gmail.com> |
||
|
|
64ac362a40 |
feat(rednote): add rednote.com adapter mirroring xiaohongshu read commands (#1136) (#1475)
* feat(rednote): add rednote.com adapter mirroring xiaohongshu read commands (#1136) Implements rednote.com support as discussed in issue #1136. The mainland xiaohongshu adapter stays in place; international users redirected to www.rednote.com now have a CLI without a copy-pasted adapter. Issue #1136 documents that xiaohongshu and rednote share DOM selectors, URL paths, API paths, response schema, cookies, and the xsec_token auth mechanism. The only material differences: Layer xiaohongshu rednote Web host www.xiaohongshu.com www.rednote.com API host edith.xiaohongshu.com webapi.rednote.com Security host fe-static.xhscdn.com as.rednote.com Cookie root .xiaohongshu.com .rednote.com Search gate Inline text Full-screen modal + text ## Architecture (minimal) `clis/xiaohongshu/*` keep all selector / regex / extraction logic. Each command file is touched minimally to export the IIFE or pipeline so the sibling adapter can reuse it: search.js + export const buildSearchExtractJs(webHost) + export const command = cli({...}) note.js + export const NOTE_EXTRACT_JS + export const command = cli({...}) comments.js + export function buildCommentsExtractJs(withReplies) + export parseCommentLimit + export const command = cli({...}) download.js + export function buildDownloadExtractJs(noteId) (CDN allowlist now includes rednote alongside xhscdn) + export const command = cli({...}) user.js + export const USER_SNAPSHOT_JS + export const command = cli({...}) feed.js + export function buildFeedPipeline(webHost) + export const command = cli({...}) notifications.js + export function buildNotificationsPipeline(webHost) + export const command = cli({...}) note-helpers.js buildNoteUrl now accepts `cookieRoot` + `signedUrlHint` options (defaults preserved so xhs callers and tests are unchanged) user-helpers.js buildXhsNoteUrl / extractXhsUserNotes accept an optional `webHost` argument (default xhs) The `export const command = cli({...})` pattern matches twitter/lists.js and clis/discord-app/*; without it the build-manifest scanner attributes xhs's command to whichever rednote sibling triggered the transitive import first. ## clis/rednote/ — thin shims Each rednote command file imports the relevant builder / constant from its xiaohongshu sibling and calls `cli()` with the rednote host triple. No selectors, regexes, or extraction logic are duplicated. search.js imports buildSearchExtractJs + noteIdToDate declares its own WAIT_FOR_CONTENT_JS (modal + text login-gate variants — the one xhs behaviour that genuinely differs) note.js imports NOTE_EXTRACT_JS + buildNoteUrl + parseNoteId comments.js imports buildCommentsExtractJs + parseCommentLimit + buildNoteUrl + parseNoteId download.js imports buildDownloadExtractJs + buildNoteUrl + parseNoteId user.js imports USER_SNAPSHOT_JS + extractXhsUserNotes + normalizeXhsUserId ## Scope (initial) Ships the five commands verified live against the user's logged-in rednote.com session: search / note / comments / user / download. `feed` and `notifications` are intentionally left out. Both rely on intercepting the xiaohongshu Pinia store at the `homefeed` / `you` capture pattern; live verification on rednote returns `tap → dict (error)` for the feed step, so shipping them would surface a broken contract. The mainland xiaohongshu commands continue to work. Adding the rednote-side feed / notifications is straightforward follow-up work once someone with rednote access maps the network surface. Creator-center commands (publish, creator-*) have no rednote counterpart and stay xiaohongshu-only, per the reporter's note in #1136. ## Verification - clis/xiaohongshu/ + clis/rednote/: 103/103 tests green - npx tsc --noEmit: clean - npm run build: 807 manifest entries (xhs 13 + rednote 5 + everything else preserved) - silent-column-drop / typed-error-lint: 103 / 189 baseline entries, no new violations - Live verify against the user's rednote.com session: rednote search "travel" --limit 1 → real note row rednote note <signed-url> → 7 field/value rows rednote comments <signed-url> --limit 3 → 3 top-level rows rednote user 5b21f6564eacab3b38f05c39 --limit 2 → 2 profile notes Spaced 15–30s between runs per the xhs/rednote rate-limit guidance; no write commands invoked. Regression check: xiaohongshu/feed on the existing mainland session still returns the standard 6-field rows after the refactor. Closes #1136 * fix(rednote): tighten adapter failure boundaries --------- Co-authored-by: jackwener <jakevingoo@gmail.com> |
||
|
|
b262d8ffd5 |
feat(chatgpt): support local image uploads (#1476)
* feat(chatgpt): support local image uploads * chore: refresh cli manifest * fix(chatgpt): harden image upload flow * fix(chatgpt): validate image uploads before navigation * fix(chatgpt): keep send fallback click in sync --------- Co-authored-by: jackwener <jakevingoo@gmail.com> |
||
|
|
467fdd0b62 | refactor(adapter): rename site browser reuse to persistent sessions (#1462) | ||
|
|
9c06e84c89 | refactor(browser): replace workspaces with sessions (#1461) | ||
|
|
674f0e1105 |
feat(openreview): add author command for ID-explicit publication lookup (#1365)
* feat(openreview): add author command for ID-explicit publication lookup
Closes the missing leaf in the openreview adapter. Among the public-strategy
academic adapters, dblp and arxiv both already ship an `author` command for
ID-explicit publication lookup; openreview only had `search` (full-text),
`paper` (detail by note id), `reviews` (thread by forum id) and `venue`
(listing by invitation / venue text). There was no way to ask "give me every
submission this author put on OpenReview, newest first."
`openreview author <profile>`:
- takes a canonical profile id (`~First_LastN`); validated by
`requireProfileId` so a dblp PID or a bare name fails before any
network call,
- hits `/notes?content.authorids=~<id>&limit=<n>&sort=cdate:desc`,
- returns rank-ordered rows with the same shape as `openreview search`
(id / title / authors / venue / pdate / url),
- throws `EmptyResultError` when the profile has no public submissions
instead of returning an empty list,
- inherits the typed-error envelope from `openreviewFetch` so network
failure, non-200, malformed JSON, and in-band error envelopes all
surface as `CommandExecutionError`.
Tests: 6 new `it` blocks plus 1 updated registration test in
`clis/openreview/openreview.test.js`.
- `requireProfileId` (1 block, 9 assertions): accepts canonical
`~First_LastN`, `~Bo_Liu17`, and a multi-segment middle-name id;
rejects empty, whitespace, missing tilde, missing trailing number,
embedded space, and a dblp-style PID.
- 5 author runtime cases covering pre-network ArgumentError, empty
result, non-200, fetch network error, and the happy path with a
request-shape assertion (`content.authorids` filter + `cdate:desc`
sort).
- Registration test extended to expect five commands and lock the new
`columns` contract.
Manifest auto-regenerated to register the new command.
Live-verified end to end against `~Yoshua_Bengio1`: the most recent ICLR
2026 workshop submissions return with the expected fields. A malformed
profile is rejected before any HTTP call. A nonexistent profile yields
`EMPTY_RESULT`.
* fix(openreview): accept real profile id slugs
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
|
||
|
|
aa6696f6ce | feat(browser): add annotated screenshot refs (#1433) | ||
|
|
accdd970a4 |
test(browser): add real Chrome AX smoke (#1445)
* test(browser): add real Chrome AX smoke * fix(browser): attach cross-origin frame targets directly * fix(browser): resolve frame target by URL * test(browser): include frame target URL in AX smoke * fix(browser): discover iframe targets before routing * fix(browser): resolve iframe targets through CDP * fix(browser): auto-attach iframe targets for routing * test(browser): make cross-origin AX smoke a capability probe * docs(browser): mark cross-origin AX as best-effort * ci(browser): keep AX smoke out of normal e2e sweep |
||
|
|
1364a11ab2 |
feat(browser): add semantic locators to input actions
Add semantic locator flags to browser type/fill/select while preserving explicit target syntax. |
||
|
|
19976723c1 |
feat(browser): route AX refs through cross-origin frames
Route AX snapshot and AX ref click CDP calls through attachable cross-origin frame targets. Bump Browser Bridge extension to 1.0.9 for frame target routing. |
||
|
|
65903a09ff | feat(browser): wait for downloads (#1441) | ||
|
|
bfe7116e82 | feat(browser): extend semantic locators to actions (#1440) | ||
|
|
4e4bef6474 | feat(browser): add drag command (#1439) | ||
|
|
98fcce7bd3 | feat(browser): add upload command (#1438) | ||
|
|
6e1c56e1e6 | feat(browser): add check and uncheck (#1437) | ||
|
|
b69b2e384d | feat(browser): add hover focus and dblclick (#1435) | ||
|
|
76b34b7e87 |
feat(browser): add semantic locator flags (#1434)
* feat(browser): add semantic locator flags * fix(browser): report semantic read match totals |
||
|
|
70981ef06a | docs(browser): document AX validation workflow (#1416) | ||
|
|
53516f7511 |
docs(browser): design agent runtime roadmap (#1411)
* docs(browser): design agent runtime roadmap * docs(browser): tighten runtime MVP criteria |
||
|
|
644d45177b |
feat(twitter): add unlike + retweet + unretweet + quote (write-action symmetry P0) (#1400)
Round 21 P0 — Twitter write-action symmetry (4 of 4: unlike, retweet, unretweet, quote). ## Scope Closes write-action gap with existing siblings (`like`, `bookmark`, `unbookmark`, `delete`): - `unlike` (UI strategy, navigateBefore:true) - `retweet` (UI strategy) - `unretweet` (UI strategy) - `quote` (UI strategy, `/compose/post?url=` route — same family as `reply.js` `/compose/post?in_reply_to=`) +745/-0 in initial commit, plus 3 progressive review fixes. Final: 4 adapters + 4 tests; modified `shared.js`, `shared.test.js`, manifest, docs. ## Iteration history (4 heads, 102/102 tests on final) - `07836783` — initial 4 adapters + 4 tests, 96/96 - `55a89776` — fix #1: shared `parseTweetUrl()` URL invariant + quote post-submit verify (102/102) - `dc9eab66` — fix #2: article-scoping for unlike/retweet/unretweet (delete.js sibling pattern) - `8809d2c1` — fix #3: exact status-id matching (`match?.[1] === tweetId`) + quote-card exact id guard ## 4 progressive blockers caught (codex-mini0 lead + F-P-0 aux) 1. **URL validation (silent-clamp class)**: original passed any host containing `/status/<id>`. Fixed: `parseTweetUrl()` requires `https` + Twitter/X exact host + exact `/<user|i>/status/<id>` path; host-suffix, embedded URL, path-suffix all `ArgumentError` pre-nav. 2. **Quote silent-success illusion**: original click-implies-success without composer/toast verify. Fixed: pre-submit quoted-card exact id render assertion + post-submit success toast OR composer-clear assertion, otherwise return failed row. 3. **Broad querySelector scoping (delete.js sibling pattern)**: original state probe + click + post-click verify on conversation pages picked first matching button. Fixed: scope to `article` containing requested exact status id (sibling `clis/twitter/delete.js:22-23` pattern). 4. **Substring vs exact status-id matching**: `/status/123` substring-matched `/status/1234`. Fixed: regex `/\/status\/${id}(?:\/|$)/` segment-edge anchor + `match?.[1] === tweetId` exact compare. ## Cultural sediment (Round 21) **Audit checklist 5 rules (pre-write upstream selection net)**: 1. cross-grep sibling URL-construction patterns before adopting 2. silent-clamp class detection (any normalize-then-trust path) 3. broad querySelector → article-scoping requirement 4. missing-validation early reject before navigation/IO 5. ID-based DOM/URL matching exact-not-substring **Augment framing**: Round 21 audit-first 是 Round 18 字面量 self-check 的 **upstream pre-write 阶段**, 两者作用阶段不同, 共存比替换稳。 **Meta-anchor "Structural exactness for identity matching"** unifying: - URL layer (#1391 isFacebookAuthRedirectPath: `\.php` + `(/|$)` segment edge) - URL parser layer (#1392 parseGrokSessionId: bare UUID exact / URL host-exact-or-subdomain + path-exact) - DOM layer (#1400 article-scoping: status-id `/\/status\/${id}(?:\/|$)/` regex or pathname segment-array exact compare) Common invariant: boundary-lock structural shape, 不 trust substring 模糊 — fuzzy match 是 silent failure 温床。 ## Validation gates (final head `8809d2c1`) Local: Twitter tests 102/102, `node --check` touched files, `npx tsc --noEmit`, `npm run build`, typed-error-lint 189/189, silent-column-drop 103/103, doc-coverage 140/140, docs:build clean, listing-id advisory unchanged 13, `git diff --check` clean, merge-tree clean. GitHub: build×3 (ubuntu/macos/windows) SUCCESS, unit-test×2 shards SUCCESS, bun-test SUCCESS, adapter-test SUCCESS, audit SUCCESS, doc-coverage SUCCESS, docs-build SUCCESS, smoke-test skipped, PR `CLEAN/MERGEABLE`. ## Strategy/UI boundary (better-solution verdict) UI write path acceptable for P0 symmetry (matches existing Twitter write siblings). GraphQL write migration + structured `idempotent:true` flag are cross-sibling upgrades, P5 candidate, not P0 blockers. Round 17 race-mitigation 第 8 连续 race-free execution (this round absorbed author scope-uncertainty hold-then-retract event without producing actual race). Reviewers: - Lead: @codex-mini0 (4-round iteration, all blockers caught) - Aux: @First-principles-0 (better-solution triangulation, scope-discipline verdict, regression invariants) - Author: @opencli-user |
||
|
|
bf914f20f1 |
fix(grok): replace sentinel rows + silent-clamp with typed errors, deliver image cmd (#1397)
fix(grok): replace sentinel rows and deliver image command |
||
|
|
3b585fb4d1 |
feat(grok): add browser chat baseline commands (read/history/detail/new/send/status) (#1392)
Phase 3 — Grok adapter baseline (LLM browser-chat command family, parallel to ChatGPT/Qwen/Yuanbao). ## Surface 6 commands: `status` / `history` / `read` / `detail` / `new` / `send`. Site-local `clis/grok/utils.js` justified by 6 commands sharing helpers, not over-abstraction. ## 4-head review iteration 1. **`b4e81bad`** — initial baseline (12 Grok/shared files) 2. **`0a8112fc`** — mechanical rebase (CHANGELOG conflict only, all 12 Grok files preserved business-equivalent through rebase) 3. **`481e87e2`** — security fix: `parseGrokSessionId()` SSRF-shape vulnerability close — switched from regex string match to `new URL()` parser with branch separation: - Bare UUID mode: only exact UUID shape (no URL/query suffix accepted) - URL mode: requires `https` scheme + exact `grok.com` or subdomain host + exact `/c/<uuid>` path 4. **`a082023c`** — test-only hardening: 2 additional negative anchors covering existing implementation rejections (bare UUID `?next=abc` query tail / `grok.com.evil.com` host-suffix trick) ## Negative anchor coverage (8 cases) http / off-domain / fakegrok / host-suffix subdomain / embedded URL / path suffix / UUID-tail / bare query tail ## Better-solution evidence form LLM browser-chat family pattern (matching ChatGPT/Qwen/Yuanbao baseline) + 5 live probes — not first-site hostile scrape. TipTap editor API send seam (`editor.commands.focus/clearContent/insertContent`) is correct boundary because Grok ignores DOM input events; isolated in `sendMessage()`. Lack of full TipTap mock = residual risk, not blocker. ## Invariants locked - `parseGrokSessionId()` URL parser branch separation (bare UUID exact / URL exact path) - `history --limit` rejects invalid/out-of-range - `status` uses `null` for unknowns (no fabrication) - Bubble extraction preserves image-only assistant turns (no silent HTML-only drop) - Auth/empty semantics aligned with LLM browser-chat baseline family ## Verification Local: Grok adapter tests `28/28`, typecheck, build/manifest, docs:build, typed-error-lint `189/189`, silent-column-drop `103/103`, doc coverage `140/140`, listing-id advisory `13` unchanged, diff-check clean. GitHub: build ubuntu/macos/windows × unit-test 1/2 + 2/2, bun-test, adapter-test, audit, doc-coverage, docs-build all SUCCESS. PR CLEAN/MERGEABLE. Lead: codex-mini1. Aux: First-principles-1. Coordination: pr-monitor. |
||
|
|
b2ebe211d1 |
feat(yuanbao): add browser-web baseline commands (status/read/detail/history/send) (#1394)
Wire up the standard browser-LLM command surface for Yuanbao, matching the recently shipped chatgpt + claude + qwen baselines: - status — login + current model + (agentId, convId) + URL - read — render the visible conversation as User/Assistant rows - detail — open `<agentId>/<convId>` and read its messages - history — list sidebar conversations with stable IDs - send — fire-and-forget, returns once the send button has been clicked Refactor `ask.js` to share helpers (`sendYuanbaoMessage`, `normalizeBooleanFlag`) with the new commands via `shared.js`, keeping the public ask behavior intact. Notable bits: - `parseYuanbaoSessionId` accepts only full chat URLs or `<agentId>/<convId>` pairs — Yuanbao chat URLs encode both, and silently opening the wrong agent on a bare UUID is a worse failure mode than throwing. URL regex anchored with `(?:[/?#]|$)` so 37+ char tails reject rather than truncate. - `sendYuanbaoMessage` polls the send button (up to 3s) for the React re-render that drops `style__send-btn--disabled___*` after composer input — a fixed wait raced the debounce and produced silent no-op clicks. - `getYuanbaoMessageBubbles` uses `data-conv-id`/`data-conv-idx`/ `data-conv-speaker` attributes for stable per-turn identity (was relying on innerHTML alone). - Status surfaces both human label (`Yuanbao`) and `dt-model-id` (`hunyuan_gpt_175B_0404`) — sentinel strings would silently look like a real model name; null is the typed-unknown signal. Verified: 25 unit tests pass; targeted live smoke for status/read/detail/ history/new/send + ask round-trip on yuanbao.tencent.com. |
||
|
|
b9b87a5c64 |
refactor(facebook/notifications): pipeline→func + typed errors + 7-col contract + runtime upfront limit (Phase 3 P5, #1391)
First Facebook adapter — Pattern C HTML scrape (lead 5 + author 4 = 7 endpoint family probe matrix dual-source negative evidence: graphql×3 / m.facebook redirect / login.php / checkpoint.php / fetch-patch / Messenger relay / ajax legacy 全 unauth 不可达, DOM walk over rendered notification rows + path-anchored auth detection 是当前 reviewable boundary). Caller-visible delta: 3 cols (index/text/time) → 7 cols (+unread/+url/+notif_id/+notif_type). [Bug fix] — 5 silent failures resolved - silent-bad-shape: text.substring(0,150) → full body via per-row 'Mark as read' aria-label - silent-bad-shape: time || '-' sentinel → string|null typed unknown - silent-column-drop: unread badge / anchor href / notif_id / notif_t 暴露 - silent-empty-row: /login(.php)? + /checkpoint(.php)? redirect 返 [] → AuthRequiredError; empty/no-recoverable-text → EmptyResultError - silent-clamp: limit 越界 silent clamp → ArgumentError (1-100), upfront before any navigation (navigateBefore: false) [Structural refactor] - pipeline → cli() func form + Strategy.COOKIE + navigateBefore: false (runtime upfront invariant 与 #1387 standard 拉齐) - module-level pure exports: normalizeNotificationsLimit, stripMarkAsReadPrefix, stripAnchorChrome, parseNotifQuery, extractNotificationRowsFromDoc, isFacebookAuthRedirectPath, buildNotificationsScript - Live IIFE 通过 \${fn.toString()} 嵌入 (dianping #1313 / hupu #1387 / xiaoe #1388 lineage) - Locale 表 6 prefix / 4 badge label 显式列出 - AUTH_REQUIRED: sentinel → Node-side AuthRequiredError mapper [Typed-error hardening] - Path-anchored auth helper: isFacebookAuthRedirectPath(/^\/(?:login|checkpoint)(?:\.php)?(?:\/|\$)/i) — domain-invariant-first encoding (FB top-level auth-only invariant), 排除 /loginhelp /help/login /account/login/identify - Three-layer navigateBefore=false invariant lock: registration assertion + manifest absence + executeCommand runtime page.goto-zero-call (test layer 与 invariant layer 完整对齐) - Row-level silent-empty-row defense: anchor rows with no recoverable body text 直接 skip, 不 emit text:null success row [Doc fix] - docs/adapters/browser/facebook.md notifications enrichment + Output table (列类型 / null vs sentinel 语义) + auth/empty error contract - Boy Scout audit: cross-checked profile / feed / search / marketplace-listings / marketplace-inbox 例 commands 与 args 定义一致 Tests - notifications.test.js 39/39 + src/execution.test.ts 21/21 - Anti-pattern regression guards: not.toMatch(/text\.substring\(0,\s*150\)/) + not.toMatch(/time\s*\|\|/) - JSDOM frozen-fixture (slim 13 lines, 0 blank): header listitem skip / full text / unread badge / query parsing / null time / blank-row skip / relative href absolute / 19-case auth path matrix - typed-error-lint baseline 192 → 191 (silent-sentinel resolved 1) Review iterations (4 head, A 组 codex-mini0 lead + First-principles-0 aux): 1. |
||
|
|
381f095706 |
feat(qwen): add detail command + fix stale message bubble selector (#1390)
* feat(qwen): add detail command + fix stale message bubble selector `getMessageBubbles` was matching `[data-msgid="<id>-question|answer"]` from an older Qianwen frontend. The reshipped DOM no longer carries that attribute on chat turns; `[data-message-id]` now lives on citation cards inside assistant responses, so the old selector silently returned an empty list and `qwen read` had been silently broken. Rewire to walk `[data-chat-question-wrap]` and `[data-chat-answers-wrap]` in DOM order (correct Q/A interleaving) and synthesize stable IDs from the nearest sibling `data-req-id` so `waitForAnswer.seenAssistantId` and read/ask/detail dedupe paths keep working. Verified live against an existing conversation: 3 user turns + 3 assistant turns extracted; old selector returned 0. `qwen detail <id|url>`: open a specific conversation by ID or full chat URL, poll up to 20s for the transcript to render, return Role/Text rows. Adds `parseQianwenSessionId` (5 unit tests covering ID/URL parsing + ArgumentError on malformed input). Reuses the same site-level browser session as `read`/ `ask` so consecutive calls continue in the same Qwen tab. - clis/qwen/detail.js (new) - clis/qwen/utils.js (parseQianwenSessionId + getMessageBubbles rewire) - clis/qwen/utils.test.js (new) - docs/adapters/browser/qwen.md (detail entry + options/columns) - cli-manifest.json (regenerated) * fix(qwen): anchor URL regex to reject 33+ hex tail truncation codex-coder review on PR #1390 caught that `https://www.qianwen.com/chat/<33+ hex>` would silently truncate to the first 32 chars and open the wrong conversation. Adds end-of-input / slash / query / fragment boundary to the URL match group and two new unit-test cases (digit tail + letters tail) covering the truncation gap. |
||
|
|
99986c3101 |
feat(chatgpt): add browser chat baseline commands
Add ChatGPT web ask/send/read/history/detail/new/status alongside existing image support. Tighten ChatGPT web helper selectors and typed error contracts, update docs/changelog, regenerate manifest, and seed local ChatGPT verify fixtures for ask/read. |
||
|
|
6f7eb6a76a |
refactor(xiaoe x3): pipeline→func + typed errors + content silent-drop fix (Phase 3 P1)
Phase 3 P1 (xiaoe catalog/courses/content) — pipeline→func refactor + typed-error hardening + content silent-drop bug fix + URL upfront validation + inherited legacy doc fix。
## Tags (PR body honesty 演进 dual-nature framing 试用)
- **[Bug fix]** `xiaoe/content` silent-column-drop (caller-visible delta)
- **[Structural refactor]** `xiaoe/catalog` + `xiaoe/courses` pipeline→func 包壳 (parity by construction, IIFE 字节级保留)
- **[Typed-error hardening]** 三 func `page.goto` + `page.evaluate` failure 包成 `CommandExecutionError`; `content/catalog` URL upfront `ArgumentError` (missing/malformed/non-https/off-domain) before navigation
- **[Doc fix]** `docs/adapters/browser/xiaoe.md` `courses --limit 10` (legacy doc 错误 inherit) + `--url` wording → 实际 positional `url` (manifest aligned)
## Per-tag detail
### [Bug fix] content silent-column-drop (real caller-visible bug)
adapter 名"提取小鹅通图文页面内容为文本", IIFE 返 `{title, content, content_length, image_count, images}`, 但 columns 只声明 `[title, content_length, image_count]` → `content` (那段文本本身) 被 silent drop。**用户拿到 "1234 chars" 但拿不到那 1234 chars** — adapter 名字撒谎了。
- Fix: 公开列 `[title, content, content_length, image_count]`, `content` 真 caller-visible delta
- Choice A (vs B reshape): legacy `images` 是 `JSON.stringify(slice(0, 20))` 截断/stringified 坏合同, **不暴露成新列** (避免把 silent-bad-shape 升级成公开坏合同), 留 follow-up 另开 explicit media/images contract
- `image_count` 用 `countXiaoeImages(doc)` 全页计数, 不 slice (既有 metadata 质量修正)
### [Structural refactor] catalog + courses pipeline→func wrapper (parity by construction)
- `pipeline:[]` form → `func` form
- IIFE body 字节级保留 (Xiaoe 没 public REST, Vue 私有 runtime 是唯一稳定 hook, JSDOM 复刻不了 Vue tree)
- Pure helpers extracted: `pickContentText`, `countXiaoeImages` (content) / `typeLabel`, `buildItemUrl`, `chapterUrlPath` (catalog) / `buildCourseUrl` (courses)
- IIFE 通过 `\${fn.toString()}` 嵌同一份代码 (dianping #1313 / hupu #1387 同模式)
- No live verify acceptable: IIFE 字节级保留 + helper 全 unit-test + manifest column shape 不变 = 行为 parity by construction
- `buildScript` 反向断言 `images.slice(0, 20)` legacy anti-pattern 不出现 (anti-pattern regression guard, 同 #1387 `documentElement.outerHTML` 反向 guard)
### [Typed-error hardening] 三 func navigation + evaluate boundary
- `requireXiaoePageUrl()` for `content/catalog`: missing/malformed/non-https/off-domain URL → upfront `ArgumentError` before `page.goto` (test asserts `expect(page.goto).not.toHaveBeenCalled()`)
- `content/catalog/courses`: `page.goto` moved inside try, navigation/evaluate failures both wrap as `CommandExecutionError`, no raw CDP/browser error path leaks
- Empty shell stays `EmptyResultError` (no reliable login-wall signal to justify `AuthRequiredError`, 避免 false positive — 应用 #1384 secUid 教训)
### [Doc fix] inherited legacy doc errors
- `xiaoe courses --limit 10` example removed (no `--limit` arg in manifest, legacy doc 错误 inherit)
- positional `url` wording aligned with manifest (was incorrectly `--url`)
- 同 #1386 positional docs 教训, 但延伸到 "继承 legacy doc 错误也是新 PR 责任" (Boy Scout typed-error hardening 在 doc 层延伸)
## Tests: 46/46 green
- 3 cmd registration contract
- pure helper unit tests (selector chain / image filter / URL priority / type label fallback / no synthetic URL)
- `buildScript` invariants (`images.slice(0, 20)` 反向断言)
- wire tests: ArgumentError upfront (BEFORE page.goto), EmptyResultError empty rows + empty content, CommandExecutionError navigation/evaluate failure, rows verbatim happy path
## Lint gates
- typed-error-lint 190/190 (no new) ✓
- silent-column-drop 103/103 (no new) ✓ (注: `pipeline:[]` IIFE string template AST walker 看不进, lint follow-up scope)
- doc-coverage 140/140 ✓
- listing-id-pairing advisory unchanged 13 ✓
## GitHub checks (head
|
||
|
|
e610260705 |
refactor(hupu/hot): pipeline→func + querySelectorAll + 4 enrichment columns (Phase 3 P3)
Phase 3 P3 (hupu/hot) — pipeline→func refactor + 2 真 bug 修 + 4 列 enrichment + JSDOM-frozen-fixture test pattern (#1313 复用) + anti-pattern regression guard。
## Summary
- Pipeline form (`pipeline:[]` + `documentElement.outerHTML` regex) → `func` form (`querySelectorAll('.t-info')` DOM walk)
- **Bug 1 修**: outerHTML regex 静默漏行 (markup 抖动就漏, mocked test 抓不到)
- **Bug 2 修**: regex 抓所有 9-digit 锚点 → ~70 个 anchor 但页面只 render 60 个 `.t-info` row → legacy adapter 每次返 ~10 个 phantom 行 (导航链接 conflated 成 thread 行)
- **4 enrichment columns** (4→8): `lights` (亮 count int|null, 万 expanded `1.2万→12000`) / `replies` (回复 count int|null) / `forum` (per-row sub-section) / `is_hot` (bool 暴露 hupu \" hot\" marker, 不 filter 行序保持页面顺序)
- columns/manifest/docs sync: `[rank, tid, title, lights, replies, forum, is_hot, url]`,`null` vs `0` 语义清楚
## Typed errors
- `--limit` 上游 `ArgumentError` for 0/-1/>100/1.5/non-numeric (BEFORE `page.goto`,**不 silent clamp**)
- 空页 `EmptyResultError`
- `page.evaluate` failure 包成 `CommandExecutionError` (test regression locked)
## JSDOM frozen-fixture test pattern (#1313 复用)
- 抽 `extractHupuHotRowsFromDoc(doc, limit, parseCount)` 为 module-level pure export
- in-page IIFE 通过 `\${fn.toString()}` 嵌同一份代码
- JSDOM test 直接调 export against `__fixtures__/hot-home.html` (slim 6-row hand-crafted fixture)
- 17/17 tests green (contract / normalize / parseCount / extract / buildHotScript invariants / wiring / phantom-anchor exclusion / evaluate-error envelope)
## Anti-pattern regression guard (#1313 fixture pattern 延伸)
- `buildHotScript` 反向断言 `not.toContain('documentElement.outerHTML')` 锁不回退到旧 broad regex
- `buildHotScript` 反向断言 `not.toContain('regex.exec')` 同向锁
- fixture 顶部 `.t-info` 外的 9-digit phantom anchor `639999999` 反向锁: 旧 broad regex 会抓到, 新 `.t-info` extractor 不抓 — 把 fixture 反向验证从断言层升到证据层
## Better-solution check (live probe evidence-based)
DOM `.t-info` = 60 visible rows, `window.\$\$data.pageData.threads` = 70 (10 hidden/non-rendered)。对"首页可见 hot rows" 任务, DOM walk 比 bootstrap JSON 更贴 source of truth (后者会引入 hidden/不渲染条目)。这条 60 vs 70 数字是设计决策的硬 justify, 不是设计意见。
## Lint gates
- typed-error-lint 190/190 (no new) ✓
- silent-column-drop 103/103 (no new) ✓
- doc-coverage 140/140 ✓
- listing-id-pairing advisory unchanged 13 ✓
## GitHub checks (head
|
||
|
|
464de7059e |
refactor(tiktok): write commands -> button-walker Route 1 with typed errors (Phase 3 P0.5)
Phase 3 P0.5: refactor 3 TikTok write commands (comment, follow, unfollow) from time-window-wait UI flow to a button-walker + state-verification path with a typed-error boundary, sharing a parallel helper structure to the #1384 read PR. Two-layer helper boundary (clis/tiktok/utils.js extension): - BUTTON_WALKER_HELPERS (browser side): button-walker (locate / pre-click state read / click / state-verify post-click) + cleanText reuse + cookie/auth-secUid plumbing for write-auth + plain Error throws on contract violations - throwButtonWalkerError() (Node side): map browser-thrown errors -> typed CommandExecutionError (button missing / state-verify fail / captcha / rate-limit / navigation/eval/empty-row defensive failures) / AuthRequiredError (cookie + viewer secUid) / ArgumentError (upfront input validation). Explicitly NO EmptyResultError mapping (button contract violation is not an empty result, per #1384 R4 lesson on auth-vs-empty classification). Per command: - comment <video-url> <text>: button-walker click + state-verify by checking comment-list state (not wait-2s) - follow <username>: pre-click state read distinguishes idempotent fast path (`already-following` / `already-friends`) from post-click success (`followed`). Post-click result causality preserved (post-click never returns `already-*`). - unfollow <username>: pre-click `already-not-following` fast path; post-click `unfollowed`. result enums (per row): - comment: `posted` (no idempotent path - comments cannot dedupe) - follow: `followed` | `already-following` | `already-friends` (last two pre-click only) - unfollow: `unfollowed` | `already-not-following` (last one pre-click only) retryable contract (in hint string `retryable=<bool> reason=<...>`): - comment failures: retryable=false reason=server-fan-out - follow/unfollow failures: retryable=true reason=idempotent (server-side dedupe is safe) Lead push iterations during review (codex-mini1 maintainer-fixes-directly): - |
||
|
|
9a7dd44b3e |
refactor(tiktok): 6 read commands -> page-context API (Phase 3 P0, absorbs #1382)
Phase 3 P0: refactor 6 TikTok read commands (explore, following, friends, live, notifications, user) from DOM/network-intercept to TikTok web's own page-context API endpoints, sharing one helper boundary. Helper boundary (clis/tiktok/utils.js): - BROWSER_HELPERS: in-browser fetchJson + cleanText + asNumber (null/'' -> null preserve missing-vs-zero distinction) + cookie/msToken plumbing - VIDEO_ITEM_NORMALIZER: normalize page-context item -> row shape - assertTikTokApiSuccess(data, label): unify TikTok in-band envelope (status_code/statusCode != 0; code 8 or auth-looking message -> AUTH_REQUIRED; other -> upstream label API failed) - throwTikTokPageContextError() (Node side): map browser-thrown errors -> AuthRequiredError / EmptyResultError / CommandExecutionError Per command: - explore: /api/recommend/item_list/ pagination, --limit upfront ArgumentError - following: /api/user/list/ relationships - friends: /api/user/list/ + cross-filter - live: /api/live/discover/ feed - notifications: /api/notice/multi/ (status 8 -> AUTH_REQUIRED) - user (absorbed from #1382): secUid resolve via __UNIVERSAL_DATA_FOR_REHYDRATION__ -> /api/user/detail/, /api/post/item_list/ pagination, /api/search/general/full/ exact-author fallback. !secUid -> EmptyResultError (NOT AuthRequiredError; auth still covered by HTTP 401/403 + envelope status_code 8/auth-looking msg). source field = bootstrap | profile-api | search-fallback in row/columns/manifest/docs/tests. Closes #1382 (absorbed; #1382 closed without separate merge per WAWQAQ direction). Validation: - clis/tiktok/ tests: 38/38 - typed-error-lint: 190/190 - silent-column-drop: 103/103 - doc-coverage: 140/140 - docs:build pass, manifest no drift - GitHub gates: build x3 / unit x2 / bun / adapter-test / audit / doc-coverage / docs-build all SUCCESS, smoke skipped, MERGEABLE Reviewers: codex-mini0 (lead, push 4 boundary fixes |
||
|
|
b327da5b3c | feat(llm): reuse browser sessions by site (#1385) | ||
|
|
fa7851bb9a | feat(browser): add adapter session reuse (#1383) | ||
|
|
c6d5da54ee | feat(web): add exhaustive same-origin frame mode (#1373) | ||
|
|
67cde0e263 |
enrich(coupang): product detail cmd + replace silent clamp/sentinel/Error with typed errors (#1370)
* enrich(coupang): add product detail cmd + replace silent clamp/sentinel/Error with typed errors Two enrichment changes plus three silent-failure fixes on top of existing search / add-to-cart. New cmd: coupang product ───────────────────────── Pairs with search as the listing↔detail round-trip target. Reads a logged-in product page and extracts a single canonical row with price, original_price, discount_rate, rating, review_count, seller, brand, rocket, delivery_promise, image_url, url. Three-source extractor (JSON-LD Product schema → bootstrap globals → DOM) merged in priority order, mirroring the search.js pattern. The columns use string|null typing — null means "upstream did not provide this field on this product" (e.g. some items have no original_price). Failures (login wall / page mismatch / page failed to render) raise typed errors instead of silently returning empty rows, so callers can treat any returned row as real data. Search column shape: added product_id ───────────────────────────────────── Listing must pair with detail by id. The data was already extracted by normalizeSearchItem; only the columns array needed updating so the field projects through to the rendered row. Per the listing-id-pairing convention (PR #1297) the new column lets agents round-trip rows directly into `coupang product` without re-scraping URLs. Silent-failure fixes ──────────────────── 1. search --limit silent clamp. Old: `Math.min(Math.max(Number(kwargs.limit||20),1),50)` silently rewrote `--limit 999` to 50 and `--limit 0` to 1. New: `parseLimitArg(raw, 20, 50)` throws ArgumentError on out-of-range / non-integer / negative input. Same convention as the typed-fail-fast memory & PR #1289. 2. search --page silent clamp. Old: `Math.max(Number(kwargs.page||1),1)` silently lifted negative pages. New: parsePageArg throws ArgumentError on non-positive input. 3. Generic `throw new Error(...)` → typed errors. - Empty query, unsupported --filter, missing --product-id/--url → ArgumentError - Login wall detection → AuthRequiredError('coupang.com', ...) - Empty result / filter-not-rendered → EmptyResultError - PRODUCT_MISMATCH / OPTION_REQUIRED / button-not-found / unknown ack failure (add-to-cart) → CommandExecutionError - The PRODUCT_MISMATCH and `actualProductId || 'unknown'` sentinel were also fixed (silent-sentinel was the audit hit there). Coverage ──────── - 21 contract assertions in clis/coupang/coupang.test.js covering parseLimitArg / parsePageArg (no silent clamp), registry shape (search has product_id, product is read-class with expected columns, add-to-cart is write-class), and typed-error pre-flight rejections (empty query / bad filter / out-of-range limit & page / missing detail args). - Manifest 763 → 764 (+1 entry: coupang/product). - Audits: typed-error-lint 196 → 194 (resolved 2 silent-clamp/sentinel baseline entries; baseline updated). silent-column-drop 103/103 unchanged. * fix(coupang): tighten product id and browser errors * fix(coupang): require real product urls |
||
|
|
a5a3248a77 |
refactor(linux-do): remove deprecated hot/category/latest compat shims (#1368)
* refactor(linux-do): remove deprecated hot/category/latest compat shims
The three shims have been pure backward-compat wrappers since linux-do/feed
became the unified entrypoint. With no stable release commitment to preserve,
they are pure surface cost: 3 manifest entries, 3 deprecated branches in help
output, and a `buildLinuxDoCompatFooter` helper that exists only to feed them.
- delete clis/linux-do/{hot,category,latest}.js
- drop now-orphaned `buildLinuxDoCompatFooter` from feed.js and unexport
`executeLinuxDoFeed` (no external consumers remain)
- remove the Compatibility section in docs/adapters/browser/linux-do.md
- regenerate cli-manifest.json (-125 lines)
BREAKING CHANGE: `opencli linux-do hot|category|latest` are removed. Use
`opencli linux-do feed --view top --period <period>`,
`opencli linux-do feed --category <id-or-name>`, and
`opencli linux-do feed --view latest` instead.
* fix(linux-do): finish compat shim removal
|
||
|
|
dcaae37068 |
refactor(registry): remove dead adapter metadata (#1369)
* refactor(registry): remove dead adapter metadata * docs(changelog): note header strategy removal |
||
|
|
4ef2cb8b1c |
enrich(toutiao): hot board (public) + bug fixes (silent column drop, partial render) (#1366)
* enrich(toutiao): hot board + bug fixes (silent column drop, partial render) Per WAWQAQ "丰富现有 adapter" pivot — Phase 2 site #3. ## New command - `toutiao hot` (Strategy.PUBLIC, browser:false) — public homepage hot board via the toutiao.com hot-event/hot-board endpoint. No login required. Returns 8 stable columns (rank/id/title/query/hot_value/label/url/image). ## Bug fixes for `toutiao articles` - **Silent column drop fixed**: `parseToutiaoArticlesText` previously did `if (title && stats) push(...)`, silently dropping any row where the stats span hadn't finished rendering by the time page.innerText was read. Slow-render bugs were invisible — adapter looked "complete" while writers saw extra rows in the dashboard. Partial rows now surface with `null` stat columns. - **Silent clamp on `--page` removed**: out-of-range / non-integer values raise `ArgumentError` with explicit bounds [1, 4]. Same validation reused by both `articles` and `hot` via `parseArticlesPage` / `parseHotLimit` in `utils.js`. - **Empty result typed**: zero-row scrape now raises `EmptyResultError` instead of returning `[]` silently (would otherwise look like a legitimate "no articles" response). ## Refactor - Parser logic extracted to `clis/toutiao/utils.js` (alongside hot-row mapping, validators, and the hot-board URL constant). - `articles.js` switches from declarative `pipeline:` to imperative `func` form so `parseArticlesPage` validation can run before the navigation step (declarative pipeline can't pre-validate args). - Strategy is now explicit: `Strategy.COOKIE, browser: true` for articles (creator dashboard is logged-in only). ## hot field map `ClusterIdStr` (or numeric `ClusterId`) → id; `Title` → title; `QueryWord` → query (falls back to title); `HotValue` → hot_value (non-negative numeric, else null); `Label`, `Url`, `Image` → respective columns. `pickImage` walks `Image.url` → first truthy `Image.url_list[]`. Empty-title rows are dropped (returns null) before ranks are densely re-assigned 1..N. ## Tests 29 contract assertions across `parseArticlesPage` / `parseHotLimit` / `parseToutiaoArticlesText` / `mapHotRow` + registry-level shape checks + `hot` adapter func behaviour (typed errors / no silent clamp / fetch failure paths / dense-rank). ## Audits - typed-error-lint: 196 = 196 (unchanged baseline) - silent-column-drop: 103 = 103 (unchanged baseline) - listing-id-pairing: hot has `id` column (round-trippable when a detail command lands later); advisory list unchanged. ## Manifest 757 → 758 entries (+1 for `hot`). ## Doc - index.md: toutiao mode 🔐 → 🌐/🔐 (hot is public, articles is logged-in) - toutiao.md: per-command mode/domain table + column docs + prerequisites * fix(toutiao): tighten hot and articles contracts |
||
|
|
69ee36f997 |
fix(linkedin): surface detail_error on --details (no silent catch / no silent empty) (#1363)
* fix(linkedin): surface detail_error on --details (no silent catch / no silent empty)
The previous --details enrichment path had two indistinguishable failure modes
that both produced `description: '', apply_url: ''`:
1. `if (!job.url)` early return — row had no jobId, so we couldn't navigate.
2. `} catch {}` — page.goto / page.evaluate threw (network, timeout, parse error).
Callers couldn't tell "upstream had no description" from "we failed to fetch",
and the catch swallowed every error without logging. For an enrichment that
costs one page navigation per row, silent failure is especially harmful — users
just see an empty cell with no way to debug.
Fix: replace empty strings with `null` for missing/failed rows, add a new
`detail_error` column (string|null) carrying a short typed reason:
- 'no url' — row had no jobId
- 'fetch failed: <msg>' — page.goto / page.evaluate threw
- 'missing description' — page loaded but body was empty
- null — success
Every failure is also logged to stderr with the offending URL so debugging is
possible. Per-row failures still don't abort the batch (the original intent),
but they're now visible.
Tests: 13 new contract assertions in clis/linkedin/search.test.js covering
parseCsvArg, mapFilterValues (ArgumentError on unknown values), decodeLinkedinRedirect,
and 5 enrichJobDetails paths (no-url / goto-throw / empty-description / success /
multi-row-mixed). Added `export const __test__` for testability.
Audits clean: typed-error-lint 196/196, silent-column-drop 103/103.
* fix(linkedin): fail fast on auth walls
|
||
|
|
da2453cfbd |
enrich(reuters): article-detail + bug fixes (silent clamp, silent error envelope) (#1362)
* enrich(reuters): article-detail + bug fixes (silent clamp, silent error envelope) Per WAWQAQ "丰富现有 adapter" pivot — Phase 2 site #2. - `reuters article-detail` — full article body + canonical metadata for a Reuters URL. Pairs with `reuters search` (use the `url` column to round-trip). - **Silent clamp on `--limit` removed**: out-of-range values now raise `ArgumentError`. Validation happens before browser navigation. - **Silent error envelope removed**: the in-page IIFE used to swallow `fetch` errors with `catch(e) {}` and return `{error: ...}`, then the node side did `if (!Array.isArray(data)) return [];`. Now: - in-page IIFE returns `{ ok, status, body, error? }` raw envelope - node side throws typed errors: - `CommandExecutionError` on in-page exception - `CliError(FETCH_ERROR)` on non-2xx upstream - `CommandExecutionError` on captcha HTML (200 + non-JSON body) - `EmptyResultError` on empty articles array - **Empty query**: now `ARGUMENT_INVALID` instead of triggering an empty upstream call. - **Column shape enriched**: previously dropped `section_path` and `authors` are now stable columns. - `docs/adapters/browser/reuters.md`: full Commands / Columns / Error Behaviour section (was a 3-line stub). - `docs/adapters/index.md`: add `article-detail` to the commands cell. 27 contract assertions across `parseLimit` / `mapSearchArticles` / `mapArticleDetail` / `buildSearchScript` / `buildArticleDetailScript` + registry-level checks for both commands (Strategy, ARG validation before nav, every typed-error path, success path). - typed-error-lint: 196 → 195 (silent-clamp resolved on `clis/reuters/search.js:18`); baseline updated. - silent-column-drop: 103 = 103 (unchanged). - listing-id-pairing: advisory only (article-detail keys off `url`). 757 → 758 entries (+1 for `article-detail`). * fix(reuters): type auth and fetch failures * fix(reuters): preserve search detail round trip |
||
|
|
61c4637b4c |
enrich(ctrip): hotel-suggest + bug fixes (silent clamp, dropped columns, fake URL) (#1361)
* enrich(ctrip): hotel-suggest + bug fixes (silent clamp, dropped columns, fake URL) Per WAWQAQ "丰富现有 adapter" pivot — Phase 2 site #1. ## New command - `ctrip hotel-suggest` — surfaces hotel-context suggestions (cities, business areas, individual hotels) via the same backing endpoint with searchType=H. Distinct from `ctrip search` (searchType=D) which returns destinations / scenic spots / railway stations. ## Bug fixes for `ctrip search` - **Silent clamp on `--limit` removed**: out-of-range values (≤0, ≥51, non-integer) now raise `ArgumentError` with explicit bounds rather than silently snapping to [1, 50]. - **Silent column drop fixed**: previously the adapter discarded `id`, `cityId`, `cityName`, `provinceName`, `countryName`, `lat`, `lon`, `eName` and `displayType` from upstream rows. Now all are surfaced as stable columns. - **Fake URL fixed**: previously `url` was always `''`. Now constructs canonical Ctrip URLs by `type` (City / Markland / Hotel / Zone / RailwayStation) and returns `null` (no silent fabrication) for unknown types. - **In-band error envelope typed**: `Result: false` payloads now surface as `COMMAND_EXEC` (was previously not handled — adapter returned empty rows). ## Doc fix - `Mode: 🔐 Browser` → `🌐 Public` (search uses public API, no login) - Add `hotel-suggest` to commands table in both `docs/adapters/index.md` and `docs/adapters/browser/ctrip.md`. ## Coords picker Mainland China rows ship `gdLat`/`gdLon` (gaode); international rows ship `gLat`/`gLon` (wgs84). Adapter picks the first non-zero pair (zero is the upstream sentinel for "missing"); returns `null` if all variants are zero. ## Tests 25 contract assertions across `parseLimit` / `pickCoords` / `buildUrl` / `mapSuggestRow` + registry-level checks for both commands (Strategy / shape parity / typed errors / no silent clamp). ## Audits - typed-error-lint: 196 = 196 (unchanged baseline) - silent-column-drop: 103 = 103 (unchanged baseline) - listing-id-pairing: advisory only (search has `id` round-trip column) ## Manifest 757 → 758 entries (+1 for `hotel-suggest`). * fix(ctrip): wrap suggest fetch and json failures |
||
|
|
6469a02ea6 |
feat(deepseek): add detail and send commands for explicit conversation control (#1344)
* feat(deepseek): add detail and send commands for explicit conversation control
doubao already ships `detail <id>` and `send` for ID-explicit conversation
read/write; deepseek had only `read` (current page only) plus the
implicit-resume `ask`. Adding both gives users a stable handle when they
know the conversation ID, without going through `ask`'s resume detection
or its full prompt-then-wait pipeline.
`deepseek detail <id>`:
- parses a bare UUID or any URL containing `/a/chat/s/<id>`,
- rejects malformed input via `ArgumentError` before any browser
navigation,
- navigates to `https://chat.deepseek.com/a/chat/s/<id>` and returns
the visible message list,
- throws `EmptyResultError` when the conversation has no rendered
messages.
`deepseek send <id> <prompt>`:
- takes the conversation id as a required positional, because the
framework runs each browser command in an ephemeral per-command
workspace (a fresh tab) and there is no shared "current conversation"
across commands; the navigation must be explicit,
- drives input through CDP `Input.insertText` via `page.nativeType`,
mirroring the doubao adapter (#1278); `execCommand('insertText')` plus
a synthesised input event leaves the React-controlled state desynced
on a freshly-opened tab and the resulting click silently no-ops,
- keeps the verification loop inside the same `page.evaluate` so the
framework cannot close the tab mid-flight; counts user-class bubbles
by text-match (DeepSeek virtualises the message list, so a numeric
bubble-count check is unreliable),
- throws `CommandExecutionError` with a specific reason when the
textarea did not populate, the send button stayed disabled, the
bubble never settled, or the optimistic render rolled back during
a 3s settle window,
- treats "Promise was collected" from the post-click eval as success,
matching the existing pattern in `ask --file`.
Helper `parseDeepSeekConversationId` is exported from utils.js so the
same parser feeds both commands and round-trips the canonical lower-case
ID.
Tests:
- utils.test.js: 5 cases covering bare UUID, upper-case
normalisation, URL extraction with and without query string, empty /
null / whitespace input, and non-UUID rejection.
- detail.test.js: 5 cases covering registration, navigation +
message return, URL normalisation, ArgumentError before browser
navigation, and EmptyResultError on no-messages.
- send.test.js: 7 cases covering registration, ArgumentError on bad
id, full happy-path through nativeType + IIFE verification, the
textarea-mount timeout, missing nativeType helper, focus failure,
IIFE-reason translation to CommandExecutionError, and the
"Promise was collected" success path.
Manifest auto-regenerated to register both commands.
Live-verified end-to-end against my own DeepSeek session:
- `detail` returns the canonical message list for a bare UUID, parses
a full chat URL, and rejects malformed IDs before any browser
navigation,
- `send` lands the prompt as the latest user message in the target
conversation and gets an AI response back; reload of the
conversation page in a separate tab confirms the message persisted
server-side.
* docs(deepseek): document detail and send commands
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
|
||
|
|
f033481e67 |
feat: 2 read adapters (wttr, openfda) + contract tests (#1355)
* feat: 2 read adapters across 2 new sites + contract tests (wttr, openfda)
Trimmed from original Round 11 per WAWQAQ feedback (msg=3899a382): drop
novelty/niche sites (timeapi / zippopotam / spacedevs / citybik) — keep
only sites with clear real-world utility:
- wttr (current, forecast) — wttr.in weather, no auth, simple text/json toggle
- openfda (drug-label, food-recall) — FDA drug labels + food recall enforcement
13 contract tests across 2 sites cover Lucene operator query construction
(openfda +AND+ literal handling), [string] 1-elem array unwrap, brand-OR-
generic match, wttr [{value:"..."}] array-of-objects 1-elem unwrap.
Manifest 757→759 (+2). Audits clean: typed-error-lint=196 baseline.
* fix(openfda): use brand or generic label search
|
||
|
|
39943c05e9 |
feat: 12 read adapters across 6 new sites + contract tests (round 7) (#1350)
* feat: 12 read adapters across 6 new sites + contract tests (round 7) New sites: wikidata, lichess, rest-countries, nuget, flathub, oeis - wikidata: search (wbsearchentities) + entity (Special:EntityData) — Q/P/L ids, localised label/description with English fallback - lichess: user + top (perf rankings) — closed accounts → EmptyResultError, no silent disabled rows; 13 perf types validated - rest-countries: country (substring) + region — population-sorted by default, flattened languages/currencies/capitals - nuget: search + package (full version history) — registration page-walking for 100+ version histories; case-insensitive id with strict shape gate - flathub: search + app — appId reverse-DNS, dual-shape timestamp coercion (search is unix-seconds int, /appstream is ISO string) - oeis: search (paginated) + sequence — A-id zero-padded, 12-term preview with (+N) suffix, surfaces commentCount/formulaCount/etc instead of full graphs Contract tests: 6 files × 7 assertions = 42 contract assertions, all green. Live verified all 12 commands against real APIs. Audits clean: typed-error 196=baseline, silent-column 103=baseline, 0 Round-7 listing-id-pairing violations (all 6 listings carry round-trip ids). * fix(nuget): fail fast on malformed registration pages |
||
|
|
9ae44c228a | fix(round5): strengthen contract tests (#1348) | ||
|
|
498ad3930c |
feat: 13 read adapters across 6 new sites (round 4) (#1347)
Six new public-API sites — package registries + Docker images + OpenAlex
scholarly works — all unauthenticated, no browser required.
dockerhub search image
rubygems search gem
homebrew formula cask popular
packagist search package
maven search artifact
openalex search work
Conventions held:
- access: 'read' on every command
- typed errors (ArgumentError / EmptyResultError / CommandExecutionError)
instead of generic CliError or silent fallback
- input validators per site (image slugs, gem names, Composer names,
Maven coordinates, OpenAlex work-id / DOI normalization)
- listing rows carry an id-shaped column (image / gem / token / package /
coordinate / id) that round-trips into the corresponding detail command
- HTTP 429 surfaces with retry hint, 404 → EmptyResultError
Audits:
- check:typed-error-lint → no new violations (baseline 196)
- check:silent-column-drop → no new violations (baseline 103)
- advise:listing-id-pairing → unchanged at 13
|
||
|
|
55088bbb28 |
feat: 13 read adapters across 5 new sites + 4 extensions (round 3) (#1346)
New sites (8 commands):
- npm : search / package / downloads (registry.npmjs.org + api.npmjs.org)
- pypi : package / downloads (pypi.org + pypistats.org)
- crates : search / crate (crates.io)
- mdn : search (developer.mozilla.org)
- nvd : cve (services.nvd.nist.gov)
Extensions (5 commands; +1 dblp/author surfaced in index):
- hf : spaces (Hugging Face Spaces by likes / created_at / last_modified)
- dblp : venue (search dblp's venue registry by acronym/topic)
- coingecko : derivatives (perpetual / futures markets, 24h volume)
- stackoverflow : related (related questions for a given question id)
All commands hit public unauthenticated endpoints (Strategy.PUBLIC, browser:false),
typed-fail-fast on bad inputs (no silent fallback / clamp), and round-trip listing
ids into their detail commands where applicable.
Audits (all green vs baseline):
- typed-error-lint : 196 = 196 baseline, no new
- silent-column-drop : 103 = 103 baseline, no new
- listing-id-pairing : 13 advisory (was 12; +1 = dblp/venue with no
corresponding venue-detail command)
Doc coverage : 120/120 adapter dirs documented (+5 new doc pages, +4 updated)
Manifest : 722 entries (was 709; +13 commands)
Live verified:
- npm search react / npm package react / npm downloads react --period last-week
- npm downloads react --period 2025-01-01:2025-01-05
- pypi package requests / pypi downloads requests --period recent / overall
- crates search tokio / crates crate serde
- mdn search fetch
- nvd cve CVE-2021-44228
- hf spaces --limit 3
- dblp venue ICLR
- coingecko derivatives --limit 3
- stackoverflow related 79935770 --limit 3
- typed-error sanity: invalid CVE id, bad npm name, bad --period
|