@jackwener/opencli 1.7.8 → 1.7.10

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.
Files changed (281) hide show
  1. package/README.md +49 -14
  2. package/README.zh-CN.md +30 -10
  3. package/cli-manifest.json +646 -30
  4. package/clis/36kr/news.js +1 -1
  5. package/clis/apple-podcasts/commands.test.js +4 -4
  6. package/clis/apple-podcasts/episodes.js +1 -1
  7. package/clis/apple-podcasts/search.js +1 -1
  8. package/clis/apple-podcasts/top.js +1 -1
  9. package/clis/arxiv/paper.js +1 -1
  10. package/clis/arxiv/search.js +1 -1
  11. package/clis/band/mentions.js +3 -3
  12. package/clis/bbc/news.js +1 -1
  13. package/clis/bilibili/subtitle.js +2 -2
  14. package/clis/bloomberg/businessweek.js +1 -1
  15. package/clis/bloomberg/economics.js +1 -1
  16. package/clis/bloomberg/industries.js +1 -1
  17. package/clis/bloomberg/main.js +1 -1
  18. package/clis/bloomberg/markets.js +1 -1
  19. package/clis/bloomberg/opinions.js +1 -1
  20. package/clis/bloomberg/politics.js +1 -1
  21. package/clis/bloomberg/tech.js +1 -1
  22. package/clis/boss/search.js +49 -8
  23. package/clis/boss/search.test.js +78 -0
  24. package/clis/boss/send.js +3 -3
  25. package/clis/chatgpt/image.js +37 -8
  26. package/clis/chatgpt/image.test.js +92 -0
  27. package/clis/chatgpt/utils.js +39 -6
  28. package/clis/chatgpt/utils.test.js +63 -0
  29. package/clis/chatgpt-app/ask.js +1 -1
  30. package/clis/chatgpt-app/ax.js +4 -2
  31. package/clis/chatgpt-app/ax.test.js +12 -0
  32. package/clis/chatgpt-app/model.js +1 -1
  33. package/clis/chatgpt-app/new.js +1 -1
  34. package/clis/chatgpt-app/read.js +1 -1
  35. package/clis/chatgpt-app/send.js +1 -1
  36. package/clis/chatgpt-app/status.js +1 -1
  37. package/clis/chatwise/ask.js +2 -2
  38. package/clis/chatwise/model.js +2 -2
  39. package/clis/chatwise/send.js +2 -2
  40. package/clis/claude/ask.js +128 -0
  41. package/clis/claude/ask.test.js +338 -0
  42. package/clis/claude/commands.test.js +118 -0
  43. package/clis/claude/detail.js +29 -0
  44. package/clis/claude/history.js +31 -0
  45. package/clis/claude/new.js +21 -0
  46. package/clis/claude/read.js +24 -0
  47. package/clis/claude/send.js +41 -0
  48. package/clis/claude/status.js +24 -0
  49. package/clis/claude/utils.js +440 -0
  50. package/clis/claude/utils.test.js +148 -0
  51. package/clis/codex/ask.js +2 -2
  52. package/clis/codex/send.js +2 -2
  53. package/clis/ctrip/search.js +1 -1
  54. package/clis/ctrip/search.test.js +4 -4
  55. package/clis/cursor/ask.js +2 -2
  56. package/clis/cursor/composer.js +2 -2
  57. package/clis/cursor/send.js +2 -2
  58. package/clis/deepseek/ask.js +17 -4
  59. package/clis/deepseek/ask.test.js +46 -0
  60. package/clis/deepseek/utils.js +55 -16
  61. package/clis/deepseek/utils.test.js +124 -5
  62. package/clis/doubao/utils.js +53 -11
  63. package/clis/doubao/utils.test.js +22 -2
  64. package/clis/eastmoney/announcement.js +1 -1
  65. package/clis/eastmoney/convertible.js +1 -1
  66. package/clis/eastmoney/etf.js +1 -1
  67. package/clis/eastmoney/holders.js +1 -1
  68. package/clis/eastmoney/index-board.js +1 -1
  69. package/clis/eastmoney/kline.js +1 -1
  70. package/clis/eastmoney/kuaixun.js +1 -1
  71. package/clis/eastmoney/longhu.js +1 -1
  72. package/clis/eastmoney/money-flow.js +1 -1
  73. package/clis/eastmoney/northbound.js +1 -1
  74. package/clis/eastmoney/quote.js +1 -1
  75. package/clis/eastmoney/rank.js +1 -1
  76. package/clis/eastmoney/sectors.js +1 -1
  77. package/clis/facebook/marketplace-inbox.js +83 -0
  78. package/clis/facebook/marketplace-listings.js +83 -0
  79. package/clis/facebook/marketplace.test.js +91 -0
  80. package/clis/google/news.js +1 -1
  81. package/clis/google/suggest.js +1 -1
  82. package/clis/google/trends.js +1 -1
  83. package/clis/google-scholar/cite.js +74 -0
  84. package/clis/google-scholar/cite.test.js +47 -0
  85. package/clis/google-scholar/profile.js +92 -0
  86. package/clis/google-scholar/profile.test.js +49 -0
  87. package/clis/google-scholar/search.js +1 -1
  88. package/clis/google-scholar/search.test.js +15 -0
  89. package/clis/hf/top.js +1 -1
  90. package/clis/instagram/collection-create.js +57 -0
  91. package/clis/instagram/saved.js +21 -7
  92. package/clis/jd/item.js +679 -47
  93. package/clis/jd/item.test.js +318 -7
  94. package/clis/jd/item.test.ts +517 -0
  95. package/clis/lesswrong/comments.js +1 -1
  96. package/clis/lesswrong/curated.js +1 -1
  97. package/clis/lesswrong/frontpage.js +1 -1
  98. package/clis/lesswrong/new.js +1 -1
  99. package/clis/lesswrong/read.js +1 -1
  100. package/clis/lesswrong/sequences.js +1 -1
  101. package/clis/lesswrong/shortform.js +1 -1
  102. package/clis/lesswrong/tag.js +1 -1
  103. package/clis/lesswrong/tags.js +1 -1
  104. package/clis/lesswrong/top-month.js +1 -1
  105. package/clis/lesswrong/top-week.js +1 -1
  106. package/clis/lesswrong/top-year.js +1 -1
  107. package/clis/lesswrong/top.js +1 -1
  108. package/clis/lesswrong/user-posts.js +1 -1
  109. package/clis/lesswrong/user.js +1 -1
  110. package/clis/paperreview/commands.test.js +6 -6
  111. package/clis/paperreview/feedback.js +1 -1
  112. package/clis/paperreview/review.js +1 -1
  113. package/clis/paperreview/submit.js +1 -1
  114. package/clis/producthunt/posts.js +1 -1
  115. package/clis/producthunt/today.js +1 -1
  116. package/clis/sinablog/search.js +1 -1
  117. package/clis/sinafinance/news.js +1 -1
  118. package/clis/sinafinance/stock.js +1 -1
  119. package/clis/sinafinance/stock.test.js +2 -2
  120. package/clis/spotify/spotify.js +6 -6
  121. package/clis/substack/search.js +1 -1
  122. package/clis/toutiao/articles.js +5 -6
  123. package/clis/toutiao/articles.test.js +22 -15
  124. package/clis/twitter/followers.js +2 -2
  125. package/clis/twitter/following.js +224 -73
  126. package/clis/twitter/following.test.js +277 -0
  127. package/clis/twitter/post.js +184 -47
  128. package/clis/twitter/post.test.js +114 -34
  129. package/clis/uiverse/_shared.js +63 -4
  130. package/clis/uiverse/_shared.test.js +7 -0
  131. package/clis/uiverse/code.js +1 -0
  132. package/clis/uiverse/navigation.test.js +12 -0
  133. package/clis/uiverse/preview.js +1 -0
  134. package/clis/web/read.js +319 -81
  135. package/clis/web/read.test.js +221 -5
  136. package/clis/weibo/favorites.js +169 -0
  137. package/clis/weibo/favorites.test.js +114 -0
  138. package/clis/weibo/publish.js +282 -0
  139. package/clis/weibo/publish.test.js +183 -0
  140. package/clis/weread/ranking.js +1 -1
  141. package/clis/weread/search-regression.test.js +8 -8
  142. package/clis/weread/search.js +1 -1
  143. package/clis/wikipedia/random.js +1 -1
  144. package/clis/wikipedia/search.js +1 -1
  145. package/clis/wikipedia/summary.js +1 -1
  146. package/clis/wikipedia/trending.js +1 -1
  147. package/clis/xianyu/chat.js +3 -3
  148. package/clis/xianyu/item.js +2 -2
  149. package/clis/xianyu/item.test.js +3 -3
  150. package/clis/xiaohongshu/search.js +17 -2
  151. package/clis/xiaohongshu/search.test.js +37 -1
  152. package/clis/xiaoyuzhou/download.js +1 -1
  153. package/clis/xiaoyuzhou/download.test.js +3 -3
  154. package/clis/xiaoyuzhou/episode.js +1 -1
  155. package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
  156. package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
  157. package/clis/xiaoyuzhou/podcast.js +1 -1
  158. package/clis/xiaoyuzhou/transcript.js +1 -1
  159. package/clis/xiaoyuzhou/transcript.test.js +5 -5
  160. package/clis/yollomi/models.js +1 -1
  161. package/clis/youtube/channel.js +24 -1
  162. package/clis/youtube/channel.test.js +59 -0
  163. package/clis/zhihu/answer.js +21 -162
  164. package/clis/zhihu/answer.test.js +26 -53
  165. package/clis/zhihu/collection.js +197 -0
  166. package/clis/zhihu/collection.test.js +290 -0
  167. package/clis/zhihu/collections.js +127 -0
  168. package/clis/zhihu/collections.test.js +182 -0
  169. package/clis/zhihu/comment.js +24 -305
  170. package/clis/zhihu/comment.test.js +31 -35
  171. package/clis/zhihu/favorite.js +44 -182
  172. package/clis/zhihu/favorite.test.js +30 -167
  173. package/clis/zhihu/follow.js +25 -56
  174. package/clis/zhihu/follow.test.js +20 -23
  175. package/clis/zhihu/like.js +22 -67
  176. package/clis/zhihu/like.test.js +19 -42
  177. package/clis/zhihu/search.js +3 -2
  178. package/clis/zhihu/write-shared.js +8 -1
  179. package/clis/zhihu/write-shared.test.js +1 -0
  180. package/clis/zlibrary/commands.test.js +75 -0
  181. package/clis/zlibrary/info.js +47 -0
  182. package/clis/zlibrary/search.js +46 -0
  183. package/clis/zlibrary/utils.js +136 -0
  184. package/dist/src/adapter-source.d.ts +11 -0
  185. package/dist/src/adapter-source.js +24 -0
  186. package/dist/src/adapter-source.test.js +29 -0
  187. package/dist/src/browser/base-page.d.ts +3 -1
  188. package/dist/src/browser/base-page.js +76 -1
  189. package/dist/src/browser/base-page.test.d.ts +1 -0
  190. package/dist/src/browser/base-page.test.js +74 -0
  191. package/dist/src/browser/bridge.d.ts +1 -2
  192. package/dist/src/browser/bridge.js +40 -41
  193. package/dist/src/browser/cdp.d.ts +1 -0
  194. package/dist/src/browser/cdp.js +3 -3
  195. package/dist/src/browser/daemon-client.d.ts +38 -4
  196. package/dist/src/browser/daemon-client.js +24 -7
  197. package/dist/src/browser/daemon-client.test.js +49 -0
  198. package/dist/src/browser/daemon-lifecycle.d.ts +23 -0
  199. package/dist/src/browser/daemon-lifecycle.js +67 -0
  200. package/dist/src/browser/daemon-version.d.ts +4 -0
  201. package/dist/src/browser/daemon-version.js +12 -0
  202. package/dist/src/browser/errors.js +3 -0
  203. package/dist/src/browser/errors.test.js +3 -0
  204. package/dist/src/browser/network-cache.d.ts +1 -0
  205. package/dist/src/browser/page.d.ts +3 -1
  206. package/dist/src/browser/page.js +10 -2
  207. package/dist/src/browser/profile.d.ts +14 -0
  208. package/dist/src/browser/profile.js +85 -0
  209. package/dist/src/build-manifest.d.ts +2 -0
  210. package/dist/src/build-manifest.js +13 -3
  211. package/dist/src/build-manifest.test.js +20 -2
  212. package/dist/src/cli.d.ts +6 -0
  213. package/dist/src/cli.js +477 -35
  214. package/dist/src/cli.test.js +303 -2
  215. package/dist/src/commanderAdapter.js +17 -9
  216. package/dist/src/commanderAdapter.test.js +67 -2
  217. package/dist/src/commands/daemon.d.ts +2 -0
  218. package/dist/src/commands/daemon.js +42 -1
  219. package/dist/src/commands/daemon.test.js +103 -2
  220. package/dist/src/completion-shared.js +1 -2
  221. package/dist/src/completion.test.js +3 -2
  222. package/dist/src/daemon.js +125 -41
  223. package/dist/src/doctor.d.ts +5 -6
  224. package/dist/src/doctor.js +77 -19
  225. package/dist/src/doctor.test.js +117 -0
  226. package/dist/src/engine.test.js +6 -5
  227. package/dist/src/errors.d.ts +14 -8
  228. package/dist/src/errors.js +36 -30
  229. package/dist/src/errors.test.js +5 -5
  230. package/dist/src/execution.d.ts +4 -0
  231. package/dist/src/execution.js +173 -25
  232. package/dist/src/execution.test.js +171 -1
  233. package/dist/src/main.js +10 -0
  234. package/dist/src/observation/artifact.d.ts +16 -0
  235. package/dist/src/observation/artifact.js +260 -0
  236. package/dist/src/observation/artifact.test.d.ts +1 -0
  237. package/dist/src/observation/artifact.test.js +121 -0
  238. package/dist/src/observation/events.d.ts +89 -0
  239. package/dist/src/observation/events.js +1 -0
  240. package/dist/src/observation/index.d.ts +7 -0
  241. package/dist/src/observation/index.js +7 -0
  242. package/dist/src/observation/manager.d.ts +9 -0
  243. package/dist/src/observation/manager.js +27 -0
  244. package/dist/src/observation/manager.test.d.ts +1 -0
  245. package/dist/src/observation/manager.test.js +13 -0
  246. package/dist/src/observation/redaction.d.ts +11 -0
  247. package/dist/src/observation/redaction.js +81 -0
  248. package/dist/src/observation/redaction.test.d.ts +1 -0
  249. package/dist/src/observation/redaction.test.js +32 -0
  250. package/dist/src/observation/retention.d.ts +32 -0
  251. package/dist/src/observation/retention.js +160 -0
  252. package/dist/src/observation/retention.test.d.ts +1 -0
  253. package/dist/src/observation/retention.test.js +118 -0
  254. package/dist/src/observation/ring-buffer.d.ts +22 -0
  255. package/dist/src/observation/ring-buffer.js +45 -0
  256. package/dist/src/observation/ring-buffer.test.d.ts +1 -0
  257. package/dist/src/observation/ring-buffer.test.js +22 -0
  258. package/dist/src/observation/session.d.ts +25 -0
  259. package/dist/src/observation/session.js +50 -0
  260. package/dist/src/pipeline/executor.test.js +1 -0
  261. package/dist/src/pipeline/steps/download.test.js +1 -0
  262. package/dist/src/pipeline/steps/fetch.js +1 -21
  263. package/dist/src/pipeline/steps/fetch.test.js +6 -12
  264. package/dist/src/plugin-scaffold.js +1 -1
  265. package/dist/src/plugin-scaffold.test.js +1 -1
  266. package/dist/src/registry.d.ts +40 -9
  267. package/dist/src/registry.js +3 -1
  268. package/dist/src/runtime-detect.d.ts +10 -0
  269. package/dist/src/runtime-detect.js +19 -0
  270. package/dist/src/runtime-detect.test.js +12 -1
  271. package/dist/src/runtime.d.ts +2 -0
  272. package/dist/src/runtime.js +1 -0
  273. package/dist/src/types.d.ts +22 -0
  274. package/dist/src/update-check.d.ts +31 -1
  275. package/dist/src/update-check.js +62 -16
  276. package/dist/src/update-check.test.js +86 -1
  277. package/package.json +1 -1
  278. package/dist/src/diagnostic.d.ts +0 -63
  279. package/dist/src/diagnostic.js +0 -292
  280. package/dist/src/diagnostic.test.js +0 -302
  281. /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
@@ -57,7 +57,7 @@ cli({
57
57
  'turnoverRate', 'amplitude', 'peDynamic', 'priceBook',
58
58
  'marketCap', 'floatMarketCap',
59
59
  ],
60
- func: async (_page, args) => {
60
+ func: async (args) => {
61
61
  const raw = splitSymbols(args.symbols);
62
62
  if (raw.length === 0) {
63
63
  throw new CliError('INVALID_ARGUMENT', 'At least one symbol is required');
@@ -43,7 +43,7 @@ cli({
43
43
  { name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
44
44
  ],
45
45
  columns: ['rank', 'code', 'name', 'price', 'changePercent', 'change', 'turnover', 'volume', 'turnoverRate', 'peDynamic', 'marketCap'],
46
- func: async (_page, args) => {
46
+ func: async (args) => {
47
47
  const market = String(args.market ?? 'hs-a').toLowerCase();
48
48
  const sortKey = String(args.sort ?? 'change').toLowerCase();
49
49
  const limit = Math.max(1, Math.min(Number(args.limit) || 20, 100));
@@ -33,7 +33,7 @@ cli({
33
33
  { name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
34
34
  ],
35
35
  columns: ['rank', 'code', 'name', 'price', 'changePercent', 'mainNet', 'leadStock', 'leadChangePercent', 'upCount', 'downCount'],
36
- func: async (_page, args) => {
36
+ func: async (args) => {
37
37
  const typeKey = String(args.type ?? 'industry').toLowerCase();
38
38
  const fs = SECTOR_TYPES[typeKey];
39
39
  if (!fs) throw new CliError('INVALID_ARGUMENT', `Unknown sector type "${typeKey}". Valid: ${Object.keys(SECTOR_TYPES).join(', ')}`);
@@ -0,0 +1,83 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
3
+
4
+ function normalizeLimit(value) {
5
+ const limit = Number(value ?? 20);
6
+ if (!Number.isInteger(limit) || limit <= 0) {
7
+ throw new ArgumentError('facebook marketplace-inbox --limit must be a positive integer');
8
+ }
9
+ return Math.min(limit, 100);
10
+ }
11
+
12
+ cli({
13
+ site: 'facebook',
14
+ name: 'marketplace-inbox',
15
+ description: 'List recent Facebook Marketplace buyer/seller conversations',
16
+ domain: 'www.facebook.com',
17
+ strategy: Strategy.COOKIE,
18
+ args: [
19
+ { name: 'limit', type: 'int', default: 20, help: 'Number of conversations to return' },
20
+ ],
21
+ columns: ['index', 'buyer', 'listing', 'snippet', 'time', 'unread'],
22
+ func: async (page, args) => {
23
+ if (!page) throw new CommandExecutionError('Browser session required for facebook marketplace-inbox');
24
+ const limit = normalizeLimit(args.limit);
25
+ await page.goto('https://www.facebook.com/marketplace/inbox/');
26
+ await page.wait(4);
27
+
28
+ const result = await page.evaluate(String.raw`(() => {
29
+ const clean = (s) => String(s || '').replace(/[\u00a0\u202f]/g, ' ').replace(/\s+/g, ' ').trim();
30
+ const timeRe = /^(?:\d{1,2}:\d{2}\s?(?:AM|PM|am|pm|上午|下午)?|Mon|Tue|Wed|Thu|Fri|Sat|Sun|Today|Yesterday|\d+[mhdw]|\d+\s*(?:min|h|d|w))$/;
31
+ const text = document.body?.innerText || '';
32
+ if (/log in|sign in/i.test(text) && !/Marketplace/i.test(text)) {
33
+ return { authRequired: true, rows: [] };
34
+ }
35
+
36
+ const lines = text.split(/\n+/).map(clean).filter(Boolean);
37
+ const out = [];
38
+ const seen = new Set();
39
+ const skipBuyer = /^(Marketplace|Browse all|Notifications|Inbox|Marketplace access|Buying|Selling|Create new listing|Create multiple listings|Location|Categories|Vehicles|Property Rentals|All|Pending payment|Paid|To be shipped|Shipped|Cash on delivery|Completed|Filter by label)$/i;
40
+ for (let i = 0; i < lines.length - 2; i += 1) {
41
+ const buyer = lines[i];
42
+ const meta = lines[i + 1];
43
+ if (skipBuyer.test(buyer) || !/^·\s+/.test(meta)) continue;
44
+ const listing = meta.replace(/^·\s*/, '');
45
+ if (!listing || /^Within\b/i.test(listing)) continue;
46
+ const snippet = lines[i + 2] || '';
47
+ const time = timeRe.test(lines[i + 3] || '') ? lines[i + 3] : '';
48
+ const key = buyer + '|' + listing;
49
+ if (seen.has(key)) continue;
50
+ seen.add(key);
51
+ const nearby = lines.slice(Math.max(0, i - 2), i + 5).join(' ');
52
+ out.push({
53
+ buyer,
54
+ listing,
55
+ snippet,
56
+ time,
57
+ unread: /Unread/i.test(nearby),
58
+ });
59
+ }
60
+ return { authRequired: false, rows: out };
61
+ })()`);
62
+
63
+ if (result?.authRequired) {
64
+ throw new AuthRequiredError('facebook.com', 'Facebook Marketplace inbox requires an active signed-in Facebook session.');
65
+ }
66
+ const items = Array.isArray(result?.rows) ? result.rows : [];
67
+ if (items.length === 0) {
68
+ throw new EmptyResultError('facebook marketplace-inbox', 'No Marketplace inbox conversations were visible. Check that Marketplace inbox is available for this account.');
69
+ }
70
+ return items.slice(0, limit).map((item, index) => ({
71
+ index: index + 1,
72
+ buyer: item.buyer || '',
73
+ listing: item.listing || '',
74
+ snippet: item.snippet || '',
75
+ time: item.time || '',
76
+ unread: Boolean(item.unread),
77
+ }));
78
+ },
79
+ });
80
+
81
+ export const __test__ = {
82
+ normalizeLimit,
83
+ };
@@ -0,0 +1,83 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
3
+
4
+ function normalizeLimit(value) {
5
+ const limit = Number(value ?? 20);
6
+ if (!Number.isInteger(limit) || limit <= 0) {
7
+ throw new ArgumentError('facebook marketplace-listings --limit must be a positive integer');
8
+ }
9
+ return Math.min(limit, 100);
10
+ }
11
+
12
+ cli({
13
+ site: 'facebook',
14
+ name: 'marketplace-listings',
15
+ description: 'List your Facebook Marketplace seller listings',
16
+ domain: 'www.facebook.com',
17
+ strategy: Strategy.COOKIE,
18
+ args: [
19
+ { name: 'limit', type: 'int', default: 20, help: 'Number of listings to return' },
20
+ ],
21
+ columns: ['index', 'title', 'price', 'status', 'listed', 'clicks', 'actions'],
22
+ func: async (page, args) => {
23
+ if (!page) throw new CommandExecutionError('Browser session required for facebook marketplace-listings');
24
+ const limit = normalizeLimit(args.limit);
25
+ await page.goto('https://www.facebook.com/marketplace/you/selling/');
26
+ await page.wait(4);
27
+
28
+ const result = await page.evaluate(String.raw`(() => {
29
+ const clean = (s) => String(s || '').replace(/\u00a0/g, ' ').replace(/\s+/g, ' ').trim();
30
+ const allText = document.body?.innerText || '';
31
+ if (/log in|sign in/i.test(allText) && !/Marketplace/i.test(allText)) {
32
+ return { authRequired: true, rows: [] };
33
+ }
34
+
35
+ const lines = allText.split(/\n+/).map(clean).filter(Boolean);
36
+ const seen = new Set();
37
+ const out = [];
38
+ for (let i = 1; i < lines.length; i += 1) {
39
+ if (!/^(?:CA\$|\$)\s*\d+/.test(lines[i])) continue;
40
+ const title = lines[i - 1];
41
+ if (!title || /^(Hide|All listings|Needs attention|Marketplace|Selling)$/i.test(title)) continue;
42
+ if (seen.has(title)) continue;
43
+ seen.add(title);
44
+ const windowLines = lines.slice(i, i + 12);
45
+ const status = windowLines.find((line) => /^(Active|Sold|Pending|Draft)$/i.test(line)) || '';
46
+ const listed = windowLines.find((line) => /Listed on\b/i.test(line))?.replace(/^·\s*/, '') || '';
47
+ const clickLine = windowLines.find((line) => /clicks? on listing/i.test(line)) || '';
48
+ const clickMatch = clickLine.match(/([\d,.]+)\s+clicks? on listing/i);
49
+ const actions = windowLines.filter((line) => /^(Mark as sold|Mark as available|Relist this item|Share|Boost listing)$/i.test(line));
50
+ out.push({
51
+ title,
52
+ price: lines[i],
53
+ status,
54
+ listed,
55
+ clicks: clickMatch ? clickMatch[1] : '',
56
+ actions,
57
+ });
58
+ }
59
+ return { authRequired: false, rows: out };
60
+ })()`);
61
+
62
+ if (result?.authRequired) {
63
+ throw new AuthRequiredError('facebook.com', 'Facebook Marketplace seller listings require an active signed-in Facebook session.');
64
+ }
65
+ const items = Array.isArray(result?.rows) ? result.rows : [];
66
+ if (items.length === 0) {
67
+ throw new EmptyResultError('facebook marketplace-listings', 'No seller listings were visible. Check that Marketplace selling is available for this account.');
68
+ }
69
+ return items.slice(0, limit).map((item, index) => ({
70
+ index: index + 1,
71
+ title: item.title || '',
72
+ price: item.price || '',
73
+ status: item.status || '',
74
+ listed: item.listed || '',
75
+ clicks: item.clicks || '',
76
+ actions: Array.isArray(item.actions) ? item.actions.join(', ') : String(item.actions || ''),
77
+ }));
78
+ },
79
+ });
80
+
81
+ export const __test__ = {
82
+ normalizeLimit,
83
+ };
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '@jackwener/opencli/registry';
3
+ import { ArgumentError, AuthRequiredError, EmptyResultError } from '@jackwener/opencli/errors';
4
+ import './marketplace-listings.js';
5
+ import './marketplace-inbox.js';
6
+
7
+ function makePage(overrides = {}) {
8
+ return {
9
+ goto: vi.fn().mockResolvedValue(undefined),
10
+ wait: vi.fn().mockResolvedValue(undefined),
11
+ evaluate: vi.fn().mockResolvedValue([]),
12
+ ...overrides,
13
+ };
14
+ }
15
+
16
+ describe('facebook marketplace read commands', () => {
17
+ it('marketplace-listings navigates to selling page and returns limited listing rows', async () => {
18
+ const command = getRegistry().get('facebook/marketplace-listings');
19
+ expect(command).toBeDefined();
20
+ const page = makePage({
21
+ evaluate: vi.fn().mockResolvedValue({
22
+ authRequired: false,
23
+ rows: [
24
+ { title: 'Black electric standing desk', price: 'CA$80', status: 'Active', listed: 'Listed on 4/26', clicks: '87', actions: ['Mark as sold', 'Share'] },
25
+ { title: 'Large gray corduroy beanbag chair', price: 'CA$30', status: 'Sold', listed: 'Listed on 4/26', clicks: '52', actions: ['Mark as available', 'Relist this item'] },
26
+ ],
27
+ }),
28
+ });
29
+
30
+ const rows = await command.func(page, { limit: 1 });
31
+
32
+ expect(page.goto).toHaveBeenCalledWith('https://www.facebook.com/marketplace/you/selling/');
33
+ expect(page.wait).toHaveBeenCalledWith(4);
34
+ expect(rows).toEqual([
35
+ {
36
+ index: 1,
37
+ title: 'Black electric standing desk',
38
+ price: 'CA$80',
39
+ status: 'Active',
40
+ listed: 'Listed on 4/26',
41
+ clicks: '87',
42
+ actions: 'Mark as sold, Share',
43
+ },
44
+ ]);
45
+ });
46
+
47
+ it('marketplace-inbox navigates to inbox and returns recent buyer conversations', async () => {
48
+ const command = getRegistry().get('facebook/marketplace-inbox');
49
+ expect(command).toBeDefined();
50
+ const page = makePage({
51
+ evaluate: vi.fn().mockResolvedValue({
52
+ authRequired: false,
53
+ rows: [
54
+ { buyer: 'Kulwant', listing: 'White 3-tier rolling utility cart', snippet: 'Can I pick up today?', time: '3:43 PM', unread: true },
55
+ { buyer: 'Gabriel', listing: 'Black electric standing desk', snippet: 'Yes, still available.', time: '12:17 PM', unread: false },
56
+ ],
57
+ }),
58
+ });
59
+
60
+ const rows = await command.func(page, { limit: 2 });
61
+
62
+ expect(page.goto).toHaveBeenCalledWith('https://www.facebook.com/marketplace/inbox/');
63
+ expect(page.wait).toHaveBeenCalledWith(4);
64
+ expect(rows).toEqual([
65
+ { index: 1, buyer: 'Kulwant', listing: 'White 3-tier rolling utility cart', snippet: 'Can I pick up today?', time: '3:43 PM', unread: true },
66
+ { index: 2, buyer: 'Gabriel', listing: 'Black electric standing desk', snippet: 'Yes, still available.', time: '12:17 PM', unread: false },
67
+ ]);
68
+ });
69
+
70
+ it('throws EmptyResultError when Marketplace returns no inbox rows', async () => {
71
+ const command = getRegistry().get('facebook/marketplace-inbox');
72
+ const page = makePage({ evaluate: vi.fn().mockResolvedValue({ authRequired: false, rows: [] }) });
73
+
74
+ await expect(command.func(page, { limit: 5 })).rejects.toThrow(EmptyResultError);
75
+ });
76
+
77
+ it('throws AuthRequiredError when Marketplace returns a login page', async () => {
78
+ const command = getRegistry().get('facebook/marketplace-listings');
79
+ const page = makePage({ evaluate: vi.fn().mockResolvedValue({ authRequired: true, rows: [] }) });
80
+
81
+ await expect(command.func(page, { limit: 5 })).rejects.toThrow(AuthRequiredError);
82
+ });
83
+
84
+ it('throws ArgumentError for invalid limits', async () => {
85
+ const command = getRegistry().get('facebook/marketplace-listings');
86
+ const page = makePage();
87
+
88
+ await expect(command.func(page, { limit: 0 })).rejects.toThrow(ArgumentError);
89
+ expect(page.goto).not.toHaveBeenCalled();
90
+ });
91
+ });
@@ -18,7 +18,7 @@ cli({
18
18
  { name: 'region', default: 'US', help: 'Region code (e.g. US, CN)' },
19
19
  ],
20
20
  columns: ['title', 'source', 'date', 'url'],
21
- func: async (_page, args) => {
21
+ func: async (args) => {
22
22
  const limit = Math.max(1, Math.min(Number(args.limit), 100));
23
23
  const lang = encodeURIComponent(args.lang);
24
24
  const region = encodeURIComponent(args.region);
@@ -15,7 +15,7 @@ cli({
15
15
  { name: 'lang', default: 'zh-CN', help: 'Language code' },
16
16
  ],
17
17
  columns: ['suggestion'],
18
- func: async (_page, args) => {
18
+ func: async (args) => {
19
19
  const keyword = encodeURIComponent(args.keyword);
20
20
  const lang = encodeURIComponent(args.lang);
21
21
  const url = `https://suggestqueries.google.com/complete/search?client=firefox&q=${keyword}&hl=${lang}`;
@@ -16,7 +16,7 @@ cli({
16
16
  { name: 'limit', type: 'int', default: 20, help: 'Number of results' },
17
17
  ],
18
18
  columns: ['title', 'traffic', 'date'],
19
- func: async (_page, args) => {
19
+ func: async (args) => {
20
20
  const limit = Math.max(1, Math.min(Number(args.limit), 100));
21
21
  const region = encodeURIComponent(args.region);
22
22
  const url = `https://trends.google.com/trending/rss?geo=${region}`;
@@ -0,0 +1,74 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { CommandExecutionError } from '@jackwener/opencli/errors';
3
+ import { requireNonEmptyQuery } from '../_shared/common.js';
4
+
5
+ cli({
6
+ site: 'google-scholar',
7
+ name: 'cite',
8
+ description: 'Get citation for a Google Scholar paper',
9
+ domain: 'scholar.google.com',
10
+ strategy: Strategy.PUBLIC,
11
+ browser: true,
12
+ args: [
13
+ { name: 'query', positional: true, required: true, help: 'Paper title to search for' },
14
+ { name: 'style', default: 'bibtex', choices: ['bibtex', 'endnote', 'refman', 'refworks'], help: 'Citation format' },
15
+ { name: 'index', type: 'int', default: 1, help: 'Which search result to cite (1-based)' },
16
+ ],
17
+ columns: ['title', 'format', 'citation'],
18
+ navigateBefore: false,
19
+ func: async (page, kwargs) => {
20
+ const query = requireNonEmptyQuery(kwargs.query);
21
+ const format = kwargs.style || 'bibtex';
22
+ const index = Math.max(1, kwargs.index || 1) - 1;
23
+
24
+ await page.goto(`https://scholar.google.com/scholar?q=${encodeURIComponent(query)}&hl=en`);
25
+ await page.wait(3);
26
+
27
+ const clicked = await page.evaluate(`(() => {
28
+ var cites = document.querySelectorAll('a.gs_or_cit');
29
+ if (cites.length <= ${index}) return { ok: false, reason: 'result not found at index ${index + 1}' };
30
+ var titleEl = document.querySelectorAll('.gs_r.gs_or.gs_scl')[${index}];
31
+ var title = '';
32
+ if (titleEl) {
33
+ var t = titleEl.querySelector('.gs_rt a, h3 a');
34
+ title = t ? t.textContent.trim() : '';
35
+ }
36
+ cites[${index}].click();
37
+ return { ok: true, title: title };
38
+ })()`);
39
+
40
+ if (!clicked?.ok) {
41
+ throw new CommandExecutionError(clicked?.reason || `Could not find search result at index ${index + 1}`);
42
+ }
43
+
44
+ await page.wait(2);
45
+
46
+ const formatMap = { bibtex: 'BibTeX', endnote: 'EndNote', refman: 'RefMan', refworks: 'RefWorks' };
47
+ const formatLabel = formatMap[format] || 'BibTeX';
48
+
49
+ const citeUrl = await page.evaluate(`(() => {
50
+ var links = document.querySelectorAll('#gs_cit a.gs_citi');
51
+ for (var i = 0; i < links.length; i++) {
52
+ if (links[i].textContent.trim() === '${formatLabel}') return links[i].href;
53
+ }
54
+ return null;
55
+ })()`);
56
+
57
+ if (!citeUrl) {
58
+ throw new CommandExecutionError(`Could not find ${formatLabel} citation link for result ${index + 1}`);
59
+ }
60
+
61
+ await page.goto(citeUrl);
62
+ await page.wait(2);
63
+
64
+ const citation = await page.evaluate(`(() => {
65
+ return (document.body.innerText || '').trim();
66
+ })()`);
67
+
68
+ if (!citation) {
69
+ throw new CommandExecutionError(`${formatLabel} citation page returned an empty response`);
70
+ }
71
+
72
+ return [{ title: clicked.title, format: format, citation }];
73
+ },
74
+ });
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { CommandExecutionError } from '@jackwener/opencli/errors';
3
+ import { getRegistry } from '@jackwener/opencli/registry';
4
+ import './cite.js';
5
+
6
+ describe('google-scholar cite command', () => {
7
+ const command = getRegistry().get('google-scholar/cite');
8
+
9
+ it('registers as a public browser command', () => {
10
+ expect(command).toBeDefined();
11
+ expect(command.site).toBe('google-scholar');
12
+ expect(command.strategy).toBe('public');
13
+ expect(command.browser).toBe(true);
14
+ });
15
+
16
+ it('rejects empty queries before browser navigation', async () => {
17
+ const page = { goto: vi.fn() };
18
+ await expect(command.func(page, { query: ' ' })).rejects.toMatchObject({
19
+ name: 'ArgumentError',
20
+ code: 'ARGUMENT',
21
+ });
22
+ expect(page.goto).not.toHaveBeenCalled();
23
+ });
24
+
25
+ it('throws when the requested search result index does not exist', async () => {
26
+ const page = {
27
+ goto: vi.fn().mockResolvedValue(undefined),
28
+ wait: vi.fn().mockResolvedValue(undefined),
29
+ evaluate: vi.fn().mockResolvedValueOnce({ ok: false, reason: 'result not found at index 2' }),
30
+ };
31
+ await expect(command.func(page, { query: 'test', index: 2 })).rejects.toThrow(CommandExecutionError);
32
+ });
33
+
34
+ it('looks up the requested citation style instead of only locking BibTeX', async () => {
35
+ const page = {
36
+ goto: vi.fn().mockResolvedValue(undefined),
37
+ wait: vi.fn().mockResolvedValue(undefined),
38
+ evaluate: vi.fn()
39
+ .mockResolvedValueOnce({ ok: true, title: 'Paper Title' })
40
+ .mockResolvedValueOnce('https://example.com/refworks')
41
+ .mockResolvedValueOnce('RefWorks citation body'),
42
+ };
43
+ const result = await command.func(page, { query: 'test', style: 'refworks' });
44
+ expect(result).toEqual([{ title: 'Paper Title', format: 'refworks', citation: 'RefWorks citation body' }]);
45
+ expect(page.evaluate.mock.calls[1][0]).toContain('RefWorks');
46
+ });
47
+ });
@@ -0,0 +1,92 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { CommandExecutionError } from '@jackwener/opencli/errors';
3
+ import { clampInt, requireNonEmptyQuery } from '../_shared/common.js';
4
+
5
+ cli({
6
+ site: 'google-scholar',
7
+ name: 'profile',
8
+ description: 'View a Google Scholar author profile',
9
+ domain: 'scholar.google.com',
10
+ strategy: Strategy.PUBLIC,
11
+ browser: true,
12
+ args: [
13
+ { name: 'author', positional: true, required: true, help: 'Author name or Scholar user ID (e.g. JicYPdAAAAAJ)' },
14
+ { name: 'limit', type: 'int', default: 10, help: 'Max papers to show (max 20)' },
15
+ ],
16
+ columns: ['rank', 'title', 'cited', 'year'],
17
+ navigateBefore: false,
18
+ func: async (page, kwargs) => {
19
+ const author = requireNonEmptyQuery(kwargs.author, 'author');
20
+ const limit = clampInt(kwargs.limit, 10, 1, 20);
21
+
22
+ const isUserId = /^[A-Za-z0-9_-]{12}$/.test(author);
23
+ if (isUserId) {
24
+ await page.goto(`https://scholar.google.com/citations?user=${author}&hl=en&sortby=citedby`);
25
+ } else {
26
+ await page.goto(`https://scholar.google.com/citations?view_op=search_authors&mauthors=${encodeURIComponent(author)}&hl=en`);
27
+ await page.wait(3);
28
+
29
+ const profileClicked = await page.evaluate(`(() => {
30
+ var link = document.querySelector('.gs_ai_pho, .gsc_oai_photo, a[href*="citations?user="]');
31
+ if (link) { link.click(); return true; }
32
+ return false;
33
+ })()`);
34
+
35
+ if (!profileClicked) {
36
+ throw new CommandExecutionError(`No profile found for: ${author}`);
37
+ }
38
+ }
39
+
40
+ await page.wait(3);
41
+
42
+ const data = await page.evaluate(`(() => {
43
+ var name = (document.querySelector('#gsc_prf_in') || {}).textContent || '';
44
+ var affiliation = (document.querySelector('.gsc_prf_il') || {}).textContent || '';
45
+
46
+ var stats = document.querySelectorAll('#gsc_rsb_st td.gsc_rsb_std');
47
+ var citations = stats[0] ? stats[0].textContent.trim() : '';
48
+ var hIndex = stats[2] ? stats[2].textContent.trim() : '';
49
+ var i10Index = stats[4] ? stats[4].textContent.trim() : '';
50
+
51
+ var papers = [];
52
+ var rows = document.querySelectorAll('#gsc_a_b .gsc_a_tr');
53
+ for (var i = 0; i < rows.length && i < ${limit}; i++) {
54
+ var titleEl = rows[i].querySelector('.gsc_a_at');
55
+ var citedEl = rows[i].querySelector('.gsc_a_ac');
56
+ var yearEl = rows[i].querySelector('.gsc_a_y span');
57
+ if (titleEl) papers.push({
58
+ rank: i + 1,
59
+ title: titleEl.textContent.trim(),
60
+ cited: citedEl ? citedEl.textContent.trim() : '0',
61
+ year: yearEl ? yearEl.textContent.trim() : '',
62
+ });
63
+ }
64
+
65
+ return {
66
+ name: name.trim(),
67
+ affiliation: affiliation.trim(),
68
+ citations: citations,
69
+ hIndex: hIndex,
70
+ i10Index: i10Index,
71
+ papers: papers,
72
+ };
73
+ })()`);
74
+
75
+ if (!data?.name) {
76
+ throw new CommandExecutionError(`Could not load Google Scholar profile for: ${author}`);
77
+ }
78
+
79
+ if (!data.papers || data.papers.length === 0) {
80
+ throw new CommandExecutionError(`No papers found for: ${data.name || author}`);
81
+ }
82
+
83
+ const summary = {
84
+ rank: 0,
85
+ title: data.name + (data.affiliation ? ' (' + data.affiliation + ')' : ''),
86
+ cited: 'h=' + data.hIndex + ' i10=' + data.i10Index + ' total=' + data.citations,
87
+ year: '-',
88
+ };
89
+
90
+ return [summary, ...data.papers];
91
+ },
92
+ });
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { CommandExecutionError } from '@jackwener/opencli/errors';
3
+ import { getRegistry } from '@jackwener/opencli/registry';
4
+ import './profile.js';
5
+
6
+ describe('google-scholar profile command', () => {
7
+ const command = getRegistry().get('google-scholar/profile');
8
+
9
+ it('registers as a public browser command', () => {
10
+ expect(command).toBeDefined();
11
+ expect(command.site).toBe('google-scholar');
12
+ expect(command.strategy).toBe('public');
13
+ expect(command.browser).toBe(true);
14
+ });
15
+
16
+ it('rejects empty author before browser navigation', async () => {
17
+ const page = { goto: vi.fn() };
18
+ await expect(command.func(page, { author: ' ' })).rejects.toMatchObject({
19
+ name: 'ArgumentError',
20
+ code: 'ARGUMENT',
21
+ });
22
+ expect(page.goto).not.toHaveBeenCalled();
23
+ });
24
+
25
+ it('throws when author search does not resolve to a profile', async () => {
26
+ const page = {
27
+ goto: vi.fn().mockResolvedValue(undefined),
28
+ wait: vi.fn().mockResolvedValue(undefined),
29
+ evaluate: vi.fn().mockResolvedValueOnce(false),
30
+ };
31
+ await expect(command.func(page, { author: 'missing author' })).rejects.toThrow(CommandExecutionError);
32
+ });
33
+
34
+ it('throws when the loaded profile has no papers', async () => {
35
+ const page = {
36
+ goto: vi.fn().mockResolvedValue(undefined),
37
+ wait: vi.fn().mockResolvedValue(undefined),
38
+ evaluate: vi.fn().mockResolvedValueOnce({
39
+ name: 'Author Name',
40
+ affiliation: 'Org',
41
+ citations: '0',
42
+ hIndex: '0',
43
+ i10Index: '0',
44
+ papers: [],
45
+ }),
46
+ };
47
+ await expect(command.func(page, { author: 'JicYPdAAAAAJ' })).rejects.toThrow(CommandExecutionError);
48
+ });
49
+ });
@@ -23,7 +23,7 @@ cli({
23
23
  (() => {
24
24
  const normalize = v => (v || '').replace(/\\s+/g, ' ').trim();
25
25
  const results = [];
26
- for (const el of document.querySelectorAll('.gs_r.gs_or.gs_scl, .gs_ri')) {
26
+ for (const el of document.querySelectorAll('.gs_r.gs_or.gs_scl')) {
27
27
  const container = el.querySelector('.gs_ri') || el;
28
28
  const titleEl = container.querySelector('.gs_rt a, h3 a');
29
29
  const title = normalize(titleEl?.textContent);
@@ -20,4 +20,19 @@ describe('google-scholar search command', () => {
20
20
  });
21
21
  expect(page.goto).not.toHaveBeenCalled();
22
22
  });
23
+
24
+ it('locks dedup to outer Scholar result cards while preserving inner content extraction', async () => {
25
+ const page = {
26
+ goto: vi.fn().mockResolvedValue(undefined),
27
+ wait: vi.fn().mockResolvedValue(undefined),
28
+ evaluate: vi.fn().mockResolvedValue([]),
29
+ };
30
+
31
+ await command.func(page, { query: 'transformer' });
32
+
33
+ const script = page.evaluate.mock.calls[0][0];
34
+ expect(script).toContain("document.querySelectorAll('.gs_r.gs_or.gs_scl')");
35
+ expect(script).not.toContain(".gs_r.gs_or.gs_scl, .gs_ri");
36
+ expect(script).toContain("const container = el.querySelector('.gs_ri') || el");
37
+ });
23
38
  });
package/clis/hf/top.js CHANGED
@@ -50,7 +50,7 @@ cli({
50
50
  return getWeekRange();
51
51
  return kwargs.date ?? new Date().toISOString().slice(0, 10);
52
52
  },
53
- func: async (_page, kwargs) => {
53
+ func: async (kwargs) => {
54
54
  const period = String(kwargs.period ?? 'daily');
55
55
  const all = Boolean(kwargs.all);
56
56
  const endpoint = process.env.HF_ENDPOINT?.replace(/\/+$/, '') || 'https://huggingface.co';