Files
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

51 lines
2.0 KiB
JavaScript

// stackoverflow user — search Stack Overflow users by name and return profiles.
import { cli, Strategy } from '@jackwener/opencli/registry';
import {
seFetch,
normalizeLimit,
requireString,
epochToDate,
ensureItems,
decodeHtmlEntities,
} from './utils.js';
cli({
site: 'stackoverflow',
name: 'user',
access: 'read',
description: 'Find Stack Overflow users by display name (highest reputation first).',
domain: 'stackoverflow.com',
strategy: Strategy.PUBLIC,
browser: false,
args: [
{ name: 'name', positional: true, required: true, type: 'string', help: 'Display name (or substring) to search.' },
{ name: 'limit', type: 'int', default: 10, help: 'Max users to return (max 100).' },
],
columns: ['userId', 'displayName', 'reputation', 'goldBadges', 'silverBadges', 'bronzeBadges', 'location', 'createdAt', 'lastAccessAt', 'url'],
func: async (args) => {
const name = requireString(args.name, 'name');
const limit = normalizeLimit(args.limit, 10, 100, 'limit');
const data = await seFetch('/users', {
searchParams: {
inname: name,
order: 'desc',
sort: 'reputation',
pagesize: limit,
},
});
const items = ensureItems(data, 'stackoverflow user');
return items.slice(0, limit).map((u) => ({
userId: u.user_id,
displayName: decodeHtmlEntities(u.display_name || ''),
reputation: u.reputation ?? 0,
goldBadges: u.badge_counts?.gold ?? 0,
silverBadges: u.badge_counts?.silver ?? 0,
bronzeBadges: u.badge_counts?.bronze ?? 0,
location: decodeHtmlEntities(u.location || ''),
createdAt: epochToDate(u.creation_date),
lastAccessAt: epochToDate(u.last_access_date),
url: u.link || (u.user_id ? `https://stackoverflow.com/users/${u.user_id}` : ''),
}));
},
});