Commit Graph

1149 Commits

Author SHA1 Message Date
jakevin b58e43da7f feat(extension): mark automation tabs with group 2026-05-06 17:57:13 +08:00
Benjamin Liu 5137aac036 fix(deepseek): skip pinned conversations and fail fast when resume target unavailable (#1343)
Closes #1342.

`opencli deepseek ask` (without --new) used to resume the most recent
conversation by clicking the first `a[href*="/a/chat/s/"]` in DOM order
after a fixed 2-second wait.

Two bugs:

  1. Pinned conversations sit in their own DOM section ("置顶") that
     renders above "30 天内" and friends. Click-first-anchor lands on the
     pinned thread, not the user's most recent. Reproduced live by
     pinning a conversation through the sidebar context menu and
     observing that the existing logic targets it instead of the most
     recent non-pinned thread.
  2. The 2s wait is fixed. On a slow network the sidebar has not
     populated yet, the click is a no-op, and `ask` silently falls
     through to the new-chat path. The user typed "follow up" and
     a brand-new conversation gets created.

Replace the click-first-anchor + fixed wait with a new helper
`pickResumeUrl(page)` in utils.js that:

  - polls the sidebar for up to 10s (5 attempts × 2s),
  - identifies pinned anchors by a text-based check on the section
    header (`/^\s*(置\s*顶|Pinned)\s*$/i`); DeepSeek's CSS-module
    class names are randomized per build, so the text is the only
    stable signal,
  - returns the URL of the first non-pinned anchor (or falls back to
    the first overall if every visible anchor is pinned),
  - returns null if no anchor surfaces in time.

`ask.js` calls the helper and `page.goto`s the returned URL. When the
helper returns null, `ask` now throws a `CommandExecutionError`
instead of silently navigating to a fresh chat. The user gets a clear
"pass --new" hint and their prompt is never sent to a wrong target.

Tests:
  - utils.test.js: 4 cases covering happy path, polling-then-success,
    timeout returns null, and a structural assertion that the embedded
    DOM walker uses text-based pinned detection.
  - ask.test.js: replaced the prior "still selects model when no
    conversation to resume" test (which exercised the silent
    fall-through) with a fail-fast assertion. Updated the resume-success
    test to mock the new helper.
2026-05-06 17:54:33 +08:00
jakevin 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
2026-05-06 17:47:05 +08:00
jakevin 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
2026-05-06 16:54:21 +08:00
jakevin 9ae44c228a fix(round5): strengthen contract tests (#1348) 2026-05-06 14:19:55 +08:00
jakevin 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
2026-05-06 13:38:43 +08:00
jakevin 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
2026-05-06 13:14:41 +08:00
jakevin a78ceb1602 feat: 11 read adapters across 8 sites (round 2) (#1345)
* feat: 11 read adapters across 8 sites (dblp / steam / bbc / devto / lobsters / medium / coingecko / hf)

Round 2 of the adapter expansion sweep. All 11 commands hit public APIs (no
browser, no auth), follow the post-#1332 typed-error / no-silent-failure
discipline, and were live-verified against real endpoints.

New adapters:
- dblp/author      : recent publications for one author (resolve PID by name, or pass --pid)
- steam/search     : storefront name search (storesearch API)
- steam/app        : single app detail (appdetails API; HTML entities decoded)
- bbc/topic        : per-topic RSS (8 canonical BBC News feeds)
- devto/latest     : /api/articles/latest with --page pagination
- lobsters/domain  : stories from a specific source domain (/domains/<d>.json)
- medium/tag       : tag RSS (description full-length, no silent truncation)
- coingecko/exchanges  : trust score + 24h BTC volume leaderboard
- coingecko/categories : sector buckets with 6 sort options
- coingecko/global     : aggregate market totals + BTC/ETH dominance
- hf/paper         : single-paper detail by arXiv id (summary, ai_summary, ai_keywords, upvotes)

Also adds clis/steam/utils.js + clis/bbc/utils.js as shared helpers (HTML entity
decode, RSS parsing). All listings carry a round-trippable id where a detail
sibling exists; advise:listing-id-pairing reports zero new violations. typed-
error-lint and silent-column-drop gates both unchanged from baseline.

Manifest: 698 → 709 (+11 entries).

* fix: tighten adapter round2 contracts
2026-05-06 12:46:14 +08:00
jakevin 6f597a2a4b feat: 8 read adapters across 5 sites (arxiv / SO / coingecko / wikipedia / hf) (#1338)
* feat: add 13 read adapters across 6 sites (github / arxiv / SO / coingecko / wikipedia / hf)

New site:
- github: user, repo, search-repos, user-repos, releases (unauth REST API; 60 req/h IP limit)

Existing sites — gap-fill for high-traffic verticals:
- arxiv author (papers by author, newest first; au:"name" phrase match on the public Atom API)
- stackoverflow user / tag (Stack Exchange API 2.3, with HTML-entity decode for display names / titles)
- coingecko coin / trending (single-coin market detail; 24h trending search-volume)
- wikipedia page (full plain-text article extract; opt-in --paragraphs cap, no silent truncation)
- hf models / datasets (downloads/likes/trending/freshness sorted lists)

All adapters use Node-side func + typed errors per the post-#1332 convention:
- ArgumentError for invalid limit / bad enum / empty positional / malformed owner-repo
- EmptyResultError for genuinely-empty results (no silent return [])
- CommandExecutionError for upstream HTTP/JSON failures (rate limit / 5xx / parse)
- AuthRequiredError reserved for endpoints that genuinely refuse anonymous traffic
- No silent clamp on --limit; no sentinel rows; no scalar 'unknown' / '-' fallbacks

Audit gates locally green:
- check:typed-error-lint        196/196 (no new)
- check:silent-column-drop      103/103 (no new)
- check:doc-coverage --strict   113/113 (added github.md, extended 5 existing pages)
- advise:listing-id-pairing     advisory only (+2 wikipedia entries: title is the
                                round-trippable key into wikipedia/page; not a gate)

* chore: drop github adapter set per WAWQAQ directive

WAWQAQ (#opencli-pr-review): "我们不需要GitHub的adapter,因为已经有GH了"

Removes the 5 github commands + utils + docs added in 664ed1aa
(github/user, github/repo, github/search-repos, github/releases,
github/user-repos). The remaining 8 read commands across 5 sites
(arxiv author, stackoverflow user/tag, coingecko coin/trending,
wikipedia page, hf models/datasets) are unaffected.

Audit gates re-checked:
- check:typed-error-lint: 196/196 (baseline unchanged)
- check:silent-column-drop: 103/103 (baseline unchanged)
- doc-coverage: 112/112 (one less site documented)
- advise:listing-id-pairing: 12 advisory (unchanged)

* fix(adapter-expansion): tighten id and currency contracts
2026-05-06 02:24:16 +08:00
hanzi 2a85152875 feat(browser): add verified fill command (#1222)
* feat(browser): add verified fill command

* feat(browser): implement exact fill primitive

* docs(browser): document fill pipeline submit

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-05-06 02:22:50 +08:00
SnakeEye-sudo (Er. Sangam Krishna) 9c9c8f976d feat: add uisdc and aibase news adapters (closes #1201) (#1249)
* Add uisdc news adapter for CLI

Implements a CLI adapter for fetching the latest AI/design news from uisdc.com. Allows specifying the number of news items to return.

* feat(aibase): add aibase daily news adapter

This file implements a news adapter for AIbase that fetches the latest AI industry news and allows for configurable limits on the number of news items returned.

* fix(news): harden uisdc and aibase adapters

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-05-06 02:14:12 +08:00
jakevin bb1208149c docs(guide): add remote-orchestration page for SSH/frpc reverse tunnel (#1337)
Document the pattern for running opencli on a remote machine while keeping
the daemon and Chrome on the local machine. Reverse-tunnel local 19825
back to the remote (via SSH -R or frp) so the remote opencli still talks
to its own loopback and the daemon never leaves localhost.

Captures the rationale we landed on after reviewing #636: native
extension-to-remote-daemon support is deferred until the daemon protocol
gains authentication; in the meantime this is the safe, zero-code path
that achieves the same outcome.
2026-05-06 02:06:14 +08:00
Greatkai d0b1b6a89e feat(pubmed): revive public eutils adapter (#819)
Co-authored-by: jackwener <jakevingoo@gmail.com>
Co-authored-by: Greatkai <4587517+Greatkai@users.noreply.github.com>
2026-05-06 01:54:29 +08:00
Shawn f1a8a2ff2d fix(chatwise): target main composer in electron UI (#427)
Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-05-06 01:45:30 +08:00
Luke 1794933d06 feat: add tiktok creator-videos command (#1335)
* feat: add tiktok creator-videos command

TikTok Studio creator content list with views/likes/comments/saves/shares.

Hits the Studio item_list endpoint
(https://www.tiktok.com/tiktok/creator/manage/item_list/v1/?aid=1988) from a
logged-in /tiktokstudio/content session and pages with cursor until limit is
satisfied (server caps size at 50). Username for the resulting video URL is
extracted from the user_text= query param on play_addr / download_info entries,
falling back to scraping a[href*="/video/<id>"] from the Studio page DOM.

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

* fix(tiktok): regen manifest + replace silent-clamp with ArgumentError

- Regenerate cli-manifest.json (CI gate: must match `npm run build` output)
- Replace `Math.max(1, Number(args.limit) || 20)` and
  `Math.min(Math.max(limit, 1), 50)` with an explicit positive-integer
  guard + a server-cap-only ternary, per the silent-clamp guidance in
  references/typed-errors.md (typed-error-lint baseline is unchanged)

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

* fix(tiktok): tighten creator videos contract

---------

Co-authored-by: root <root@example.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-05-06 00:58:38 +08:00
Carson dbf1f6afa1 feat(weixin): add Sogou article search (#1250)
- 新增 `clis/weixin/search.js`:通过 Sogou 微信搜索做公众号文章发现,`access: read`
- typed fail-fast: bad query/page/limit upfront ArgumentError;captcha/频控/goto/wait/evaluate/unreadable payload/selector drift/partial card extraction → CommandExecutionError;no-result 页面 → EmptyResultError;--limit 不 silent-clamp >10 直接拒绝
- maintainer-fixes-directly 闭环:rebase 到 latest main `f4637486`、补 access:read、删除 silent-clamp pattern
- F-P-1 first-principles 评估:Sogou 是 public 搜索页 ≠ 微信官方 API,可接受边界 = fail-fast + 清晰字段契约,不做字段猜测/partial success
- CI 全绿(smoke-test SKIPPED);B 组 codex-mini1 lead green + First-principles-1 aux green on `31c2b035`
- 非阻塞残留:search.url 是 Sogou redirect link,串联 download 需后续单独支持 redirect resolution 或暴露 resolved mp URL

Round 15a B 组对位收口。
2026-05-06 00:52:05 +08:00
jeff_woo f4637486b0 fix(twitter): rewrite followers command using DOM extraction (#1324)
- 重写 `clis/twitter/followers.js` 从 INTERCEPT (已坏) 改为 Strategy.UI DOM extraction
- bio 提取用 `data-testid$="-follow"` selector minus pattern (locale-independent button identification)
- drop `followers` column (DOM 不可靠) — manifest 同步 `strategy=ui` + columns `[screen_name,name,bio]`
- typed fail-fast: bad limit / empty user → ArgumentError;无登录 profile link → AuthRequiredError;followers link selector drift → SELECTOR typed error;空 followers → EmptyResultError;不再 silent `return []` / sentinel rows
- 恢复 `normalizeScreenName()` (opencli-user `b37f7ae3` 误删):`@elonmusk` / `/elonmusk` 不再走错路径
- `page.scroll('bottom')` no-op 改 `page.autoScroll({ times: 1, delayMs: 500 })`,避免 `--limit` 超首屏时 silent partial
- CI 全绿 (smoke-test SKIPPED);A 组 codex-mini0 lead green + First-principles-0 aux green on `dc8ac93`
- F-P-0 first-principles reflection: 长期更稳方向是 GraphQL helper (像 twitter/following),本 PR 是 INTERCEPT 已坏情况下的最小可用收口

Round 14 A 组对位收尾。
2026-05-06 00:42:10 +08:00
Jack He b485e96d90 feat(xianyu): add publish command for listing items (#1282)
- 新增 `clis/xianyu/publish.js`:发布闲鱼商品(标题/描述/分类/价格/图片/condition),`access: write`
- 参数 upfront `ArgumentError`:空 title/description/category、非法 price/original_price、未知 condition、图片格式/数量/文件不存在
- DOM/UI fail-fast:表单缺失、分类选择失败、必填字段未填、file input 缺失/上传失败、submit 失败、发布失败或超时未确认 → `CommandExecutionError`;登录墙 → `AuthRequiredError`
- 删除 `status=failed` success-row anti-pattern:失败/未知发布结果不再作为 success 表格返回
- F-P-1 aux catch real-runtime blocker:`page.url()` 在 IPage/BasePage 无定义,test mock `url` 字段遮住;改 `page.getCurrentUrl()` + fallback publish URL,加 IPage-shape 回归锁住
- xianyu publish JSDOM 回归 + docs/README/index 同步
- B 组 codex-mini1 lead green + First-principles-1 aux green on `2d78144d`

Round 14 (B 组对位)。
2026-05-05 23:35:23 +08:00
YoungCan-Wang 36d22ef4d3 feat(codex): add projects/history sidebar commands with native click + typed fail-fast (#1307)
- 新增 `clis/codex/sidebar.js` 共享 helper,使用 `data-app-action-sidebar-*` DOM 属性 + native click 触发侧边栏导航
- 新增 `clis/codex/projects.js` 与 `clis/codex/history.js`:列表/详情双契约
- typed fail-fast 收口:
  * projects/history 空列表 → EmptyResultError
  * limit/timeout/index/thread-id 非法 → ArgumentError(拒绝 silent-clamp)
  * 目标项找不到 → EmptyResultError
  * sidebar DOM 缺失 → CommandExecutionError
- 新增 8 条 regression 单测(`clis/codex/sidebar.test.js`)
- B 组 codex-mini1 lead green + First-principles-1 aux green on `bcfd88ae`

Round 13 close-out.
2026-05-05 23:03:47 +08:00
jakevin 76869bf8be docs(adapter-author): typed-errors reference + 6 conventions from #1329 (#1332)
纯 doc PR (+251/-37, 3 files),codify Round 12-13 #1329 三轮 review 沉淀的 6 条规则。

**新增 `references/typed-errors.md`** (~190 LOC):
- 5-classification 落点表 (ArgumentError / AuthRequiredError / EmptyResultError / CommandExecutionError + 第五类) 清晰判定边界
- 4 大独立 anti-pattern:silent-clamp / sentinel-row / scalar sentinel (`'-' → null`) / generic `CliError('CODE')`
- 反例引用 commit-pinned GitHub permalink (`384bcd6f` / `42e5303c` / `2b8609b8`) 防 merge 后行号漂移

**`references/adapter-template.md` 翻转**:
- 翻过期"sentinel row 比 [] 安全"建议
- 补 browser-vs-signature callout (#1329 author lesson 8 处 `(_page, args)` 错签)
- 补 intermediate-object naming 规则 (R1 lesson)
- example/COOKIE 骨架改 typed errors,convertible.js 标 grandfathered
- `page.fetchJson()` 正例从 `Number(args.limit) || 20` 改成复用已显式 validate 的 `limit` (F-P-0 catch 模板自洽性 blocker)

**`SKILL.md`**: reference table row + 3 条新约定 (中间对象 key / browser-signature / typed-error routing) 替掉过时的 `CliError('CODE')` 建议

Author: @opencli-user (jackwener)
A 组 review:
- codex-mini0 lead: 推 `971d198` (anti-pattern 拆分 + commit-pinned permalink + adapter-template 措辞) + `7921c6b` (修 page.fetchJson 模板自洽性 blocker)
- First-principles-0 aux: catch page.fetchJson `Number(args.limit) || 20` 模板会被照抄 silent-clamp 的自洽性 blocker
2026-05-05 18:32:59 +08:00
jakevin a5d70466ba feat: add qwen / 1point3acres / coingecko adapters (#1329)
3 new sites / 17 adapters,+2471 LOC。

**adapters**:
- coingecko (PUBLIC): `top` 全球加密货币市值排行
- 1point3acres (Discuz, GBK/UTF-8 mixed): `digest/forum/hot/latest/search/notifications/thread/user`
- qwen (browser, COOKIE chat): `ask/send/image/history/status`

**typed fail-fast 全闭环 (A 组三轮迭代后)**:
- silent-column-drop heuristic key collision 修法:rename intermediate keys 避开 columns 名字
- 所有外部参数 (limit/page/contentLimit/timeout/page_size) 越界/非法 → `ArgumentError`
- fetch / non-2xx / malformed JSON / API error → `CommandExecutionError`
- empty / not-found → `EmptyResultError`
- qwen prompt 缺失 → `ArgumentError` (不是 CommandExecutionError)
- qwen/status 未知 model/session 用 typed `null` (不是 `'-'` sentinel)
- success-row 永远不塞 failure/empty 业务行
- `normalizeLimit(value, default, max, label)` 共享 helper for 1point3acres 5 adapters

Author: @opencli-user (jackwener)
A 组 review (三轮):
  - codex-mini0 lead: 第三轮 maintainer-fixes-directly 直接 push `c40daf7` 收掉 4 类深一层 contract 漏洞
  - First-principles-0 aux: 第二轮 catch typed-error-lint 9 条 + 第三轮 catch generic CliError / silent-clamp on page/timeout / success-row failure / qwen prompt class 4 类 hard blocker
2026-05-05 18:00:33 +08:00
JackyWay 7aeaa053f9 fix(xianyu): chat send button detection + textarea activation (#1328)
- normalizeBtn() 去全部 whitespace 覆盖真实 `发 送`
- async IIFE send path + textarea click/focus 后 set value + dispatch input/change 触发按钮渲染
- send-button-not-found → typed selectorError fail-fast,不再 silent success
- JSDOM 直接执行 in-browser script regression 覆盖 mocked-evaluate 抓不到 DOM 内部 bug

Author: @JackyWay
B 组 review: codex-mini1 lead (rebase + 测试补全) + First-principles-1 final aux green
2026-05-05 17:36:36 +08:00
jakevin 97708ac858 feat(help): split root --help adapters into External CLI / App / Site buckets (#1330)
Per WAWQAQ feedback in #OpenCLI thread on the flat "Site adapters (112)" listing:
the bucket conflates real web sites (bilibili, dianping, ...) with desktop apps
(chatgpt-app, chatwise, codex, cursor, discord-app, doubao-app, antigravity, notion).
Group them so agents that fall back to --help can scan by category.

Three buckets, sourced from existing metadata only — no new adapter schema:

- External CLIs: passthrough binaries from loadExternalClis() (docker, gh, vercel, ...)
- App adapters:  domain is `localhost` or any non-DNS string (no `.`)
- Site adapters: domain contains `.` (real DNS), or domain is unset (default)

The classifier is one line: `domain.includes('.') ? 'site' : 'app'`. Adapters
without a domain field default to site (most are public web scrapers like
arxiv / wikipedia / spotify / ...).

Verified against the live registry: 7 External CLIs, 8 App adapters
(antigravity, chatgpt-app, chatwise, codex, cursor, discord-app, doubao-app,
notion), 104 Site adapters.

Structured help (-f yaml/json) gains parallel `external_clis` / `app_adapters`
/ `site_adapters` keys; `commands` no longer leaks adapter names.

External CLIs are now hidden from the default Commands listing (mirrors how
site adapters were already filtered) and surfaced in their own section.
2026-05-05 15:11:53 +08:00
jakevin 65979f26c2 refactor(test): extract shared page mock, remove dead test (#1321)
- Add clis/test-utils.js with standard createPageMock utility
- Migrate 11 test files to use shared utility (removes ~300 lines of duplication)
- Delete extension/src/cdp.test.ts dead skip test (chrome.scripting.executeScript removed from source)
- Remove clis/pixiv/test-utils.js (superseded by shared utility)
2026-05-05 01:39:22 +08:00
jakevin 05f7217edf bump version to 1.7.12, extension to 1.0.5 (#1320) v1.7.12 2026-05-05 01:10:29 +08:00
jakevin 2b15016801 docs(adapter-author): add jsdom-fixture-pattern reference for in-browser DOM extractors (#1319)
Codify the JSDOM-against-frozen-fixture pattern that PR #1313 introduced
for dianping (and that PR #1318 had to follow up to clean up). The skill
previously had no reference for this category of test, so authors of the
next adapter that hits silent-in-browser-DOM bugs would either reinvent
it or skip it.

Key conventions captured:

  - **Mandatory awk 'NF>0' as the final step of fixture creation.** The
    blank-line noise that PR #1318 removed (84.6% / 54.8% of file content
    in dianping/{shop,search}.html) came from manually stripping
    script/style content without collapsing the surrounding newlines.
    Skipping this step is the silent quality regression that the next
    fixture author would also hit.

  - **Trim-to-minimum but never re-flow content.** Some bugs depend on
    text-node adjacency without intervening whitespace
    (dianping #1312 bug #2: rating "4.8" + reviews "21241条" fused as
    "4.821241条"). Pretty-printing the meaningful mega-line would mask
    the very condition the test is meant to catch.

  - **Reverse-validate the regression guard.** "18/18 tests pass" only
    proves agreement with the current implementation, not that the test
    would have caught the original bug. Reintroducing the buggy variant
    must make the test fail — otherwise the fixture is over-stripped or
    the assertion is too loose.

  - **__fixtures__/ is the documented exception** to the "no committed
    HTML dumps" rule in the skill's "关键约定". Calling that out
    explicitly because the rule otherwise reads as "all HTML in repo is
    bad," which the dianping fixture pattern intentionally violates for
    a real reason.

Background: WAWQAQ in #1313 follow-up thread (`#OpenCLI:36d2f65a`) asked
twice — first about the visible blank-line noise (→ PR #1318 cleanup),
then about the root cause and what should improve in the workflow itself.
This is the workflow improvement.

No new tooling / CI gate / lint introduced (B 1-week gate freeze still
applies). When a fifth fixture site adopts this pattern,
`opencli browser fixture-snapshot` automation can be revisited; until
then, runbook discipline + skill reference is the right scope.
2026-05-05 00:58:49 +08:00
jakevin d071600684 chore(dianping/fixtures): strip whitespace-only lines from frozen HTML fixtures (#1318)
dianping/__fixtures__/search.html and shop.html came out of page.content() with
hundreds of blank lines that JSDOM ignores during parsing — pure visual / disk
noise that bloats reviewer diff and obscures the meaningful DOM subtree the
fixture freezes.

  search.html: 372 → 168 lines (-204 lines, -572 bytes)
  shop.html:    39 →   6 lines (-33 lines,  -64 bytes)

`awk 'NF>0'` keeps every line that has any non-whitespace character, so the
minified mega-line (where the rating-vs-reviews adjacency that triggers #1312
silent fusion lives) is preserved verbatim. dianping.test.js 18 tests still
pass, and reintroducing the buggy `headText.match(/(\d+)条/)` extractor still
makes the regression guard fail with the expected '821241条' (proving the
fusion-bug detection power is intact after the strip).

Per WAWQAQ feedback in #1313 thread; opus independently validated the same
approach before the cleanup.
2026-05-05 00:51:00 +08:00
jakevin 6985187705 test(dianping): JSDOM-against-frozen-fixture tests for in-browser extractors (#1313)
PR #1312 fixed two silent in-browser DOM bugs that the existing mocked
`page.evaluate` tests could not catch:

  1. shop title fallback split on ASCII `[]` while dianping renders
     full-width `【】`, so `name` was always empty (or `"undefined"`).
  2. headText `\s+` collapse fused rating "4.8" with reviews "21241条",
     so a head-wide `/\d+条/` regex captured "4.821241" → 5.

Both bugs only surfaced on live verify; mocked-evaluate unit tests fed
pre-baked results to the func and the real DOM walk never ran.

Make the in-browser extractor logic testable in CI:

  - clis/dianping/shop.js, clis/dianping/search.js: extract the IIFE
    bodies into top-level `extractShopFields()` / `extractSearchRows()`
    using bare `document` / `location`. The live adapters inject these
    via `page.evaluate(\`(\${fn.toString()})()\`)` so behavior is
    unchanged; both commands re-verified end-to-end against live
    dianping (shop returns name=芈重山老火锅(五道口店), reviews=21241,
    rating=4.8; search returns 3 result-shaped rows with correct ids).

  - clis/dianping/__fixtures__/shop.html (3.4KB), search.html (8.4KB):
    sanitized HTML snapshots — scripts/styles/iframes/comments stripped,
    img src placeholdered, only structural attributes kept. Trimmed to
    the minimum subtree needed to exercise the extractors (search keeps
    3 of 15 li cards; shop keeps .shop-head + .desc-info + .review-title
    plus full-width 【】 title and headText with the rating/reviews
    fusion preserved).

  - clis/dianping/dianping.test.js: add a fifth describe block —
    "extractors against frozen HTML fixtures" — that loads the fixtures
    via JSDOM, swaps `globalThis.document` / `globalThis.location`, and
    asserts the post-fix behavior:
      * shop: name=芈重山老火锅(五道口店), reviewsRaw=21241条, rating=4.8,
        breakdown={口味:4.8,环境:4.8,服务:4.8,食材:4.9}, hours, rank, subway.
      * search: 3 rows with correct shop_ids, names, reviewsRaw, priceRaw,
        starClass; round-trip through parseReviewCount/parsePrice mappers
        to lock in {rating:5.0,reviews:21231,price:109} et al.
      * ok:false branches: shop fixture without `.shop-head`, search
        fixture with empty `#shop-all-list`.

Manually verified the fixtures would catch the original bugs by running
buggy extractor variants against shop.html — ASCII-bracket fallback
returns `name="undefined"`, and head-wide `/\d+条/` returns `821241条`
(both fail the new assertions).

Test suite grows 14 → 18 passing tests; live verify of both commands
still produces correct output post-refactor.

Pattern intentionally limited to dianping as a reference point. If other
sites with in-browser DOM extraction encounter similar silent bugs, this
JSDOM-against-frozen-fixture pattern can be adopted per-site.
2026-05-05 00:42:28 +08:00
Benjamin Liu de0cee191c docs(cases): add three researcher workflow examples (#1317)
* docs(cases): add three researcher workflow examples

Add use cases under cases/ that exercise the recently-landed
researcher-friendly adapters:

- daily-rl-research-monitor.md uses arxiv recent + openreview venue
  + hf top to compress a morning paper-skim into one shell pipeline.
- find-paper-implementation.md chains arxiv search/paper + dblp
  search + hf top + openreview search to map a paper's canonical
  record, follow-ups, and community uptake.
- track-conference-papers.md walks openreview venue + reviews to
  shortlist accepted papers and digest review threads in batch.

Each file is a real workflow built on commands from #1289 (arxiv
recent), #1294 (openreview), and #1299 (dblp).

* docs(cases): correct venue ids and forum example to ones that return data

The first revision used "ICLR.cc/2026/Conference" and "ICLR 2026 oral"
as venue strings. Both return EMPTY_RESULT today because the venue is
not open. Update each case to use natural-language venue text that
OpenReview currently exposes ("ICLR 2024 oral", "NeurIPS 2025 oral")
and a real forum id (KS8mIvetg2, "Proving Test Set Contamination in
Black-Box Language Models") in the reviews / paper drill-down. Note
the arxiv free-text-search ranking quirk so the worked DPO example
makes sense.
2026-05-05 00:35:33 +08:00
jakevin 4de1b42ab7 chore(convention): retire listing↔detail id pairing CI gate, keep advisory (#1316)
PR #1297 introduced a CI gate that fails when a site has both a listing
and a detail command but the listing rows don't carry an id-shaped column.
The gate came with a 10-entry EXEMPT map (topic-string trending,
profile-attribute rows, UI-only sessions, ...) where each exemption
recorded a "why this listing legitimately doesn't pair" reason.

By the same filter that closed PR #1311 (write-without-delete-pair gate):

  Is "listing should pair with detail" a *permanent* anti-pattern, or
  case-by-case business judgment?

It's case-by-case. Topic-string listings and profile-attribute rows
genuinely don't pair with a detail command. The fact that we needed an
EXEMPT map with 10 entries and individual reason strings is the smell —
it's not the rule winning, it's the rule failing. Forcing every adapter
PR to either add an id column or file an exemption was a higher cognitive
cost than the silent-loss bugs the rule actually catches.

Changes:

- .github/workflows/ci.yml — drop the "Check listing↔detail id pairing"
  step. Other gates (silent-column-drop, typed-error-lint) stay in place.
- package.json — rename the script from `check:listing-id-pairing` to
  `advise:listing-id-pairing` to make the advisory nature explicit.
- scripts/check-listing-id-pairing.mjs — drop the `--strict` flag and the
  EXEMPT map. The script now always exits 0 and prints an advisory report
  of listings that don't carry an id-shaped column. Reviewers/authors use
  it as guidance, not a gate.
- docs/conventions/listing-detail-id-pairing.md — rewrite from "MUST" to
  "soft convention". Adds an explicit "why advisory, not a gate" section
  that lists the legitimate non-pairing categories so future readers know
  the rule's boundary.
- docs/developer/ts-adapter.md — match the advisory tone in the
  adapter-author guidance.

The doc, the script, and the column patterns table all stay — agents and
adapter authors can still consult them. What's gone is the CI failure and
the per-PR exempt-list maintenance burden.

Net diff: -34 lines (gate + EXEMPT map removed, advisory-tone doc adds
a small "why advisory" section).
2026-05-05 00:34:18 +08:00
jakevin c4a1d2a91c fix(audit): reduce silent column drop false positives (#1315) 2026-05-05 00:29:20 +08:00
jakevin e6b048b86c feat(browser): enforce verify row shape (#1314) 2026-05-05 00:16:51 +08:00
jakevin 5ad0b81d92 ci: gate new typed error lint violations
Adds a baseline CI gate for convention-audit typed-error lint findings. Also refreshes the silent-column-drop baseline for dianping changes already on main.
2026-05-04 23:45:36 +08:00
jakevin 46d0f24f57 ci: gate new silent column drops
Adds a baseline CI gate for convention-audit silent-column-drop findings so CI rejects only newly introduced table-output loss.
2026-05-04 23:24:13 +08:00
jakevin c1bbf0bc5d fix(dianping/shop): correct in-browser name and reviews extraction (#1312)
The merged adapter had two silent in-browser bugs that the mocked-evaluate
unit tests don't catch — only live verify against www.dianping.com surfaces
them:

1. Shop name returned `undefined`. The fallback parsed `document.title` with
   an ASCII-bracket split (`/[\\[\\]]/`) but dianping wraps the name in
   full-width brackets `【芈重山老火锅(五道口店)】...`. Switch to a `【...】`
   regex so the title fallback actually fires.

2. Reviews returned `5` instead of `21241`. The headText was whitespace-
   collapsed to `★★★★★4.821241条...`, fusing the rating and review digits;
   a head-wide `/\d+条/` then captured `4.821241` and rounded to `5`. Read
   the dedicated `.reviews / .review-num` element ("21241条") instead, with
   a `.review-title` "评价(<n>)" fallback.
2026-05-04 23:16:07 +08:00
jakevin 0f806e9473 feat(dianping): browser adapter — search + shop on www.dianping.com (#1309)
* feat(dianping): browser adapter — search + shop on www.dianping.com

Adds two browser-mode adapters for the dianping (大众点评) PC site:

- `dianping search "<keyword>" --city <name|id> --limit <n>`: keyword
  shop/restaurant search. Returns rank, shop_id, name, rating, reviews,
  price, cuisine, district, url. shop_id round-trips into `dianping shop`.
- `dianping shop <shop_id>` (alias `detail`): shop detail sheet
  (field/value rows: name, rating, breakdown 口味/环境/服务/食材, reviews,
  price, rank, hours, address, subway, features, url).

Both use Strategy.COOKIE on www.dianping.com (the PC site renders search
SSR and does not require JS hydration). m.dianping.com is intentionally
crippled for non-mobile UAs, so it's not used.

Auth detection (utils.detectAuthOrEmpty) inspects both response text and
final URL for the Meituan Yoda captcha redirect (verify.meituan.com) and
the dianping login redirect; raises AuthRequiredError with the captcha
URL embedded so the user can clear it manually in the same profile.

Listing↔detail id pairing: search.shop_id → shop.<id>. Adds 'shop' to
DETAIL_NAMES in scripts/check-listing-id-pairing.mjs so the convention
gate scans this site (35 sites / 78 listings now covered).

* fix(dianping): harden browser failure classification

* fix(dianping): fail on partial missing shop ids
2026-05-04 23:00:38 +08:00
jakevin 73dc1295e7 feat(cli): add convention audit command
Adds opencli convention-audit for batch convention scanning, with structured output, strict mode, docs, and startup isolation from local user/plugin discovery.
2026-05-04 22:52:42 +08:00
jakevin f482a6b2b1 feat(youtube/xiaohongshu/xiaoe): surface dropped ids/url on listings (sweep) (#1305)
* feat(youtube/xiaohongshu/xiaoe): surface dropped ids/url on listings (sweep)

Round 8 same silent-column-drop class as #1300/#1301/#1302 — row already
emits the id/url field but `columns` array forgot to project it, so table
view drops it and agent loses the chain into detail commands.

- youtube/feed: rename row.videoId → video_id (snake_case convention),
  add to columns. youtube/video accepts both URL and id, so url-based
  round-trip already worked, but exposing the canonical id removes the
  url-parse step for chained calls.
- xiaohongshu/feed: pipeline map already extracts `id` from the homefeed
  payload, columns now lists it.
- xiaoe/catalog: pipeline map already projects `url`, columns now lists
  it. xiaoe/detail takes a positional url, so this completes the
  round-trip explicitly.

Also fixes one camelCase column violation on youtube/feed (videoId vs the
project's snake_case convention as in twitter `is_retweet`/`created_at`,
douban `subject_id`/`photo_id`, hupu `thread_title`).

CI gate `check:listing-id-pairing` ✓ (34 sites, 77 listings, 10 exempt).
typecheck clean. 114 tests pass for youtube + xiaohongshu.

* fix(youtube): keep feed continuation ids after rename
2026-05-04 22:03:52 +08:00
jakevin 0d37f48626 feat(cli): add agent-native structured help (#1304) 2026-05-04 21:55:09 +08:00
jakevin edf3c07d66 add cases/ directory for collecting user use cases (#1303)
Users can submit PRs adding individual .md files — one per case,
no merge conflicts.
2026-05-04 21:26:10 +08:00
jakevin ac94b75879 feat(1688/hupu/douban/linux-do): surface dropped ids on listings (#1302)
Round 7 — silent-drop sweep. Continues the listing→detail id-pairing
work from #1297. Each row was already extracting these ids/urls
internally; only the `columns` projection was missing, so they showed
up in `-f json` but never on the table view.

| Adapter            | Added columns                       |
|--------------------|-------------------------------------|
| `1688 search`      | `item_url`, `member_id`             |
| `hupu mentions`    | `tid`, `pid`, `url`                 |
| `douban photos`    | `photo_id`, `subject_id`            |
| `linux-do tags`    | `slug`                              |

Round-trip wins:
- `1688 search` → `1688 item <item_url>` (item_url is the canonical
  detail.1688.com URL); `1688 search` → `1688 store <member_id>`
- `hupu mentions` → `hupu detail <tid>` (and `pid` for the deep link)
- `douban photos` → tied back to the parent movie via `subject_id`
- `linux-do tags` → `linux-do feed --tag <slug>` (slug is the URL form)

No logic change — only the column array. JSON output unchanged.
Tests: 45/45 pass for the four affected sites.
2026-05-04 21:22:23 +08:00
jakevin ae9ad4aeec feat(twitter): surface tweet id on bookmarks/likes/tweets listings (#1301)
Round 6 — silent-drop audit follow-up. Sibling twitter listings have
been inconsistent about the canonical tweet `id` (rest_id):

- timeline      ✓ exposes id
- search        ✓ exposes id
- list-tweets   ✓ exposes id
- notifications ✓ exposes id
- bookmarks     ✗ extracts but drops it from columns
- likes         ✗ extracts but drops it from columns
- tweets        ✗ extracts but drops it from columns

The `id` is already in the row object — only the `columns` projection
was missing. With the listing↔detail id-pairing CI gate from #1297 now
on main, surfacing `id` makes round-trip into `twitter thread <id>` /
`twitter delete <id>` / `twitter like <id>` work from the table view too
(previously only via `-f json`).

Other field-presentation drift (`name`, `created_at`, `retweets`)
aligned with sibling adapters where those values are already emitted.

Tests: tweets.test.js asserts `toEqual` on the columns array — updated
that assertion. Other twitter tests use `toMatchObject` and pass
unchanged. 81/81 in `clis/twitter/`.
2026-05-04 21:22:10 +08:00
jakevin 2b9af38db4 feat(pixiv): surface user_id + url on listings, url on user/illusts (#1300)
While auditing instagram/facebook/pixiv coverage gaps, found that pixiv
listings already extract `user_id` and construct `url` per row but drop
both fields from the table view (`columns` doesn't list them). The data
is in the row object — only the column projection was missing.

Per the listing↔detail id pairing convention (#1297), surface them so:
- `user_id` round-trips from `ranking` / `search` → `user` / `illusts`
- `url` is the canonical share link for every illust / user record

Changes:
- `ranking`: + user_id, + url
- `search`:  + user_id, + url
- `illusts`: + url (user_id is the arg, no need to repeat per row)
- `user`:    + url

No behavior change beyond the table view — JSON output already had these
fields, so existing scripts that consume `-f json` keep working.
2026-05-04 21:09:01 +08:00
jakevin 545f91a2d2 feat(dblp): public bibliography adapter — search + paper (#1299)
* feat(dblp): public bibliography adapter — search + paper

Wraps the dblp.org public API:
- `dblp search <query>` → /search/publ/api JSON, projected into one row per hit
- `dblp paper <key>` → /rec/<key>.xml, parsed into a one-row record

Why dblp on top of arxiv/openreview: dblp is the largest, oldest CS
bibliography (7M+ entries) and the only one of the three that consistently
indexes pre-arXiv literature, journal articles, books, and theses. The
canonical record key (e.g. `conf/nips/VaswaniSPUJGKP17`) round-trips
cleanly between the two commands per the listing↔detail convention.

Implementation notes:
- No deps beyond the registry — XML parsed with conservative regexes,
  same approach as the arxiv adapter.
- Polite User-Agent per dblp's API guidance; HTTP 429 mapped to a
  CommandExecutionError with a "lower --limit" hint.
- Author homonym suffixes (`"Smith 0001"`) trimmed for clean output.
- 39 unit tests cover validators, XML extraction, both commands.

* fix(dblp): fail fast on API status envelopes
2026-05-04 21:04:50 +08:00
jakevin 0a85e73aa5 feat(convention): listing↔detail id pairing rule + CI gate (#1297)
* feat(convention): listing↔detail id pairing rule + CI gate

Adds a hard convention: when a site exposes both a listing-class command
(search / hot / top / recent / ...) and a detail-class command (read /
paper / article / view / ...), every listing row MUST surface an id-shaped
column whose value round-trips into the detail command. Without that, an
agent has no way to follow up on a listing row except re-searching by
title or scraping URLs out of band — both of which break the agent-native
contract.

What's in this PR

- docs/conventions/listing-detail-id-pairing.md — full rule, examples
  table, why-it-matters, what counts as id-shaped, exemption taxonomy,
  how to add an id column to a listing.
- scripts/check-listing-id-pairing.mjs — validator that reads
  cli-manifest.json, classifies each entry as listing / detail / other,
  and fails when a listing on a site that also has a read-detail command
  is missing an id-shaped column. Exemption allowlist records WHY each
  pair is exempt so future maintainers know what to verify.
- npm run check:listing-id-pairing — strict-mode wrapper.
- CI: new step in build job runs the validator after the manifest
  freshness check on Linux.
- docs/developer/ts-adapter.md — cross-link from the adapter authoring
  guide.
- docs/.vitepress/config.mts — sidebar entries for the new conventions
  section.

Fixes brought to zero violations

- 1688/search: add offer_id (already extracted, just surfaced)
- bluesky/user: add uri (AT URI round-trips into bluesky/thread)
- tieba/search: add id + url (thread_id already extracted)
- tieba/hot: add url (rows are topics, not threads — url is the
  best-effort round-trip handle, doc'd as such)

Exemptions (intentional, doc'd in EXEMPT map with rationale)

- nowcoder/hot, bluesky/trending, twitter/trending — listing rows are
  topic strings, not posts.
- lesswrong/user, reddit/user — rows are profile-attribute key/value
  pairs, addressed by the username arg.
- discord-app/search — desktop UI session, message ids not extractable.
- notion/search — Strategy.UI Quick Find, page ids not exposed in DOM.

Validator output after this PR: 32 sites scanned, 75 listings checked,
7 exempted, 0 violations.

* fix(convention): tighten listing id gate

* fix(convention): close url-derived id loophole
2026-05-04 20:54:14 +08:00
jakevin 29b4869efd feat(indeed): add search and job adapters (US site) (#1298)
* feat(indeed): add `search` and `job` adapters (US site)

Adds an Indeed adapter that fills the US job-search gap (alongside
existing 51job / boss-zhipin / linkedin coverage). Both commands run
through a real browser session because Indeed sits behind Cloudflare
and answers bare HTTP fetches with `403` + `cf-mitigated: challenge`.

## Commands

- `indeed search <query>` — keyword job search
  - args: `query`, `--location`, `--fromage`, `--sort`, `--start`, `--limit`
  - columns: `rank, id, title, company, location, salary, tags, url`
- `indeed job <jk>` (alias `detail`, `view`) — full job posting
  - args: `id` (positional, the 16-char hex `jk` from `search`)
  - columns: `id, title, company, location, salary, job_type, description, url`

## Listing↔detail id pairing

`search.id` is the Indeed `jk` (job key, 16-char lowercase hex). It feeds
directly into `indeed job <jk>`. Conforms to the listing↔detail id
pairing convention proposed in #1297.

## CF challenge handling

The adapter polls the result selectors for up to 15s after navigation,
giving the browser time to clear the Cloudflare interstitial. If the
challenge is still up after the wait, the adapter throws a
`CommandExecutionError` with a hint pointing the user at the connected
browser to clear it once. Subsequent calls reuse the warmed cookies via
`Strategy.COOKIE`, mirroring the v2ex / boss / linkedin patterns.

## Validation

`utils.js` keeps argument validation pure and unit-testable:

- `requireJobKey` rejects anything that isn't a 16-char lowercase hex
- `requireFromage` only accepts `1` / `3` / `7` / `14` (Indeed's enum)
- `requireSort` only accepts `relevance` / `date`
- `requireBoundedInt(limit, default=15, max=25)` — Indeed serves at most
  one page (10 jobs/page); ArgumentError on out-of-range, no silent
  clamping, per the typed-error feedback in #1289.

## Tests

18 unit tests in `clis/indeed/indeed.test.js` cover registration,
validators, URL builders, and DOM-card normalizers. Browser-driven
verification stays out of CI by design (CF challenge is interactive).

## Docs

- `docs/adapters/browser/indeed.md` — full adapter doc with prerequisite
  CF-challenge notes and listing↔detail id pairing callout.
- Sidebar entry + adapter index row.

* fix(indeed): tighten timeout fail-fast and runtime tests

* fix(indeed): align readiness with search parser
2026-05-04 20:52:16 +08:00
jakevin eea9ff8bfe feat(cli): add command access metadata (#1296) 2026-05-04 19:47:08 +08:00
jakevin 328140966e feat(openreview): public adapter — search/venue/paper/reviews (#1294)
* feat(openreview): add public adapter — search/venue/paper/reviews

OpenReview is the open peer-review platform used by ICLR / TMLR / COLM
and ML workshops. Its v2 API exposes everyone-readable submissions,
reviews, and decisions without auth, so all four commands run with
`browser: false`.

Commands:
- `openreview search <query>` — full-text search
- `openreview venue <venue>` — list submissions; accepts either a venue
  display name (matched against `content.venue`, e.g. "ICLR 2024 oral")
  or a full invitation id (e.g. "ICLR.cc/2025/Conference/-/Submission")
  via `/-/` heuristic; supports offset pagination
- `openreview paper <id>` — single-paper detail with full abstract
- `openreview reviews <forum>` — paper + threaded reviews/decisions/
  comments, ordered chronologically with paper lifted to row 0;
  classifies notes via invitation tail (REVIEW / DECISION / REBUTTAL /
  COMMENT / META_REVIEW / WITHDRAWAL); per-row truncation via
  `--max-length` (min 200)

Listing IDs round-trip into `paper`/`reviews`. PDF URLs normalized to
absolute `https://openreview.net/pdf/...`. `pdate` falls back to
`cdate` when missing, formatted as `YYYY-MM-DD`.

All limits/offsets/ids fail-fast with typed errors (`ArgumentError`,
`EmptyResultError`, `CommandExecutionError`) — no silent clamping, no
empty-array fallbacks. fetch + json + non-2xx + 404 are wrapped so
network/API failures never look like empty results.

Tests: 23 unit tests covering the column contract, content extraction,
date/PDF normalization, invitation-vs-venue dispatch, error paths
(network/JSON/HTTP), pagination offset accounting, and the reviews
classifier + section joiner + truncation.

Live-verified against api2.openreview.net for search ("diffusion
model"), venue ("ICLR 2024 oral"), paper (KS8mIvetg2), and reviews on
that paper's full thread.

* fix(openreview): tighten error and review typing

* fix(openreview): stabilize review contracts
2026-05-04 19:23:55 +08:00
jakevin ed0b2acc82 docs(stackoverflow): clarify read fetches answers up to --answers-limit (not 'all') (#1295)
Follow-up from PR #1293 review: 'all answers' was misleading because
the implementation is limit-bounded (default 10, max 100) rather than
unbounded pagination. Spell out the actual contract — including the
accepted-answer-outside-page fallback path — so users don't expect
infinite-scroll behaviour.

Non-blocking docs-only change flagged by codex-mini1 + First-principles-1
during #1293 review.
2026-05-04 19:20:43 +08:00
jakevin c1a4bd3b7e feat(stackoverflow): surface question_id on listings + new read <id> (#1293)
* feat(stackoverflow): surface question_id + metadata on listings, add `read <id>`

Agent-native gap: all 4 stackoverflow listings (`hot`, `search`,
`unanswered`, `bounties`) only emitted `[title, score, answers, url]`,
which means an agent could see a hot question but had no `id` to round-
trip into a body read, no `tags` to filter by topic, no `views` to gauge
demand, and no `is_answered` / `creation_date` / `author` to triage.
There also wasn't a `read` adapter, so reading a SO question through
opencli was impossible.

Listings (`hot` / `search` / `bounties` / `unanswered`):
- Add `rank`, `id` (question_id), `views`, `is_answered` (skipped on
  `unanswered` since always false), `tags` (joined), `author`
  (owner.display_name), `creation_date` columns.
- Pass `pagesize` to the upstream API instead of fetching the default
  page and trimming locally.

New `stackoverflow read <id>`:
- 4-call fan-out against the public Stack Exchange API
  (`/questions/{id}` + `/questions/{id}/comments` +
  `/questions/{id}/answers` + batched `/answers/a;b;c/comments`).
- Returns `POST` + `Q-COMMENT` + `ANSWER` + `A-COMMENT` rows mirroring
  the `hackernews read` and `lobsters read` shape.
- Accepted answer is always surfaced first and tagged `accepted='true'`;
  remaining answers follow in descending vote order, capped by
  `--answers-limit`.
- HTML body cleanup: tags stripped, `<pre><code>` preserved, `<code>`
  inline-fenced, `<li>` rendered as `- `, comments indented with `> `.
- Entity decoding: a shared `decodeEntities` handles named (incl.
  `&hellip;`/`&copy;`/etc), decimal (`&#246;`), and hex (`&#x27;`)
  forms, applied to both bodies AND `display_name` (otherwise users
  like `Jonas K&#246;lker` come through mojibaked).
- Typed fail-fast: `ArgumentError` for non-numeric id and
  `--max-length < 100` (with no-fetch assertion); `EmptyResultError`
  when `items` is empty; `CommandExecutionError` for HTTP non-2xx and
  for Stack Exchange's in-band `error_id` envelopes (throttle / quota).
  No silent clamps anywhere.

Tests: 14 vitest assertions
- 4 listing column-shape (incl. `unanswered` skipping `is_answered` and
  `bounties` keeping its `bounty` column position)
- 10 read-adapter cases: registration / args / strategy + 3 typed-error
  fail-fast paths (with no-fetch assertion on the pre-fetch ones) + the
  full POST/Q-COMMENT/ANSWER/A-COMMENT row order with accepted-first +
  the answer-comments fetch verified to batch ids semicolon-joined +
  HTML entity decoding (named/decimal/hex) on both body and display_name
  + answers-limit honored when there are more answers than the cap.

Live verification:
- `stackoverflow hot --limit 2` → `id`/`tags`/`views`/`is_answered`/
  `author` populated.
- `stackoverflow search "async await" --limit 1`,
  `stackoverflow unanswered --limit 1` → same shape.
- `stackoverflow read 79935770` and the very-long classic question
  `stackoverflow read 11227809 --answers-limit 1 --comments-limit 2`
  → produces the threaded POST/Q-COMMENT/ANSWER/A-COMMENT structure
  with proper entity decoding (`Jonas Kölker` reads correctly).
- `stackoverflow read not-numeric` → exits with `ARGUMENT`.
- `stackoverflow read 999999999` → exits with `EMPTY_RESULT`.

* fix(stackoverflow): wrap fetch/json/coerce paths in typed errors

Apply the 3 lessons from PR #1292 (devto) review at merge time, before
B-group hits this PR:

1. CLI args may arrive as strings (e.g. `--max-length 50` → `'50'`).
   The bare `Number.isInteger(value)` in `requirePositiveInt` /
   `requireMinInt` would accept negative-but-coerced numbers and reject
   string-form integers. Now the helpers `coerceInt` first then validate,
   and the rejection message echoes the raw input via `JSON.stringify`.

2. `await fetch(url)` and `await res.json()` were not wrapped — a network
   blip would surface as a raw `TypeError` and a maintenance HTML page
   would surface as a raw `SyntaxError`. Both are now caught and rethrown
   as `CommandExecutionError` with hints, matching the in-band error_id
   path.

Tests: +3 cases (17 total)
- fetch network failure → CommandExecutionError
- malformed JSON body → CommandExecutionError
- string-form max-length "50" / "abc" rejected with ArgumentError before
  fetching

* fix(stackoverflow): avoid partial read fanout
2026-05-04 19:10:50 +08:00