@jackwener/opencli 1.7.11 → 1.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.zh-CN.md +2 -1
- package/cli-manifest.json +1417 -24
- package/clis/1688/assets.js +1 -0
- package/clis/1688/download.js +1 -0
- package/clis/1688/item.js +1 -0
- package/clis/1688/search.js +2 -1
- package/clis/1688/store.js +1 -0
- package/clis/36kr/article.js +1 -0
- package/clis/36kr/hot.js +1 -0
- package/clis/36kr/news.js +1 -0
- package/clis/36kr/search.js +1 -0
- package/clis/51job/company.js +1 -0
- package/clis/51job/detail.js +1 -0
- package/clis/51job/hot.js +1 -0
- package/clis/51job/search.js +1 -0
- package/clis/amazon/bestsellers.js +1 -0
- package/clis/amazon/discussion.js +1 -0
- package/clis/amazon/movers-shakers.js +1 -0
- package/clis/amazon/new-releases.js +1 -0
- package/clis/amazon/offer.js +1 -0
- package/clis/amazon/product.js +1 -0
- package/clis/amazon/rankings.js +1 -0
- package/clis/amazon/search.js +1 -0
- package/clis/antigravity/dump.js +1 -0
- package/clis/antigravity/extract-code.js +1 -0
- package/clis/antigravity/model.js +1 -0
- package/clis/antigravity/new.js +1 -0
- package/clis/antigravity/read.js +1 -0
- package/clis/antigravity/send.js +1 -0
- package/clis/antigravity/status.js +1 -0
- package/clis/antigravity/watch.js +1 -0
- package/clis/apple-podcasts/episodes.js +1 -0
- package/clis/apple-podcasts/search.js +1 -0
- package/clis/apple-podcasts/top.js +1 -0
- package/clis/arxiv/arxiv.test.js +112 -0
- package/clis/arxiv/paper.js +4 -3
- package/clis/arxiv/recent.js +33 -0
- package/clis/arxiv/search.js +19 -7
- package/clis/arxiv/utils.js +68 -5
- package/clis/baidu-scholar/search.js +1 -0
- package/clis/band/bands.js +1 -0
- package/clis/band/mentions.js +1 -0
- package/clis/band/post.js +1 -0
- package/clis/band/posts.js +1 -0
- package/clis/barchart/flow.js +1 -0
- package/clis/barchart/greeks.js +1 -0
- package/clis/barchart/options.js +1 -0
- package/clis/barchart/quote.js +1 -0
- package/clis/bbc/news.js +1 -0
- package/clis/bilibili/comments.js +1 -0
- package/clis/bilibili/download.js +1 -0
- package/clis/bilibili/dynamic.js +1 -0
- package/clis/bilibili/favorite.js +1 -0
- package/clis/bilibili/feed.js +2 -0
- package/clis/bilibili/following.js +1 -0
- package/clis/bilibili/history.js +1 -0
- package/clis/bilibili/hot.js +6 -1
- package/clis/bilibili/hot.test.js +17 -0
- package/clis/bilibili/me.js +1 -1
- package/clis/bilibili/ranking.js +1 -0
- package/clis/bilibili/search.js +1 -1
- package/clis/bilibili/subtitle.js +1 -0
- package/clis/bilibili/user-videos.js +1 -0
- package/clis/bilibili/video.js +1 -0
- package/clis/binance/asks.js +1 -0
- package/clis/binance/depth.js +1 -0
- package/clis/binance/gainers.js +1 -0
- package/clis/binance/klines.js +1 -0
- package/clis/binance/losers.js +1 -0
- package/clis/binance/pairs.js +1 -0
- package/clis/binance/price.js +1 -0
- package/clis/binance/prices.js +1 -0
- package/clis/binance/ticker.js +1 -0
- package/clis/binance/top.js +1 -0
- package/clis/binance/trades.js +1 -0
- package/clis/bloomberg/businessweek.js +1 -0
- package/clis/bloomberg/economics.js +1 -0
- package/clis/bloomberg/feeds.js +1 -0
- package/clis/bloomberg/industries.js +1 -0
- package/clis/bloomberg/main.js +1 -0
- package/clis/bloomberg/markets.js +1 -0
- package/clis/bloomberg/news.js +1 -0
- package/clis/bloomberg/opinions.js +1 -0
- package/clis/bloomberg/politics.js +1 -0
- package/clis/bloomberg/tech.js +1 -0
- package/clis/bluesky/feeds.js +1 -0
- package/clis/bluesky/followers.js +1 -0
- package/clis/bluesky/following.js +1 -0
- package/clis/bluesky/profile.js +1 -0
- package/clis/bluesky/search.js +1 -0
- package/clis/bluesky/starter-packs.js +1 -0
- package/clis/bluesky/thread.js +1 -0
- package/clis/bluesky/trending.js +1 -0
- package/clis/bluesky/user.js +3 -1
- package/clis/boss/batchgreet.js +1 -0
- package/clis/boss/chatlist.js +1 -0
- package/clis/boss/chatmsg.js +1 -0
- package/clis/boss/detail.js +1 -0
- package/clis/boss/exchange.js +1 -0
- package/clis/boss/greet.js +1 -0
- package/clis/boss/invite.js +1 -0
- package/clis/boss/joblist.js +1 -0
- package/clis/boss/mark.js +1 -0
- package/clis/boss/recommend.js +1 -0
- package/clis/boss/resume.js +1 -0
- package/clis/boss/search.js +1 -0
- package/clis/boss/send.js +1 -0
- package/clis/boss/stats.js +1 -0
- package/clis/chaoxing/assignments.js +1 -0
- package/clis/chaoxing/exams.js +1 -0
- package/clis/chatgpt/image.js +1 -0
- package/clis/chatgpt-app/ask.js +1 -0
- package/clis/chatgpt-app/model.js +1 -0
- package/clis/chatgpt-app/new.js +1 -0
- package/clis/chatgpt-app/read.js +1 -0
- package/clis/chatgpt-app/send.js +1 -0
- package/clis/chatgpt-app/status.js +1 -0
- package/clis/chatwise/ask.js +1 -0
- package/clis/chatwise/export.js +1 -0
- package/clis/chatwise/history.js +1 -0
- package/clis/chatwise/model.js +1 -0
- package/clis/chatwise/read.js +1 -0
- package/clis/chatwise/send.js +1 -0
- package/clis/claude/ask.js +1 -0
- package/clis/claude/detail.js +1 -0
- package/clis/claude/history.js +1 -0
- package/clis/claude/new.js +1 -0
- package/clis/claude/read.js +1 -0
- package/clis/claude/send.js +1 -0
- package/clis/claude/status.js +1 -0
- package/clis/cnki/search.js +1 -0
- package/clis/codex/ask.js +1 -0
- package/clis/codex/export.js +1 -0
- package/clis/codex/extract-diff.js +1 -0
- package/clis/codex/history.js +1 -0
- package/clis/codex/model.js +1 -0
- package/clis/codex/read.js +1 -0
- package/clis/codex/send.js +1 -0
- package/clis/coupang/add-to-cart.js +1 -0
- package/clis/coupang/search.js +1 -0
- package/clis/ctrip/search.js +1 -0
- package/clis/cursor/ask.js +1 -0
- package/clis/cursor/composer.js +1 -0
- package/clis/cursor/export.js +1 -0
- package/clis/cursor/extract-code.js +1 -0
- package/clis/cursor/history.js +1 -0
- package/clis/cursor/model.js +1 -0
- package/clis/cursor/read.js +1 -0
- package/clis/cursor/send.js +1 -0
- package/clis/dblp/dblp.test.js +397 -0
- package/clis/dblp/paper.js +40 -0
- package/clis/dblp/search.js +45 -0
- package/clis/dblp/utils.js +290 -0
- package/clis/deepseek/ask.js +1 -0
- package/clis/deepseek/history.js +1 -0
- package/clis/deepseek/new.js +1 -0
- package/clis/deepseek/read.js +1 -0
- package/clis/deepseek/status.js +1 -0
- package/clis/devto/devto.test.js +236 -0
- package/clis/devto/read.js +103 -0
- package/clis/devto/tag.js +5 -1
- package/clis/devto/top.js +5 -1
- package/clis/devto/user.js +5 -1
- package/clis/dianping/__fixtures__/search.html +168 -0
- package/clis/dianping/__fixtures__/shop.html +6 -0
- package/clis/dianping/dianping.test.js +424 -0
- package/clis/dianping/search.js +154 -0
- package/clis/dianping/shop.js +173 -0
- package/clis/dianping/utils.js +157 -0
- package/clis/dictionary/examples.js +1 -0
- package/clis/dictionary/search.js +1 -0
- package/clis/dictionary/synonyms.js +1 -0
- package/clis/discord-app/channels.js +1 -0
- package/clis/discord-app/delete.js +1 -0
- package/clis/discord-app/members.js +1 -0
- package/clis/discord-app/read.js +1 -0
- package/clis/discord-app/search.js +1 -0
- package/clis/discord-app/send.js +1 -0
- package/clis/discord-app/servers.js +1 -0
- package/clis/discord-app/status.js +1 -0
- package/clis/douban/book-hot.js +1 -0
- package/clis/douban/download.js +1 -0
- package/clis/douban/marks.js +1 -0
- package/clis/douban/movie-hot.js +2 -1
- package/clis/douban/movie-hot.test.js +14 -0
- package/clis/douban/photos.js +2 -1
- package/clis/douban/reviews.js +1 -0
- package/clis/douban/search.js +1 -0
- package/clis/douban/subject.js +1 -0
- package/clis/douban/top250.js +1 -0
- package/clis/douban/utils.js +11 -13
- package/clis/douban/utils.test.js +79 -0
- package/clis/doubao/ask.js +1 -0
- package/clis/doubao/detail.js +1 -0
- package/clis/doubao/history.js +1 -0
- package/clis/doubao/meeting-summary.js +1 -0
- package/clis/doubao/meeting-transcript.js +1 -0
- package/clis/doubao/new.js +1 -0
- package/clis/doubao/read.js +1 -0
- package/clis/doubao/send.js +1 -0
- package/clis/doubao/status.js +1 -0
- package/clis/doubao-app/ask.js +1 -0
- package/clis/doubao-app/dump.js +1 -0
- package/clis/doubao-app/new.js +1 -0
- package/clis/doubao-app/read.js +1 -0
- package/clis/doubao-app/screenshot.js +1 -0
- package/clis/doubao-app/send.js +1 -0
- package/clis/doubao-app/status.js +1 -0
- package/clis/douyin/activities.js +1 -0
- package/clis/douyin/collections.js +1 -0
- package/clis/douyin/delete.js +1 -0
- package/clis/douyin/draft.js +1 -0
- package/clis/douyin/drafts.js +1 -0
- package/clis/douyin/hashtag.js +1 -0
- package/clis/douyin/location.js +1 -0
- package/clis/douyin/profile.js +1 -0
- package/clis/douyin/publish.js +1 -0
- package/clis/douyin/stats.js +1 -0
- package/clis/douyin/update.js +1 -0
- package/clis/douyin/user-videos.js +1 -0
- package/clis/douyin/videos.js +1 -0
- package/clis/eastmoney/announcement.js +1 -0
- package/clis/eastmoney/convertible.js +1 -0
- package/clis/eastmoney/etf.js +1 -0
- package/clis/eastmoney/holders.js +1 -0
- package/clis/eastmoney/hot-rank.js +1 -0
- package/clis/eastmoney/index-board.js +1 -0
- package/clis/eastmoney/kline.js +1 -0
- package/clis/eastmoney/kuaixun.js +1 -0
- package/clis/eastmoney/longhu.js +1 -0
- package/clis/eastmoney/money-flow.js +1 -0
- package/clis/eastmoney/northbound.js +1 -0
- package/clis/eastmoney/quote.js +1 -0
- package/clis/eastmoney/rank.js +1 -0
- package/clis/eastmoney/sectors.js +1 -0
- package/clis/facebook/add-friend.js +1 -0
- package/clis/facebook/events.js +1 -0
- package/clis/facebook/feed.js +1 -0
- package/clis/facebook/friends.js +1 -0
- package/clis/facebook/groups.js +1 -0
- package/clis/facebook/join-group.js +1 -0
- package/clis/facebook/marketplace-inbox.js +1 -0
- package/clis/facebook/marketplace-listings.js +1 -0
- package/clis/facebook/memories.js +1 -0
- package/clis/facebook/notifications.js +1 -0
- package/clis/facebook/profile.js +1 -0
- package/clis/facebook/search.js +1 -0
- package/clis/gemini/ask.js +1 -0
- package/clis/gemini/deep-research-result.js +1 -0
- package/clis/gemini/deep-research.js +1 -0
- package/clis/gemini/image.js +1 -0
- package/clis/gemini/new.js +1 -0
- package/clis/gitee/search.js +1 -0
- package/clis/gitee/trending.js +1 -0
- package/clis/gitee/user.js +1 -0
- package/clis/google/news.js +1 -0
- package/clis/google/search.js +1 -0
- package/clis/google/suggest.js +1 -0
- package/clis/google/trends.js +1 -0
- package/clis/google-scholar/cite.js +1 -0
- package/clis/google-scholar/profile.js +1 -0
- package/clis/google-scholar/search.js +1 -0
- package/clis/gov-law/recent.js +1 -0
- package/clis/gov-law/search.js +1 -0
- package/clis/gov-policy/recent.js +1 -0
- package/clis/gov-policy/search.js +1 -0
- package/clis/grok/ask.js +1 -0
- package/clis/grok/image.ts +1 -0
- package/clis/hackernews/ask.js +3 -1
- package/clis/hackernews/best.js +3 -1
- package/clis/hackernews/hackernews.test.js +132 -0
- package/clis/hackernews/jobs.js +3 -1
- package/clis/hackernews/new.js +3 -1
- package/clis/hackernews/read.js +188 -0
- package/clis/hackernews/search.js +3 -1
- package/clis/hackernews/show.js +3 -1
- package/clis/hackernews/top.js +3 -1
- package/clis/hackernews/user.js +1 -0
- package/clis/hf/top.js +1 -0
- package/clis/hupu/detail.js +1 -0
- package/clis/hupu/hot.js +3 -1
- package/clis/hupu/like.js +1 -0
- package/clis/hupu/mentions.js +2 -1
- package/clis/hupu/reply.js +1 -0
- package/clis/hupu/search.js +3 -1
- package/clis/hupu/unlike.js +1 -0
- package/clis/imdb/person.js +1 -0
- package/clis/imdb/reviews.js +1 -0
- package/clis/imdb/search.js +1 -0
- package/clis/imdb/title.js +1 -0
- package/clis/imdb/top.js +1 -0
- package/clis/imdb/trending.js +1 -0
- package/clis/indeed/indeed.test.js +375 -0
- package/clis/indeed/job.js +86 -0
- package/clis/indeed/search.js +110 -0
- package/clis/indeed/utils.js +152 -0
- package/clis/instagram/collection-create.js +1 -0
- package/clis/instagram/collection-delete.js +1 -0
- package/clis/instagram/comment.js +1 -0
- package/clis/instagram/download.js +1 -0
- package/clis/instagram/explore.js +1 -0
- package/clis/instagram/follow.js +1 -0
- package/clis/instagram/followers.js +1 -0
- package/clis/instagram/following.js +1 -0
- package/clis/instagram/like.js +1 -0
- package/clis/instagram/note.js +1 -0
- package/clis/instagram/post.js +1 -0
- package/clis/instagram/profile.js +1 -0
- package/clis/instagram/reel.js +1 -0
- package/clis/instagram/save.js +1 -0
- package/clis/instagram/saved.js +1 -0
- package/clis/instagram/search.js +1 -0
- package/clis/instagram/story.js +1 -0
- package/clis/instagram/unfollow.js +1 -0
- package/clis/instagram/unlike.js +1 -0
- package/clis/instagram/unsave.js +1 -0
- package/clis/instagram/user.js +1 -0
- package/clis/jd/add-cart.js +1 -0
- package/clis/jd/cart.js +1 -0
- package/clis/jd/detail.js +1 -0
- package/clis/jd/item.js +1 -0
- package/clis/jd/reviews.js +1 -0
- package/clis/jd/search.js +1 -0
- package/clis/jianyu/detail.js +1 -0
- package/clis/jianyu/search.js +1 -0
- package/clis/jike/comment.js +1 -0
- package/clis/jike/create.js +1 -0
- package/clis/jike/feed.js +3 -1
- package/clis/jike/like.js +1 -0
- package/clis/jike/notifications.js +1 -0
- package/clis/jike/post.js +1 -0
- package/clis/jike/repost.js +1 -0
- package/clis/jike/search.js +3 -1
- package/clis/jike/topic.js +1 -0
- package/clis/jike/user.js +3 -1
- package/clis/jimeng/generate.js +1 -0
- package/clis/jimeng/history.js +1 -0
- package/clis/jimeng/new.js +1 -0
- package/clis/jimeng/workspaces.js +1 -0
- package/clis/ke/chengjiao.js +1 -0
- package/clis/ke/ershoufang.js +1 -0
- package/clis/ke/xiaoqu.js +1 -0
- package/clis/ke/zufang.js +1 -0
- package/clis/lesswrong/comments.js +1 -0
- package/clis/lesswrong/curated.js +1 -0
- package/clis/lesswrong/frontpage.js +1 -0
- package/clis/lesswrong/new.js +1 -0
- package/clis/lesswrong/read.js +1 -0
- package/clis/lesswrong/sequences.js +1 -0
- package/clis/lesswrong/shortform.js +1 -0
- package/clis/lesswrong/tag.js +1 -0
- package/clis/lesswrong/tags.js +1 -0
- package/clis/lesswrong/top-month.js +1 -0
- package/clis/lesswrong/top-week.js +1 -0
- package/clis/lesswrong/top-year.js +1 -0
- package/clis/lesswrong/top.js +1 -0
- package/clis/lesswrong/user-posts.js +1 -0
- package/clis/lesswrong/user.js +1 -0
- package/clis/linkedin/search.js +1 -0
- package/clis/linkedin/timeline.js +1 -0
- package/clis/linux-do/categories.js +1 -0
- package/clis/linux-do/category.js +1 -0
- package/clis/linux-do/feed.js +1 -0
- package/clis/linux-do/hot.js +1 -0
- package/clis/linux-do/latest.js +1 -0
- package/clis/linux-do/search.js +1 -0
- package/clis/linux-do/tags.js +2 -1
- package/clis/linux-do/topic-content.js +1 -0
- package/clis/linux-do/topic.js +1 -0
- package/clis/linux-do/user-posts.js +1 -0
- package/clis/linux-do/user-topics.js +1 -0
- package/clis/lobsters/active.js +4 -1
- package/clis/lobsters/hot.js +4 -1
- package/clis/lobsters/lobsters.test.js +169 -0
- package/clis/lobsters/newest.js +4 -1
- package/clis/lobsters/read.js +196 -0
- package/clis/lobsters/tag.js +4 -1
- package/clis/maimai/search-talents.js +1 -0
- package/clis/medium/feed.js +1 -0
- package/clis/medium/search.js +1 -0
- package/clis/medium/user.js +1 -0
- package/clis/mubu/doc.js +1 -0
- package/clis/mubu/docs.js +1 -0
- package/clis/mubu/notes.js +1 -0
- package/clis/mubu/recent.js +1 -0
- package/clis/mubu/search.js +1 -0
- package/clis/notebooklm/current.js +1 -0
- package/clis/notebooklm/get.js +1 -0
- package/clis/notebooklm/history.js +1 -0
- package/clis/notebooklm/list.js +1 -0
- package/clis/notebooklm/note-list.js +1 -0
- package/clis/notebooklm/notes-get.js +1 -0
- package/clis/notebooklm/open.js +1 -0
- package/clis/notebooklm/source-fulltext.js +1 -0
- package/clis/notebooklm/source-get.js +1 -0
- package/clis/notebooklm/source-guide.js +1 -0
- package/clis/notebooklm/source-list.js +1 -0
- package/clis/notebooklm/status.js +1 -0
- package/clis/notebooklm/summary.js +1 -0
- package/clis/notion/export.js +1 -0
- package/clis/notion/favorites.js +1 -0
- package/clis/notion/new.js +1 -0
- package/clis/notion/read.js +1 -0
- package/clis/notion/search.js +1 -0
- package/clis/notion/sidebar.js +1 -0
- package/clis/notion/status.js +1 -0
- package/clis/notion/write.js +1 -0
- package/clis/nowcoder/companies.js +1 -0
- package/clis/nowcoder/creators.js +1 -0
- package/clis/nowcoder/detail.js +1 -0
- package/clis/nowcoder/experience.js +1 -0
- package/clis/nowcoder/hot.js +1 -0
- package/clis/nowcoder/jobs.js +1 -0
- package/clis/nowcoder/notifications.js +1 -0
- package/clis/nowcoder/papers.js +1 -0
- package/clis/nowcoder/practice.js +1 -0
- package/clis/nowcoder/recommend.js +1 -0
- package/clis/nowcoder/referral.js +1 -0
- package/clis/nowcoder/salary.js +1 -0
- package/clis/nowcoder/search.js +1 -0
- package/clis/nowcoder/suggest.js +1 -0
- package/clis/nowcoder/topics.js +1 -0
- package/clis/nowcoder/trending.js +1 -0
- package/clis/ones/login.js +1 -0
- package/clis/ones/logout.js +1 -0
- package/clis/ones/me.js +1 -0
- package/clis/ones/my-tasks.js +1 -0
- package/clis/ones/task.js +1 -0
- package/clis/ones/tasks.js +1 -0
- package/clis/ones/token-info.js +1 -0
- package/clis/ones/worklog.js +1 -0
- package/clis/openreview/openreview.test.js +345 -0
- package/clis/openreview/paper.js +43 -0
- package/clis/openreview/reviews.js +131 -0
- package/clis/openreview/search.js +46 -0
- package/clis/openreview/utils.js +158 -0
- package/clis/openreview/venue.js +63 -0
- package/clis/paperreview/feedback.js +1 -0
- package/clis/paperreview/review.js +1 -0
- package/clis/paperreview/submit.js +1 -0
- package/clis/pixiv/detail.js +1 -0
- package/clis/pixiv/download.js +1 -0
- package/clis/pixiv/illusts.js +2 -1
- package/clis/pixiv/ranking.js +2 -1
- package/clis/pixiv/search.js +2 -1
- package/clis/pixiv/user.js +2 -0
- package/clis/powerchina/search.js +1 -0
- package/clis/producthunt/browse.js +1 -0
- package/clis/producthunt/hot.js +1 -0
- package/clis/producthunt/posts.js +1 -0
- package/clis/producthunt/today.js +1 -0
- package/clis/quark/ls.js +1 -0
- package/clis/quark/mkdir.js +1 -0
- package/clis/quark/mv.js +1 -0
- package/clis/quark/rename.js +1 -0
- package/clis/quark/rm.js +1 -0
- package/clis/quark/save.js +1 -0
- package/clis/quark/share-tree.js +1 -0
- package/clis/reddit/comment.js +1 -0
- package/clis/reddit/frontpage.js +1 -0
- package/clis/reddit/hot.js +6 -1
- package/clis/reddit/hot.test.js +18 -0
- package/clis/reddit/popular.js +1 -0
- package/clis/reddit/read.js +1 -0
- package/clis/reddit/save.js +1 -0
- package/clis/reddit/saved.js +1 -0
- package/clis/reddit/search.js +1 -0
- package/clis/reddit/subreddit.js +1 -0
- package/clis/reddit/subscribe.js +1 -0
- package/clis/reddit/upvote.js +1 -0
- package/clis/reddit/upvoted.js +1 -0
- package/clis/reddit/user-comments.js +1 -0
- package/clis/reddit/user-posts.js +1 -0
- package/clis/reddit/user.js +1 -0
- package/clis/reuters/search.js +1 -0
- package/clis/sinablog/article.js +1 -0
- package/clis/sinablog/hot.js +1 -0
- package/clis/sinablog/search.js +1 -0
- package/clis/sinablog/user.js +1 -0
- package/clis/sinafinance/news.js +1 -0
- package/clis/sinafinance/rolling-news.js +1 -0
- package/clis/sinafinance/stock-rank.js +1 -0
- package/clis/sinafinance/stock.js +1 -0
- package/clis/smzdm/search.js +1 -0
- package/clis/spotify/spotify.js +11 -0
- package/clis/stackoverflow/bounties.js +11 -3
- package/clis/stackoverflow/hot.js +10 -2
- package/clis/stackoverflow/read.js +314 -0
- package/clis/stackoverflow/search.js +10 -2
- package/clis/stackoverflow/stackoverflow.test.js +346 -0
- package/clis/stackoverflow/unanswered.js +9 -2
- package/clis/steam/top-sellers.js +1 -0
- package/clis/substack/feed.js +1 -0
- package/clis/substack/publication.js +1 -0
- package/clis/substack/search.js +1 -0
- package/clis/taobao/add-cart.js +1 -0
- package/clis/taobao/cart.js +1 -0
- package/clis/taobao/detail.js +1 -0
- package/clis/taobao/reviews.js +1 -0
- package/clis/taobao/search.js +1 -0
- package/clis/tdx/hot-rank.js +1 -0
- package/clis/ths/hot-rank.js +1 -0
- package/clis/tieba/hot.js +2 -1
- package/clis/tieba/posts.js +1 -0
- package/clis/tieba/read.js +1 -0
- package/clis/tieba/search.js +2 -1
- package/clis/tiktok/comment.js +1 -0
- package/clis/tiktok/explore.js +1 -0
- package/clis/tiktok/follow.js +1 -0
- package/clis/tiktok/following.js +1 -0
- package/clis/tiktok/friends.js +1 -0
- package/clis/tiktok/like.js +1 -0
- package/clis/tiktok/live.js +1 -0
- package/clis/tiktok/notifications.js +1 -0
- package/clis/tiktok/profile.js +1 -0
- package/clis/tiktok/save.js +1 -0
- package/clis/tiktok/search.js +1 -0
- package/clis/tiktok/unfollow.js +1 -0
- package/clis/tiktok/unlike.js +1 -0
- package/clis/tiktok/unsave.js +1 -0
- package/clis/tiktok/user.js +1 -0
- package/clis/toutiao/articles.js +1 -0
- package/clis/twitter/accept.js +1 -0
- package/clis/twitter/article.js +1 -0
- package/clis/twitter/block.js +1 -0
- package/clis/twitter/bookmark.js +1 -0
- package/clis/twitter/bookmarks.js +2 -1
- package/clis/twitter/delete.js +1 -0
- package/clis/twitter/download.js +1 -0
- package/clis/twitter/follow.js +1 -0
- package/clis/twitter/followers.js +1 -0
- package/clis/twitter/following.js +1 -0
- package/clis/twitter/hide-reply.js +1 -0
- package/clis/twitter/like.js +1 -0
- package/clis/twitter/likes.js +2 -1
- package/clis/twitter/list-add.js +1 -0
- package/clis/twitter/list-remove.js +1 -0
- package/clis/twitter/list-tweets.js +1 -0
- package/clis/twitter/lists.js +1 -0
- package/clis/twitter/notifications.js +1 -0
- package/clis/twitter/post.js +1 -0
- package/clis/twitter/profile.js +1 -0
- package/clis/twitter/reply-dm.js +1 -0
- package/clis/twitter/reply.js +1 -0
- package/clis/twitter/search.js +1 -0
- package/clis/twitter/thread.js +1 -0
- package/clis/twitter/timeline.js +1 -0
- package/clis/twitter/trending.js +11 -12
- package/clis/twitter/trending.test.js +15 -0
- package/clis/twitter/tweets.js +2 -1
- package/clis/twitter/tweets.test.js +2 -2
- package/clis/twitter/unblock.js +1 -0
- package/clis/twitter/unbookmark.js +1 -0
- package/clis/twitter/unfollow.js +1 -0
- package/clis/uiverse/code.js +1 -0
- package/clis/uiverse/preview.js +1 -0
- package/clis/v2ex/daily.js +1 -0
- package/clis/v2ex/hot.js +1 -0
- package/clis/v2ex/latest.js +1 -0
- package/clis/v2ex/me.js +1 -0
- package/clis/v2ex/member.js +1 -0
- package/clis/v2ex/node.js +1 -0
- package/clis/v2ex/nodes.js +1 -0
- package/clis/v2ex/notifications.js +1 -0
- package/clis/v2ex/replies.js +1 -0
- package/clis/v2ex/topic.js +1 -0
- package/clis/v2ex/user.js +1 -0
- package/clis/wanfang/search.js +1 -0
- package/clis/web/read.js +1 -0
- package/clis/weibo/comments.js +1 -0
- package/clis/weibo/favorites.js +1 -0
- package/clis/weibo/feed.js +3 -1
- package/clis/weibo/hot.js +1 -0
- package/clis/weibo/me.js +1 -0
- package/clis/weibo/post.js +1 -0
- package/clis/weibo/publish.js +1 -0
- package/clis/weibo/search.js +8 -2
- package/clis/weibo/user.js +1 -0
- package/clis/weixin/create-draft.js +1 -0
- package/clis/weixin/download.js +1 -0
- package/clis/weixin/drafts.js +1 -0
- package/clis/weread/ai-outline.js +1 -0
- package/clis/weread/book.js +1 -0
- package/clis/weread/highlights.js +1 -0
- package/clis/weread/notebooks.js +1 -0
- package/clis/weread/notes.js +1 -0
- package/clis/weread/ranking.js +1 -0
- package/clis/weread/search.js +1 -0
- package/clis/weread/shelf.js +1 -0
- package/clis/wikipedia/random.js +1 -0
- package/clis/wikipedia/search.js +1 -0
- package/clis/wikipedia/summary.js +1 -0
- package/clis/wikipedia/trending.js +1 -0
- package/clis/xianyu/chat.js +1 -0
- package/clis/xianyu/item.js +1 -0
- package/clis/xianyu/search.js +1 -0
- package/clis/xiaoe/catalog.js +2 -1
- package/clis/xiaoe/content.js +1 -0
- package/clis/xiaoe/courses.js +1 -0
- package/clis/xiaoe/detail.js +1 -0
- package/clis/xiaoe/play-url.js +1 -0
- package/clis/xiaohongshu/comments.js +1 -0
- package/clis/xiaohongshu/creator-note-detail.js +1 -0
- package/clis/xiaohongshu/creator-notes-summary.js +1 -0
- package/clis/xiaohongshu/creator-notes.js +1 -0
- package/clis/xiaohongshu/creator-profile.js +1 -0
- package/clis/xiaohongshu/creator-stats.js +1 -0
- package/clis/xiaohongshu/download.js +1 -0
- package/clis/xiaohongshu/feed.js +2 -1
- package/clis/xiaohongshu/note.js +1 -0
- package/clis/xiaohongshu/notifications.js +1 -0
- package/clis/xiaohongshu/publish.js +1 -0
- package/clis/xiaohongshu/search.js +1 -0
- package/clis/xiaohongshu/user.js +1 -0
- package/clis/xiaoyuzhou/download.js +1 -0
- package/clis/xiaoyuzhou/episode.js +1 -0
- package/clis/xiaoyuzhou/podcast-episodes.js +1 -0
- package/clis/xiaoyuzhou/podcast.js +1 -0
- package/clis/xiaoyuzhou/transcript.js +1 -0
- package/clis/xueqiu/comments.js +1 -0
- package/clis/xueqiu/earnings-date.js +1 -0
- package/clis/xueqiu/feed.js +1 -0
- package/clis/xueqiu/fund-holdings.js +1 -0
- package/clis/xueqiu/fund-snapshot.js +1 -0
- package/clis/xueqiu/groups.js +1 -0
- package/clis/xueqiu/hot-stock.js +1 -0
- package/clis/xueqiu/hot.js +1 -0
- package/clis/xueqiu/kline.js +1 -0
- package/clis/xueqiu/search.js +1 -0
- package/clis/xueqiu/stock.js +1 -0
- package/clis/xueqiu/watchlist.js +1 -0
- package/clis/yahoo-finance/quote.js +1 -0
- package/clis/yollomi/background.js +1 -0
- package/clis/yollomi/edit.js +1 -0
- package/clis/yollomi/face-swap.js +1 -0
- package/clis/yollomi/generate.js +1 -0
- package/clis/yollomi/models.js +1 -0
- package/clis/yollomi/object-remover.js +1 -0
- package/clis/yollomi/remove-bg.js +1 -0
- package/clis/yollomi/restore.js +1 -0
- package/clis/yollomi/try-on.js +1 -0
- package/clis/yollomi/upload.js +1 -0
- package/clis/yollomi/upscale.js +1 -0
- package/clis/yollomi/video.js +1 -0
- package/clis/youtube/channel.js +1 -0
- package/clis/youtube/comments.js +1 -0
- package/clis/youtube/feed.js +8 -7
- package/clis/youtube/feed.test.js +131 -0
- package/clis/youtube/history.js +1 -0
- package/clis/youtube/like.js +1 -0
- package/clis/youtube/playlist.js +1 -0
- package/clis/youtube/search.js +1 -0
- package/clis/youtube/subscribe.js +1 -0
- package/clis/youtube/subscriptions.js +1 -0
- package/clis/youtube/transcript.js +1 -0
- package/clis/youtube/unlike.js +1 -0
- package/clis/youtube/unsubscribe.js +1 -0
- package/clis/youtube/video.js +1 -0
- package/clis/youtube/watch-later.js +1 -0
- package/clis/yuanbao/ask.js +1 -0
- package/clis/yuanbao/new.js +1 -0
- package/clis/zhihu/answer.js +1 -0
- package/clis/zhihu/collection.js +1 -0
- package/clis/zhihu/collections.js +1 -0
- package/clis/zhihu/comment.js +1 -0
- package/clis/zhihu/download.js +1 -0
- package/clis/zhihu/favorite.js +1 -0
- package/clis/zhihu/follow.js +1 -0
- package/clis/zhihu/hot.js +1 -0
- package/clis/zhihu/like.js +1 -0
- package/clis/zhihu/question.js +1 -0
- package/clis/zhihu/search.js +1 -0
- package/clis/zlibrary/info.js +1 -0
- package/clis/zlibrary/search.js +1 -0
- package/clis/zsxq/dynamics.js +1 -0
- package/clis/zsxq/groups.js +1 -0
- package/clis/zsxq/search.js +1 -0
- package/clis/zsxq/topic.js +1 -0
- package/clis/zsxq/topics.js +1 -0
- package/dist/src/adapter-source.test.js +1 -1
- package/dist/src/browser/analyze.test.js +2 -0
- package/dist/src/browser/base-page.d.ts +9 -0
- package/dist/src/browser/base-page.js +72 -3
- package/dist/src/browser/base-page.test.js +42 -0
- package/dist/src/browser/cdp.js +6 -0
- package/dist/src/browser/cdp.test.js +3 -0
- package/dist/src/browser/page.d.ts +1 -0
- package/dist/src/browser/page.js +6 -0
- package/dist/src/browser/page.test.js +17 -0
- package/dist/src/browser/target-resolver.d.ts +10 -3
- package/dist/src/browser/target-resolver.js +16 -10
- package/dist/src/browser/verify-fixture.d.ts +6 -1
- package/dist/src/browser/verify-fixture.js +87 -0
- package/dist/src/browser/verify-fixture.test.js +44 -1
- package/dist/src/build-manifest.js +3 -0
- package/dist/src/build-manifest.test.js +18 -12
- package/dist/src/capabilityRouting.test.js +1 -1
- package/dist/src/cli.js +141 -5
- package/dist/src/cli.test.js +179 -0
- package/dist/src/commanderAdapter.d.ts +1 -1
- package/dist/src/commanderAdapter.js +17 -7
- package/dist/src/commanderAdapter.test.js +7 -6
- package/dist/src/convention-audit.d.ts +50 -0
- package/dist/src/convention-audit.js +546 -0
- package/dist/src/convention-audit.test.d.ts +1 -0
- package/dist/src/convention-audit.test.js +226 -0
- package/dist/src/discovery.js +2 -0
- package/dist/src/doctor.js +0 -1
- package/dist/src/doctor.test.js +1 -1
- package/dist/src/engine.test.js +10 -10
- package/dist/src/execution.js +1 -1
- package/dist/src/execution.test.js +9 -9
- package/dist/src/help.d.ts +15 -0
- package/dist/src/help.js +149 -0
- package/dist/src/main.js +13 -7
- package/dist/src/manifest-types.d.ts +2 -0
- package/dist/src/plugin.test.js +26 -26
- package/dist/src/registry.d.ts +5 -0
- package/dist/src/registry.js +8 -0
- package/dist/src/registry.test.js +27 -20
- package/dist/src/serialization.d.ts +4 -0
- package/dist/src/serialization.js +27 -1
- package/dist/src/serialization.test.js +24 -2
- package/dist/src/types.d.ts +2 -0
- package/package.json +4 -1
- package/scripts/check-listing-id-pairing.mjs +193 -0
- package/scripts/check-silent-column-drop.mjs +105 -0
- package/scripts/check-typed-error-lint.mjs +118 -0
- package/scripts/silent-column-drop-baseline.json +962 -0
- package/scripts/typed-error-lint-baseline.json +1586 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DBLP adapter utilities.
|
|
3
|
+
*
|
|
4
|
+
* dblp serves a public, unauthenticated API:
|
|
5
|
+
* - Publication search (JSON):
|
|
6
|
+
* `https://dblp.org/search/publ/api?q=<query>&format=json&h=<limit>`
|
|
7
|
+
* - Per-record metadata (XML):
|
|
8
|
+
* `https://dblp.org/rec/<key>.xml`
|
|
9
|
+
*
|
|
10
|
+
* The search response is well-shaped JSON, but the per-record endpoint is
|
|
11
|
+
* XML. We parse it with conservative regexes (same pattern as the arxiv
|
|
12
|
+
* adapter) to avoid pulling in an XML lib for this single endpoint.
|
|
13
|
+
*/
|
|
14
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
15
|
+
|
|
16
|
+
export const DBLP_ORIGIN = 'https://dblp.org';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* dblp record keys look like `<type>/<venue>/<short>` (e.g.
|
|
20
|
+
* `conf/nips/VaswaniSPUJGKP17`, `journals/corr/abs-2509-05821`,
|
|
21
|
+
* `phd/Smith20`). Allow lowercase letter prefixes plus 1+ slash-segments
|
|
22
|
+
* containing letters / digits / `_` / `.` / `-`.
|
|
23
|
+
*/
|
|
24
|
+
const KEY_PATTERN = /^[a-z]+(?:\/[A-Za-z0-9_.-]+)+$/;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Wraps `fetch` with typed errors. We always set a UA per dblp's
|
|
28
|
+
* polite-fetch guidance (https://dblp.org/faq/How+to+use+the+dblp+search+API.html).
|
|
29
|
+
*/
|
|
30
|
+
async function dblpFetch(url, label, accept) {
|
|
31
|
+
let res;
|
|
32
|
+
try {
|
|
33
|
+
res = await fetch(url, {
|
|
34
|
+
headers: {
|
|
35
|
+
accept,
|
|
36
|
+
'user-agent': 'opencli-dblp/1.0 (+https://github.com/jackwener/opencli)',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
throw new CommandExecutionError(`${label} request failed: ${err?.message ?? err}`, 'Check that dblp.org is reachable from this network.');
|
|
42
|
+
}
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
if (res.status === 429) {
|
|
45
|
+
throw new CommandExecutionError(`${label} returned HTTP 429 (rate limited)`, 'dblp throttles clients that fetch too aggressively. Wait a few seconds and retry, or lower --limit.');
|
|
46
|
+
}
|
|
47
|
+
if (res.status === 404) {
|
|
48
|
+
throw new EmptyResultError(label, 'dblp returned 404 — the requested record may not exist.');
|
|
49
|
+
}
|
|
50
|
+
throw new CommandExecutionError(`${label} returned HTTP ${res.status}`, 'Inspect the response in a browser at the same URL for more context.');
|
|
51
|
+
}
|
|
52
|
+
return res;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function dblpFetchJson(path, label) {
|
|
56
|
+
const res = await dblpFetch(`${DBLP_ORIGIN}${path}`, label, 'application/json');
|
|
57
|
+
let body;
|
|
58
|
+
try {
|
|
59
|
+
body = await res.json();
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
|
|
63
|
+
}
|
|
64
|
+
const statusCode = String(body?.result?.status?.['@code'] ?? '').trim();
|
|
65
|
+
if (!statusCode) {
|
|
66
|
+
throw new CommandExecutionError(
|
|
67
|
+
`${label} returned JSON without result.status.@code`,
|
|
68
|
+
'dblp changed its JSON envelope or returned a partial error payload; inspect the raw response in a browser.',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (statusCode !== '200') {
|
|
72
|
+
const statusText = String(body?.result?.status?.text ?? '').trim();
|
|
73
|
+
throw new CommandExecutionError(
|
|
74
|
+
`${label} returned API status ${statusCode}${statusText ? ` (${statusText})` : ''}`,
|
|
75
|
+
'dblp accepted the HTTP request but reported an API-level failure. Retry later or inspect the same query in a browser.',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return body;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function dblpFetchXml(path, label) {
|
|
82
|
+
const res = await dblpFetch(`${DBLP_ORIGIN}${path}`, label, 'application/xml');
|
|
83
|
+
return res.text();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function coerceInt(value) {
|
|
87
|
+
if (value === undefined || value === null || value === '') return NaN;
|
|
88
|
+
const n = typeof value === 'number' ? value : Number(value);
|
|
89
|
+
return Number.isFinite(n) && Number.isInteger(n) ? n : NaN;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function requireBoundedInt(value, defaultValue, maxValue, label = 'limit') {
|
|
93
|
+
const raw = value ?? defaultValue;
|
|
94
|
+
const n = coerceInt(raw);
|
|
95
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
96
|
+
throw new ArgumentError(`dblp ${label} must be a positive integer`);
|
|
97
|
+
}
|
|
98
|
+
if (n > maxValue) {
|
|
99
|
+
throw new ArgumentError(`dblp ${label} must be <= ${maxValue}`);
|
|
100
|
+
}
|
|
101
|
+
return n;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function requireQuery(value, label = 'query') {
|
|
105
|
+
const q = String(value ?? '').trim();
|
|
106
|
+
if (!q) {
|
|
107
|
+
throw new ArgumentError(`dblp ${label} cannot be empty`);
|
|
108
|
+
}
|
|
109
|
+
return q;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function requireRecordKey(value) {
|
|
113
|
+
const key = String(value ?? '').trim();
|
|
114
|
+
if (!key) {
|
|
115
|
+
throw new ArgumentError('dblp paper key is required');
|
|
116
|
+
}
|
|
117
|
+
if (!KEY_PATTERN.test(key)) {
|
|
118
|
+
throw new ArgumentError(`dblp paper key "${value}" is not a valid record key`, 'Expected something like "conf/nips/VaswaniSPUJGKP17" — copy the `key` column from `dblp search`.');
|
|
119
|
+
}
|
|
120
|
+
return key;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Decode the small set of XML entities dblp emits in record bodies. */
|
|
124
|
+
export function decodeXmlEntities(text) {
|
|
125
|
+
if (!text) return '';
|
|
126
|
+
return String(text)
|
|
127
|
+
.replace(/&/g, '&')
|
|
128
|
+
.replace(/</g, '<')
|
|
129
|
+
.replace(/>/g, '>')
|
|
130
|
+
.replace(/'/g, "'")
|
|
131
|
+
.replace(/"/g, '"')
|
|
132
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16)))
|
|
133
|
+
.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(parseInt(d, 10)));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Strip dblp's per-author homonym suffixes (`Smith 0001`) → `Smith`. */
|
|
137
|
+
function trimAuthorHomonym(name) {
|
|
138
|
+
return String(name || '').replace(/\s+\d{4,}$/, '').trim();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Normalize the `info.authors.author` field from a dblp search hit. dblp
|
|
143
|
+
* collapses single authors into one object instead of a 1-element array.
|
|
144
|
+
*/
|
|
145
|
+
export function normalizeAuthors(authorsField) {
|
|
146
|
+
if (!authorsField) return [];
|
|
147
|
+
const raw = authorsField?.author;
|
|
148
|
+
if (!raw) return [];
|
|
149
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
150
|
+
return list.map((a) => {
|
|
151
|
+
if (a && typeof a === 'object') return trimAuthorHomonym(a.text || a['#text'] || '');
|
|
152
|
+
return trimAuthorHomonym(String(a));
|
|
153
|
+
}).filter(Boolean);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Project a dblp publication search hit into one row. Drops only the most
|
|
158
|
+
* volatile fields (e.g. abstract, which dblp does not expose) and keeps
|
|
159
|
+
* the canonical key as the round-trip handle.
|
|
160
|
+
*/
|
|
161
|
+
export function searchHitToRow(hit, rank) {
|
|
162
|
+
const info = hit?.info ?? {};
|
|
163
|
+
const authors = normalizeAuthors(info.authors);
|
|
164
|
+
const key = String(info.key ?? '').trim();
|
|
165
|
+
return {
|
|
166
|
+
rank,
|
|
167
|
+
key,
|
|
168
|
+
title: stripTrailingDot(decodeXmlEntities(info.title ?? '')).trim(),
|
|
169
|
+
authors: authors.join(', '),
|
|
170
|
+
venue: decodeXmlEntities(info.venue ?? ''),
|
|
171
|
+
year: String(info.year ?? '').trim(),
|
|
172
|
+
type: simplifyHitType(info.type),
|
|
173
|
+
doi: String(info.doi ?? '').trim(),
|
|
174
|
+
url: String(info.ee ?? info.url ?? '').trim(),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Title strings from dblp end with a period — strip it for cleaner display. */
|
|
179
|
+
function stripTrailingDot(s) {
|
|
180
|
+
return String(s || '').replace(/\.\s*$/, '');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Compress dblp's verbose `type` strings into single-token tags. */
|
|
184
|
+
function simplifyHitType(type) {
|
|
185
|
+
if (!type) return '';
|
|
186
|
+
const t = String(type);
|
|
187
|
+
if (/Conference and Workshop/i.test(t)) return 'conf';
|
|
188
|
+
if (/Journal Articles/i.test(t)) return 'journal';
|
|
189
|
+
if (/Books and Theses/i.test(t)) return 'book';
|
|
190
|
+
if (/Editorship/i.test(t)) return 'editorship';
|
|
191
|
+
if (/Reference Works/i.test(t)) return 'reference';
|
|
192
|
+
if (/Informal/i.test(t)) return 'preprint';
|
|
193
|
+
return t.toLowerCase().split(/\s+/)[0];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Extract a single tag from a dblp record XML blob (regex-based, same
|
|
198
|
+
* approach as arxiv/utils.js — keeps the dependency surface flat).
|
|
199
|
+
*/
|
|
200
|
+
export function extractFirst(xml, tag) {
|
|
201
|
+
const m = String(xml || '').match(new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)<\\/${tag}>`));
|
|
202
|
+
return m ? m[1] : '';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function extractAll(xml, tag) {
|
|
206
|
+
const out = [];
|
|
207
|
+
const re = new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'g');
|
|
208
|
+
let m;
|
|
209
|
+
while ((m = re.exec(String(xml || ''))) !== null) out.push(m[1]);
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Pick the first `<ee type="oa">` (open-access link) if present, otherwise
|
|
215
|
+
* fall back to the first `<ee>`.
|
|
216
|
+
*/
|
|
217
|
+
export function extractOpenAccessLink(xml) {
|
|
218
|
+
const oa = String(xml || '').match(/<ee\b[^>]*type=["']oa["'][^>]*>([\s\S]*?)<\/ee>/);
|
|
219
|
+
if (oa) return oa[1].trim();
|
|
220
|
+
const any = String(xml || '').match(/<ee\b[^>]*>([\s\S]*?)<\/ee>/);
|
|
221
|
+
return any ? any[1].trim() : '';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Pull the `key` attribute off the wrapper element regardless of record type. */
|
|
225
|
+
export function extractRecordKey(xml) {
|
|
226
|
+
const m = String(xml || '').match(/<(?:article|inproceedings|incollection|proceedings|book|phdthesis|mastersthesis)\b[^>]*\bkey="([^"]+)"/);
|
|
227
|
+
return m ? m[1] : '';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Identify the record type from the wrapper element name. Maps to the
|
|
232
|
+
* same simplified tag set the search rows use.
|
|
233
|
+
*/
|
|
234
|
+
export function extractRecordType(xml) {
|
|
235
|
+
const m = String(xml || '').match(/<(article|inproceedings|incollection|proceedings|book|phdthesis|mastersthesis)\b/);
|
|
236
|
+
if (!m) return '';
|
|
237
|
+
switch (m[1]) {
|
|
238
|
+
case 'inproceedings': return 'conf';
|
|
239
|
+
case 'article': return 'journal';
|
|
240
|
+
case 'incollection': return 'incollection';
|
|
241
|
+
case 'proceedings': return 'editorship';
|
|
242
|
+
case 'book': return 'book';
|
|
243
|
+
case 'phdthesis': return 'phdthesis';
|
|
244
|
+
case 'mastersthesis': return 'mastersthesis';
|
|
245
|
+
default: return m[1];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Parse a single dblp record XML into the row shape used by `dblp paper`.
|
|
251
|
+
* Treats every field as optional; throws upstream when the record body
|
|
252
|
+
* is missing entirely (404 already handled at fetch level).
|
|
253
|
+
*/
|
|
254
|
+
export function recordXmlToRow(xml) {
|
|
255
|
+
const key = extractRecordKey(xml);
|
|
256
|
+
const type = extractRecordType(xml);
|
|
257
|
+
const title = stripTrailingDot(decodeXmlEntities(extractFirst(xml, 'title')));
|
|
258
|
+
const authors = extractAll(xml, 'author').map((a) => trimAuthorHomonym(decodeXmlEntities(a)));
|
|
259
|
+
const year = decodeXmlEntities(extractFirst(xml, 'year'));
|
|
260
|
+
const pages = decodeXmlEntities(extractFirst(xml, 'pages'));
|
|
261
|
+
const venueRaw = type === 'conf'
|
|
262
|
+
? decodeXmlEntities(extractFirst(xml, 'booktitle'))
|
|
263
|
+
: decodeXmlEntities(extractFirst(xml, 'journal'));
|
|
264
|
+
const doi = (() => {
|
|
265
|
+
const m = String(xml || '').match(/<ee\b[^>]*>([^<]*?(?:doi\.org\/|10\.[^"<]+))<\/ee>/i);
|
|
266
|
+
if (!m) return '';
|
|
267
|
+
const v = m[1].replace(/^https?:\/\/(?:dx\.)?doi\.org\//i, '');
|
|
268
|
+
return v.startsWith('10.') ? v : '';
|
|
269
|
+
})();
|
|
270
|
+
return {
|
|
271
|
+
key,
|
|
272
|
+
type,
|
|
273
|
+
title,
|
|
274
|
+
authors: authors.join(', '),
|
|
275
|
+
venue: venueRaw,
|
|
276
|
+
year,
|
|
277
|
+
pages,
|
|
278
|
+
doi,
|
|
279
|
+
open_access_url: extractOpenAccessLink(xml),
|
|
280
|
+
dblp_url: key ? `${DBLP_ORIGIN}/rec/${key}.html` : '',
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export const SEARCH_COLUMNS = [
|
|
285
|
+
'rank', 'key', 'title', 'authors', 'venue', 'year', 'type', 'doi', 'url',
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
export const PAPER_COLUMNS = [
|
|
289
|
+
'key', 'type', 'title', 'authors', 'venue', 'year', 'pages', 'doi', 'open_access_url', 'dblp_url',
|
|
290
|
+
];
|
package/clis/deepseek/ask.js
CHANGED
package/clis/deepseek/history.js
CHANGED
|
@@ -4,6 +4,7 @@ import { DEEPSEEK_DOMAIN, getConversationList } from './utils.js';
|
|
|
4
4
|
export const historyCommand = cli({
|
|
5
5
|
site: 'deepseek',
|
|
6
6
|
name: 'history',
|
|
7
|
+
access: 'read',
|
|
7
8
|
description: 'List conversation history from DeepSeek sidebar',
|
|
8
9
|
domain: DEEPSEEK_DOMAIN,
|
|
9
10
|
strategy: Strategy.COOKIE,
|
package/clis/deepseek/new.js
CHANGED
package/clis/deepseek/read.js
CHANGED
|
@@ -4,6 +4,7 @@ import { DEEPSEEK_DOMAIN, ensureOnDeepSeek, getVisibleMessages } from './utils.j
|
|
|
4
4
|
export const readCommand = cli({
|
|
5
5
|
site: 'deepseek',
|
|
6
6
|
name: 'read',
|
|
7
|
+
access: 'read',
|
|
7
8
|
description: 'Read the current DeepSeek conversation',
|
|
8
9
|
domain: DEEPSEEK_DOMAIN,
|
|
9
10
|
strategy: Strategy.COOKIE,
|
package/clis/deepseek/status.js
CHANGED
|
@@ -4,6 +4,7 @@ import { DEEPSEEK_DOMAIN, ensureOnDeepSeek, getPageState } from './utils.js';
|
|
|
4
4
|
export const statusCommand = cli({
|
|
5
5
|
site: 'deepseek',
|
|
6
6
|
name: 'status',
|
|
7
|
+
access: 'read',
|
|
7
8
|
description: 'Check DeepSeek page availability and login state',
|
|
8
9
|
domain: DEEPSEEK_DOMAIN,
|
|
9
10
|
strategy: Strategy.COOKIE,
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
4
|
+
import './top.js';
|
|
5
|
+
import './tag.js';
|
|
6
|
+
import './user.js';
|
|
7
|
+
import './read.js';
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.unstubAllGlobals();
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('devto listing adapters surface id + reading_time + published_at', () => {
|
|
15
|
+
it('devto/top has the agent-native column shape and pipeline mapping', () => {
|
|
16
|
+
const cmd = getRegistry().get('devto/top');
|
|
17
|
+
expect(cmd?.columns).toEqual([
|
|
18
|
+
'rank', 'id', 'title', 'author', 'reactions', 'comments',
|
|
19
|
+
'reading_time', 'published_at', 'tags', 'url',
|
|
20
|
+
]);
|
|
21
|
+
const mapStep = cmd?.pipeline?.find((step) => step.map);
|
|
22
|
+
expect(mapStep?.map).toMatchObject({
|
|
23
|
+
id: '${{ item.id }}',
|
|
24
|
+
reading_time: '${{ item.reading_time_minutes }}',
|
|
25
|
+
published_at: '${{ item.published_at }}',
|
|
26
|
+
url: '${{ item.url }}',
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('devto/tag has the agent-native column shape and pipeline mapping', () => {
|
|
31
|
+
const cmd = getRegistry().get('devto/tag');
|
|
32
|
+
expect(cmd?.columns).toEqual([
|
|
33
|
+
'rank', 'id', 'title', 'author', 'reactions', 'comments',
|
|
34
|
+
'reading_time', 'published_at', 'tags', 'url',
|
|
35
|
+
]);
|
|
36
|
+
const mapStep = cmd?.pipeline?.find((step) => step.map);
|
|
37
|
+
expect(mapStep?.map).toMatchObject({
|
|
38
|
+
id: '${{ item.id }}',
|
|
39
|
+
reading_time: '${{ item.reading_time_minutes }}',
|
|
40
|
+
published_at: '${{ item.published_at }}',
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('devto/user has the agent-native column shape (no author column, since user-specific)', () => {
|
|
45
|
+
const cmd = getRegistry().get('devto/user');
|
|
46
|
+
expect(cmd?.columns).toEqual([
|
|
47
|
+
'rank', 'id', 'title', 'reactions', 'comments',
|
|
48
|
+
'reading_time', 'published_at', 'tags', 'url',
|
|
49
|
+
]);
|
|
50
|
+
const mapStep = cmd?.pipeline?.find((step) => step.map);
|
|
51
|
+
expect(mapStep?.map).toMatchObject({
|
|
52
|
+
id: '${{ item.id }}',
|
|
53
|
+
reading_time: '${{ item.reading_time_minutes }}',
|
|
54
|
+
published_at: '${{ item.published_at }}',
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('devto/read adapter', () => {
|
|
60
|
+
const cmd = getRegistry().get('devto/read');
|
|
61
|
+
|
|
62
|
+
it('registers the article-detail row shape', () => {
|
|
63
|
+
expect(cmd?.columns).toEqual([
|
|
64
|
+
'id', 'title', 'author', 'reactions', 'reading_time',
|
|
65
|
+
'tags', 'published_at', 'body', 'url',
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('takes a positional id plus a tunable max-length', () => {
|
|
70
|
+
const argNames = (cmd?.args || []).map((a) => a.name);
|
|
71
|
+
expect(argNames).toEqual(['id', 'max-length']);
|
|
72
|
+
const idArg = cmd?.args?.find((a) => a.name === 'id');
|
|
73
|
+
expect(idArg?.required).toBe(true);
|
|
74
|
+
expect(idArg?.positional).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('uses the public dev.to JSON endpoint (no browser, public strategy)', () => {
|
|
78
|
+
expect(cmd?.browser).toBe(false);
|
|
79
|
+
expect(cmd?.strategy).toBe('public');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('fails fast with ArgumentError for non-numeric id before fetching', async () => {
|
|
83
|
+
const fetchMock = vi.fn();
|
|
84
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
85
|
+
|
|
86
|
+
await expect(cmd.func({ id: 'not-a-number', 'max-length': 20000 }))
|
|
87
|
+
.rejects.toThrow(ArgumentError);
|
|
88
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('fails fast with ArgumentError for max-length below 100 before fetching', async () => {
|
|
92
|
+
const fetchMock = vi.fn();
|
|
93
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
94
|
+
|
|
95
|
+
await expect(cmd.func({ id: '12345', 'max-length': 50 }))
|
|
96
|
+
.rejects.toThrow(ArgumentError);
|
|
97
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('accepts numeric max-length strings on the direct func path', async () => {
|
|
101
|
+
const article = {
|
|
102
|
+
id: 1,
|
|
103
|
+
title: 't',
|
|
104
|
+
user: { username: 'u' },
|
|
105
|
+
public_reactions_count: 0,
|
|
106
|
+
reading_time_minutes: 1,
|
|
107
|
+
tag_list: [],
|
|
108
|
+
published_at: '',
|
|
109
|
+
body_markdown: 'x'.repeat(150),
|
|
110
|
+
url: 'https://dev.to/u/t-1',
|
|
111
|
+
};
|
|
112
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify(article), { status: 200 })));
|
|
113
|
+
|
|
114
|
+
const rows = await cmd.func({ id: '1', 'max-length': '100' });
|
|
115
|
+
expect(rows[0].body).toBe('x'.repeat(100) + '\n\n... [truncated]');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('fails fast with ArgumentError for invalid max-length strings before fetching', async () => {
|
|
119
|
+
const fetchMock = vi.fn();
|
|
120
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
121
|
+
|
|
122
|
+
await expect(cmd.func({ id: '12345', 'max-length': 'abc' }))
|
|
123
|
+
.rejects.toThrow(ArgumentError);
|
|
124
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('fails fast with EmptyResultError on 404', async () => {
|
|
128
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('Not found', { status: 404 })));
|
|
129
|
+
|
|
130
|
+
await expect(cmd.func({ id: '99999999', 'max-length': 20000 }))
|
|
131
|
+
.rejects.toThrow(EmptyResultError);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('fails fast with CommandExecutionError on non-404 HTTP failures', async () => {
|
|
135
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('Server error', { status: 500 })));
|
|
136
|
+
|
|
137
|
+
await expect(cmd.func({ id: '12345', 'max-length': 20000 }))
|
|
138
|
+
.rejects.toThrow(CommandExecutionError);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('fails fast with CommandExecutionError on invalid JSON responses', async () => {
|
|
142
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('not json', { status: 200 })));
|
|
143
|
+
|
|
144
|
+
await expect(cmd.func({ id: '12345', 'max-length': 20000 }))
|
|
145
|
+
.rejects.toThrow(CommandExecutionError);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('fails fast when the full article body is missing instead of returning a summary', async () => {
|
|
149
|
+
const article = {
|
|
150
|
+
id: 1,
|
|
151
|
+
title: 't',
|
|
152
|
+
user: { username: 'u' },
|
|
153
|
+
public_reactions_count: 0,
|
|
154
|
+
reading_time_minutes: 1,
|
|
155
|
+
tag_list: [],
|
|
156
|
+
published_at: '',
|
|
157
|
+
description: 'summary only',
|
|
158
|
+
url: 'https://dev.to/u/t-1',
|
|
159
|
+
};
|
|
160
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify(article), { status: 200 })));
|
|
161
|
+
|
|
162
|
+
await expect(cmd.func({ id: '1', 'max-length': 20000 }))
|
|
163
|
+
.rejects.toThrow(CommandExecutionError);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('returns a single article row with body_markdown extracted', async () => {
|
|
167
|
+
// Real /api/articles/<id> returns tag_list as a comma string and tags as an array.
|
|
168
|
+
const article = {
|
|
169
|
+
id: 3605688,
|
|
170
|
+
title: 'How to do thing X in Rust',
|
|
171
|
+
user: { username: 'jdoe' },
|
|
172
|
+
public_reactions_count: 42,
|
|
173
|
+
reading_time_minutes: 7,
|
|
174
|
+
tag_list: 'rust, webdev',
|
|
175
|
+
tags: ['rust', 'webdev'],
|
|
176
|
+
published_at: '2026-05-01T00:00:00Z',
|
|
177
|
+
body_markdown: '# Hello\n\nThis is the article body.',
|
|
178
|
+
url: 'https://dev.to/jdoe/how-to-do-thing-x-in-rust-1234',
|
|
179
|
+
};
|
|
180
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify(article), { status: 200 })));
|
|
181
|
+
|
|
182
|
+
const rows = await cmd.func({ id: '3605688', 'max-length': 20000 });
|
|
183
|
+
|
|
184
|
+
expect(rows).toEqual([
|
|
185
|
+
{
|
|
186
|
+
id: 3605688,
|
|
187
|
+
title: 'How to do thing X in Rust',
|
|
188
|
+
author: 'jdoe',
|
|
189
|
+
reactions: 42,
|
|
190
|
+
reading_time: 7,
|
|
191
|
+
tags: 'rust, webdev',
|
|
192
|
+
published_at: '2026-05-01T00:00:00Z',
|
|
193
|
+
body: '# Hello\n\nThis is the article body.',
|
|
194
|
+
url: 'https://dev.to/jdoe/how-to-do-thing-x-in-rust-1234',
|
|
195
|
+
},
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('handles the alternate shape where tag_list is an array (defensive)', async () => {
|
|
200
|
+
const article = {
|
|
201
|
+
id: 1,
|
|
202
|
+
title: 't',
|
|
203
|
+
user: { username: 'u' },
|
|
204
|
+
public_reactions_count: 0,
|
|
205
|
+
reading_time_minutes: 1,
|
|
206
|
+
tag_list: ['javascript', 'webdev'],
|
|
207
|
+
published_at: '',
|
|
208
|
+
body_markdown: 'body',
|
|
209
|
+
url: 'https://dev.to/u/t-1',
|
|
210
|
+
};
|
|
211
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify(article), { status: 200 })));
|
|
212
|
+
|
|
213
|
+
const rows = await cmd.func({ id: '1', 'max-length': 20000 });
|
|
214
|
+
expect(rows[0].tags).toBe('javascript, webdev');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('truncates body when over max-length and appends a marker', async () => {
|
|
218
|
+
const longBody = 'x'.repeat(500);
|
|
219
|
+
const article = {
|
|
220
|
+
id: 1,
|
|
221
|
+
title: 't',
|
|
222
|
+
user: { username: 'u' },
|
|
223
|
+
public_reactions_count: 0,
|
|
224
|
+
reading_time_minutes: 1,
|
|
225
|
+
tag_list: [],
|
|
226
|
+
published_at: '',
|
|
227
|
+
body_markdown: longBody,
|
|
228
|
+
url: 'https://dev.to/u/t-1',
|
|
229
|
+
};
|
|
230
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify(article), { status: 200 })));
|
|
231
|
+
|
|
232
|
+
const rows = await cmd.func({ id: '1', 'max-length': 100 });
|
|
233
|
+
|
|
234
|
+
expect(rows[0].body).toBe('x'.repeat(100) + '\n\n... [truncated]');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DEV.to article reader.
|
|
3
|
+
*
|
|
4
|
+
* Public API: https://dev.to/api/articles/<id>
|
|
5
|
+
* Returns the full article including `body_markdown` (and `body_html`).
|
|
6
|
+
*
|
|
7
|
+
* The DEV.to API does not currently expose article comments — this reader
|
|
8
|
+
* therefore emits one row with the article body. If/when comments become
|
|
9
|
+
* available we can extend to a POST + L0/L1 shape like `hackernews read`.
|
|
10
|
+
*/
|
|
11
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
12
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
13
|
+
|
|
14
|
+
const DEVTO_ARTICLE_BASE = 'https://dev.to/api/articles';
|
|
15
|
+
|
|
16
|
+
async function fetchArticle(id) {
|
|
17
|
+
let res;
|
|
18
|
+
try {
|
|
19
|
+
res = await fetch(`${DEVTO_ARTICLE_BASE}/${id}`);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
22
|
+
throw new CommandExecutionError(`DEV.to API request failed for article ${id}`, detail);
|
|
23
|
+
}
|
|
24
|
+
if (res.status === 404) {
|
|
25
|
+
throw new EmptyResultError(`devto/${id}`, 'Article not found');
|
|
26
|
+
}
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
throw new CommandExecutionError(`DEV.to API HTTP ${res.status} for article ${id}`, 'Check the article id');
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return await res.json();
|
|
32
|
+
} catch {
|
|
33
|
+
throw new CommandExecutionError(`DEV.to API returned invalid JSON for article ${id}`, 'Retry later or open the article URL directly');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function requireMinInt(value, min, label) {
|
|
38
|
+
const number = typeof value === 'number' ? value : Number(value);
|
|
39
|
+
if (!Number.isInteger(number) || number < min) {
|
|
40
|
+
throw new ArgumentError(`${label} must be an integer >= ${min}`);
|
|
41
|
+
}
|
|
42
|
+
return number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function requireArticleBody(article, id) {
|
|
46
|
+
if (typeof article.body_markdown === 'string' && article.body_markdown.trim()) {
|
|
47
|
+
return article.body_markdown;
|
|
48
|
+
}
|
|
49
|
+
throw new CommandExecutionError(
|
|
50
|
+
`DEV.to article ${id} did not include body_markdown`,
|
|
51
|
+
'DEV.to API response shape may have changed. Open the article URL directly or retry later.',
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
cli({
|
|
56
|
+
site: 'devto',
|
|
57
|
+
name: 'read',
|
|
58
|
+
access: 'read',
|
|
59
|
+
description: 'Read a DEV.to article body by id',
|
|
60
|
+
domain: 'dev.to',
|
|
61
|
+
strategy: Strategy.PUBLIC,
|
|
62
|
+
browser: false,
|
|
63
|
+
args: [
|
|
64
|
+
{ name: 'id', required: true, positional: true, help: 'DEV.to article id (numeric, e.g. 3605688)' },
|
|
65
|
+
{ name: 'max-length', type: 'int', default: 20000, help: 'Max characters of body to return (min 100)' },
|
|
66
|
+
],
|
|
67
|
+
columns: ['id', 'title', 'author', 'reactions', 'reading_time', 'tags', 'published_at', 'body', 'url'],
|
|
68
|
+
func: async (args) => {
|
|
69
|
+
const id = String(args.id || '').trim();
|
|
70
|
+
if (!/^\d+$/.test(id)) {
|
|
71
|
+
throw new ArgumentError(`Invalid DEV.to article id: ${args.id}`, 'Pass a numeric id like 3605688');
|
|
72
|
+
}
|
|
73
|
+
const maxLength = requireMinInt(args['max-length'] ?? 20000, 100, 'devto read --max-length');
|
|
74
|
+
|
|
75
|
+
const article = await fetchArticle(id);
|
|
76
|
+
if (!article || !article.id) {
|
|
77
|
+
throw new EmptyResultError(`devto/${id}`, 'Article not found');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const body = requireArticleBody(article, id);
|
|
81
|
+
const truncated = body.length > maxLength
|
|
82
|
+
? body.slice(0, maxLength) + '\n\n... [truncated]'
|
|
83
|
+
: body;
|
|
84
|
+
|
|
85
|
+
// The single-article endpoint returns `tag_list` as a comma-separated
|
|
86
|
+
// string and `tags` as an array — the opposite of the listing endpoints.
|
|
87
|
+
// Normalize either shape into a single comma-separated string.
|
|
88
|
+
const tagsRaw = article.tag_list ?? article.tags ?? '';
|
|
89
|
+
const tags = Array.isArray(tagsRaw) ? tagsRaw.join(', ') : String(tagsRaw);
|
|
90
|
+
|
|
91
|
+
return [{
|
|
92
|
+
id: article.id,
|
|
93
|
+
title: article.title || '',
|
|
94
|
+
author: article.user?.username || '[deleted]',
|
|
95
|
+
reactions: article.public_reactions_count ?? 0,
|
|
96
|
+
reading_time: article.reading_time_minutes ?? 0,
|
|
97
|
+
tags,
|
|
98
|
+
published_at: article.published_at || '',
|
|
99
|
+
body: truncated,
|
|
100
|
+
url: article.url || '',
|
|
101
|
+
}];
|
|
102
|
+
},
|
|
103
|
+
});
|