@jackwener/opencli 1.4.0 → 1.4.1

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 (209) hide show
  1. package/.github/actions/setup-chrome/action.yml +5 -4
  2. package/.github/workflows/ci.yml +17 -3
  3. package/.github/workflows/e2e-headed.yml +16 -3
  4. package/CHANGELOG.md +23 -0
  5. package/PRIVACY.md +57 -0
  6. package/README.md +1 -1
  7. package/README.zh-CN.md +1 -1
  8. package/SKILL.md +101 -2
  9. package/dist/cli-manifest.json +720 -32
  10. package/dist/clis/apple-podcasts/search.js +2 -1
  11. package/dist/clis/arxiv/search.js +2 -2
  12. package/dist/clis/bbc/news.js +0 -1
  13. package/dist/clis/ctrip/search.js +0 -1
  14. package/dist/clis/douyin/_shared/browser-fetch.d.ts +10 -0
  15. package/dist/clis/douyin/_shared/browser-fetch.js +30 -0
  16. package/dist/clis/douyin/_shared/browser-fetch.test.d.ts +1 -0
  17. package/dist/clis/douyin/_shared/browser-fetch.test.js +31 -0
  18. package/dist/clis/douyin/_shared/creation-id.d.ts +1 -0
  19. package/dist/clis/douyin/_shared/creation-id.js +5 -0
  20. package/dist/clis/douyin/_shared/creation-id.test.d.ts +1 -0
  21. package/dist/clis/douyin/_shared/creation-id.test.js +22 -0
  22. package/dist/clis/douyin/_shared/imagex-upload.d.ts +20 -0
  23. package/dist/clis/douyin/_shared/imagex-upload.js +53 -0
  24. package/dist/clis/douyin/_shared/imagex-upload.test.d.ts +1 -0
  25. package/dist/clis/douyin/_shared/imagex-upload.test.js +87 -0
  26. package/dist/clis/douyin/_shared/sts2.d.ts +8 -0
  27. package/dist/clis/douyin/_shared/sts2.js +15 -0
  28. package/dist/clis/douyin/_shared/text-extra.d.ts +18 -0
  29. package/dist/clis/douyin/_shared/text-extra.js +15 -0
  30. package/dist/clis/douyin/_shared/text-extra.test.d.ts +1 -0
  31. package/dist/clis/douyin/_shared/text-extra.test.js +37 -0
  32. package/dist/clis/douyin/_shared/timing.d.ts +2 -0
  33. package/dist/clis/douyin/_shared/timing.js +22 -0
  34. package/dist/clis/douyin/_shared/timing.test.d.ts +1 -0
  35. package/dist/clis/douyin/_shared/timing.test.js +28 -0
  36. package/dist/clis/douyin/_shared/tos-upload-short-read.test.d.ts +11 -0
  37. package/dist/clis/douyin/_shared/tos-upload-short-read.test.js +83 -0
  38. package/dist/clis/douyin/_shared/tos-upload.d.ts +53 -0
  39. package/dist/clis/douyin/_shared/tos-upload.js +295 -0
  40. package/dist/clis/douyin/_shared/tos-upload.test.d.ts +1 -0
  41. package/dist/clis/douyin/_shared/tos-upload.test.js +229 -0
  42. package/dist/clis/douyin/_shared/transcode.d.ts +27 -0
  43. package/dist/clis/douyin/_shared/transcode.js +45 -0
  44. package/dist/clis/douyin/_shared/transcode.test.d.ts +1 -0
  45. package/dist/clis/douyin/_shared/transcode.test.js +93 -0
  46. package/dist/clis/douyin/_shared/types.d.ts +26 -0
  47. package/dist/clis/douyin/_shared/types.js +1 -0
  48. package/dist/clis/douyin/activities.d.ts +1 -0
  49. package/dist/clis/douyin/activities.js +20 -0
  50. package/dist/clis/douyin/activities.test.d.ts +1 -0
  51. package/dist/clis/douyin/activities.test.js +22 -0
  52. package/dist/clis/douyin/collections.d.ts +1 -0
  53. package/dist/clis/douyin/collections.js +22 -0
  54. package/dist/clis/douyin/collections.test.d.ts +1 -0
  55. package/dist/clis/douyin/collections.test.js +23 -0
  56. package/dist/clis/douyin/delete.d.ts +1 -0
  57. package/dist/clis/douyin/delete.js +18 -0
  58. package/dist/clis/douyin/delete.test.d.ts +1 -0
  59. package/dist/clis/douyin/delete.test.js +11 -0
  60. package/dist/clis/douyin/draft.d.ts +14 -0
  61. package/dist/clis/douyin/draft.js +237 -0
  62. package/dist/clis/douyin/draft.test.d.ts +1 -0
  63. package/dist/clis/douyin/draft.test.js +11 -0
  64. package/dist/clis/douyin/drafts.d.ts +1 -0
  65. package/dist/clis/douyin/drafts.js +23 -0
  66. package/dist/clis/douyin/drafts.test.d.ts +1 -0
  67. package/dist/clis/douyin/drafts.test.js +11 -0
  68. package/dist/clis/douyin/hashtag.d.ts +1 -0
  69. package/dist/clis/douyin/hashtag.js +45 -0
  70. package/dist/clis/douyin/hashtag.test.d.ts +1 -0
  71. package/dist/clis/douyin/hashtag.test.js +25 -0
  72. package/dist/clis/douyin/location.d.ts +1 -0
  73. package/dist/clis/douyin/location.js +24 -0
  74. package/dist/clis/douyin/location.test.d.ts +1 -0
  75. package/dist/clis/douyin/location.test.js +23 -0
  76. package/dist/clis/douyin/profile.d.ts +1 -0
  77. package/dist/clis/douyin/profile.js +28 -0
  78. package/dist/clis/douyin/profile.test.d.ts +1 -0
  79. package/dist/clis/douyin/profile.test.js +11 -0
  80. package/dist/clis/douyin/publish.d.ts +14 -0
  81. package/dist/clis/douyin/publish.js +288 -0
  82. package/dist/clis/douyin/publish.test.d.ts +1 -0
  83. package/dist/clis/douyin/publish.test.js +38 -0
  84. package/dist/clis/douyin/stats.d.ts +1 -0
  85. package/dist/clis/douyin/stats.js +27 -0
  86. package/dist/clis/douyin/stats.test.d.ts +1 -0
  87. package/dist/clis/douyin/stats.test.js +22 -0
  88. package/dist/clis/douyin/update.d.ts +1 -0
  89. package/dist/clis/douyin/update.js +31 -0
  90. package/dist/clis/douyin/update.test.d.ts +1 -0
  91. package/dist/clis/douyin/update.test.js +11 -0
  92. package/dist/clis/douyin/videos.d.ts +1 -0
  93. package/dist/clis/douyin/videos.js +34 -0
  94. package/dist/clis/douyin/videos.test.d.ts +1 -0
  95. package/dist/clis/douyin/videos.test.js +11 -0
  96. package/dist/clis/hackernews/search.yaml +1 -1
  97. package/dist/clis/instagram/search.yaml +2 -1
  98. package/dist/clis/linux-do/search.yaml +3 -1
  99. package/dist/clis/medium/search.js +1 -1
  100. package/dist/clis/reuters/search.js +0 -1
  101. package/dist/clis/twitter/search.js +5 -3
  102. package/dist/clis/twitter/search.test.js +54 -2
  103. package/dist/clis/weibo/comments.d.ts +1 -0
  104. package/dist/clis/weibo/comments.js +53 -0
  105. package/dist/clis/weibo/feed.d.ts +1 -0
  106. package/dist/clis/weibo/feed.js +56 -0
  107. package/dist/clis/weibo/hot.js +0 -1
  108. package/dist/clis/weibo/me.d.ts +1 -0
  109. package/dist/clis/weibo/me.js +76 -0
  110. package/dist/clis/weibo/post.d.ts +1 -0
  111. package/dist/clis/weibo/post.js +75 -0
  112. package/dist/clis/weibo/user.d.ts +1 -0
  113. package/dist/clis/weibo/user.js +63 -0
  114. package/dist/clis/weibo/utils.d.ts +6 -0
  115. package/dist/clis/weibo/utils.js +30 -0
  116. package/dist/clis/weread/search.js +3 -2
  117. package/dist/clis/xueqiu/search.yaml +2 -1
  118. package/dist/clis/yahoo-finance/quote.js +0 -1
  119. package/dist/clis/youtube/channel.d.ts +1 -0
  120. package/dist/clis/youtube/channel.js +150 -0
  121. package/dist/clis/youtube/comments.d.ts +1 -0
  122. package/dist/clis/youtube/comments.js +95 -0
  123. package/dist/clis/youtube/search.js +0 -1
  124. package/dist/clis/zhihu/search.yaml +2 -1
  125. package/dist/external-clis.yaml +0 -17
  126. package/dist/weread-search-regression.test.d.ts +1 -0
  127. package/dist/weread-search-regression.test.js +39 -0
  128. package/docs/.vitepress/config.mts +13 -0
  129. package/docs/adapters/browser/douyin.md +75 -0
  130. package/docs/adapters/browser/twitter.md +6 -0
  131. package/docs/adapters/index.md +6 -1
  132. package/extension/dist/background.js +508 -518
  133. package/extension/manifest.json +6 -2
  134. package/extension/package.json +1 -1
  135. package/extension/popup.html +84 -0
  136. package/extension/popup.js +25 -0
  137. package/extension/src/background.ts +20 -1
  138. package/package.json +1 -1
  139. package/src/clis/apple-podcasts/search.ts +2 -1
  140. package/src/clis/arxiv/search.ts +2 -2
  141. package/src/clis/bbc/news.ts +0 -1
  142. package/src/clis/ctrip/search.ts +0 -1
  143. package/src/clis/douyin/_shared/browser-fetch.test.ts +38 -0
  144. package/src/clis/douyin/_shared/browser-fetch.ts +45 -0
  145. package/src/clis/douyin/_shared/creation-id.test.ts +26 -0
  146. package/src/clis/douyin/_shared/creation-id.ts +8 -0
  147. package/src/clis/douyin/_shared/imagex-upload.test.ts +113 -0
  148. package/src/clis/douyin/_shared/imagex-upload.ts +76 -0
  149. package/src/clis/douyin/_shared/sts2.ts +20 -0
  150. package/src/clis/douyin/_shared/text-extra.test.ts +42 -0
  151. package/src/clis/douyin/_shared/text-extra.ts +33 -0
  152. package/src/clis/douyin/_shared/timing.test.ts +38 -0
  153. package/src/clis/douyin/_shared/timing.ts +22 -0
  154. package/src/clis/douyin/_shared/tos-upload-short-read.test.ts +102 -0
  155. package/src/clis/douyin/_shared/tos-upload.test.ts +281 -0
  156. package/src/clis/douyin/_shared/tos-upload.ts +444 -0
  157. package/src/clis/douyin/_shared/transcode.test.ts +117 -0
  158. package/src/clis/douyin/_shared/transcode.ts +78 -0
  159. package/src/clis/douyin/_shared/types.ts +29 -0
  160. package/src/clis/douyin/activities.test.ts +25 -0
  161. package/src/clis/douyin/activities.ts +23 -0
  162. package/src/clis/douyin/collections.test.ts +26 -0
  163. package/src/clis/douyin/collections.ts +25 -0
  164. package/src/clis/douyin/delete.test.ts +12 -0
  165. package/src/clis/douyin/delete.ts +20 -0
  166. package/src/clis/douyin/draft.test.ts +12 -0
  167. package/src/clis/douyin/draft.ts +282 -0
  168. package/src/clis/douyin/drafts.test.ts +12 -0
  169. package/src/clis/douyin/drafts.ts +27 -0
  170. package/src/clis/douyin/hashtag.test.ts +28 -0
  171. package/src/clis/douyin/hashtag.ts +56 -0
  172. package/src/clis/douyin/location.test.ts +26 -0
  173. package/src/clis/douyin/location.ts +27 -0
  174. package/src/clis/douyin/profile.test.ts +12 -0
  175. package/src/clis/douyin/profile.ts +37 -0
  176. package/src/clis/douyin/publish.test.ts +45 -0
  177. package/src/clis/douyin/publish.ts +340 -0
  178. package/src/clis/douyin/stats.test.ts +25 -0
  179. package/src/clis/douyin/stats.ts +30 -0
  180. package/src/clis/douyin/update.test.ts +12 -0
  181. package/src/clis/douyin/update.ts +43 -0
  182. package/src/clis/douyin/videos.test.ts +12 -0
  183. package/src/clis/douyin/videos.ts +49 -0
  184. package/src/clis/hackernews/search.yaml +1 -1
  185. package/src/clis/instagram/search.yaml +2 -1
  186. package/src/clis/linux-do/search.yaml +3 -1
  187. package/src/clis/medium/search.ts +1 -1
  188. package/src/clis/reuters/search.ts +0 -1
  189. package/src/clis/twitter/search.test.ts +69 -2
  190. package/src/clis/twitter/search.ts +5 -3
  191. package/src/clis/weibo/comments.ts +54 -0
  192. package/src/clis/weibo/feed.ts +57 -0
  193. package/src/clis/weibo/hot.ts +0 -1
  194. package/src/clis/weibo/me.ts +77 -0
  195. package/src/clis/weibo/post.ts +77 -0
  196. package/src/clis/weibo/user.ts +64 -0
  197. package/src/clis/weibo/utils.ts +32 -0
  198. package/src/clis/weread/search.ts +3 -2
  199. package/src/clis/xueqiu/search.yaml +2 -1
  200. package/src/clis/yahoo-finance/quote.ts +0 -1
  201. package/src/clis/youtube/channel.ts +155 -0
  202. package/src/clis/youtube/comments.ts +97 -0
  203. package/src/clis/youtube/search.ts +0 -1
  204. package/src/clis/zhihu/search.yaml +2 -1
  205. package/src/external-clis.yaml +0 -17
  206. package/src/weread-search-regression.test.ts +44 -0
  207. package/tests/e2e/browser-public-extended.test.ts +162 -0
  208. package/tests/e2e/browser-public.test.ts +7 -146
  209. package/vitest.config.ts +24 -17
@@ -0,0 +1,150 @@
1
+ /**
2
+ * YouTube channel — get channel info and recent videos via InnerTube API.
3
+ */
4
+ import { cli, Strategy } from '../../registry.js';
5
+ import { CommandExecutionError } from '../../errors.js';
6
+ cli({
7
+ site: 'youtube',
8
+ name: 'channel',
9
+ description: 'Get YouTube channel info and recent videos',
10
+ domain: 'www.youtube.com',
11
+ strategy: Strategy.COOKIE,
12
+ args: [
13
+ { name: 'id', required: true, positional: true, help: 'Channel ID (UCxxxx) or handle (@name)' },
14
+ { name: 'limit', type: 'int', default: 10, help: 'Max recent videos (max 30)' },
15
+ ],
16
+ columns: ['field', 'value'],
17
+ func: async (page, kwargs) => {
18
+ const channelId = String(kwargs.id);
19
+ const limit = Math.min(kwargs.limit || 10, 30);
20
+ await page.goto('https://www.youtube.com');
21
+ await page.wait(2);
22
+ const data = await page.evaluate(`
23
+ (async () => {
24
+ const channelId = ${JSON.stringify(channelId)};
25
+ const limit = ${limit};
26
+ const cfg = window.ytcfg?.data_ || {};
27
+ const apiKey = cfg.INNERTUBE_API_KEY;
28
+ const context = cfg.INNERTUBE_CONTEXT;
29
+ if (!apiKey || !context) return {error: 'YouTube config not found'};
30
+
31
+ // Resolve handle to browseId if needed
32
+ let browseId = channelId;
33
+ if (channelId.startsWith('@')) {
34
+ const resolveResp = await fetch('/youtubei/v1/navigation/resolve_url?key=' + apiKey + '&prettyPrint=false', {
35
+ method: 'POST', credentials: 'include',
36
+ headers: {'Content-Type': 'application/json'},
37
+ body: JSON.stringify({context, url: 'https://www.youtube.com/' + channelId})
38
+ });
39
+ if (resolveResp.ok) {
40
+ const resolveData = await resolveResp.json();
41
+ browseId = resolveData.endpoint?.browseEndpoint?.browseId || channelId;
42
+ }
43
+ }
44
+
45
+ // Fetch channel data
46
+ const resp = await fetch('/youtubei/v1/browse?key=' + apiKey + '&prettyPrint=false', {
47
+ method: 'POST', credentials: 'include',
48
+ headers: {'Content-Type': 'application/json'},
49
+ body: JSON.stringify({context, browseId})
50
+ });
51
+ if (!resp.ok) return {error: 'Channel API returned HTTP ' + resp.status};
52
+ const data = await resp.json();
53
+
54
+ // Channel metadata
55
+ const metadata = data.metadata?.channelMetadataRenderer || {};
56
+ const header = data.header?.pageHeaderRenderer || data.header?.c4TabbedHeaderRenderer || {};
57
+
58
+ // Subscriber count from header
59
+ let subscriberCount = '';
60
+ try {
61
+ const rows = header.content?.pageHeaderViewModel?.metadata?.contentMetadataViewModel?.metadataRows || [];
62
+ for (const row of rows) {
63
+ for (const part of (row.metadataParts || [])) {
64
+ const text = part.text?.content || '';
65
+ if (text.includes('subscriber')) subscriberCount = text;
66
+ }
67
+ }
68
+ } catch {}
69
+ // Fallback for old c4TabbedHeaderRenderer format
70
+ if (!subscriberCount && header.subscriberCountText?.simpleText) {
71
+ subscriberCount = header.subscriberCountText.simpleText;
72
+ }
73
+
74
+ // Extract recent videos from Home tab
75
+ const tabs = data.contents?.twoColumnBrowseResultsRenderer?.tabs || [];
76
+ const homeTab = tabs.find(t => t.tabRenderer?.selected);
77
+ const recentVideos = [];
78
+
79
+ if (homeTab) {
80
+ const sections = homeTab.tabRenderer?.content?.sectionListRenderer?.contents || [];
81
+ for (const section of sections) {
82
+ for (const shelf of (section.itemSectionRenderer?.contents || [])) {
83
+ for (const item of (shelf.shelfRenderer?.content?.horizontalListRenderer?.items || [])) {
84
+ // New lockupViewModel format
85
+ const lvm = item.lockupViewModel;
86
+ if (lvm && lvm.contentType === 'LOCKUP_CONTENT_TYPE_VIDEO' && recentVideos.length < limit) {
87
+ const meta = lvm.metadata?.lockupMetadataViewModel;
88
+ const rows = meta?.metadata?.contentMetadataViewModel?.metadataRows || [];
89
+ const viewsAndTime = (rows[0]?.metadataParts || []).map(p => p.text?.content).filter(Boolean).join(' | ');
90
+ let duration = '';
91
+ for (const ov of (lvm.contentImage?.thumbnailViewModel?.overlays || [])) {
92
+ for (const b of (ov.thumbnailBottomOverlayViewModel?.badges || [])) {
93
+ if (b.thumbnailBadgeViewModel?.text) duration = b.thumbnailBadgeViewModel.text;
94
+ }
95
+ }
96
+ recentVideos.push({
97
+ title: meta?.title?.content || '',
98
+ duration,
99
+ views: viewsAndTime,
100
+ url: 'https://www.youtube.com/watch?v=' + lvm.contentId,
101
+ });
102
+ }
103
+ // Legacy gridVideoRenderer format
104
+ if (item.gridVideoRenderer && recentVideos.length < limit) {
105
+ const v = item.gridVideoRenderer;
106
+ recentVideos.push({
107
+ title: v.title?.runs?.[0]?.text || v.title?.simpleText || '',
108
+ duration: v.thumbnailOverlays?.[0]?.thumbnailOverlayTimeStatusRenderer?.text?.simpleText || '',
109
+ views: (v.shortViewCountText?.simpleText || '') + (v.publishedTimeText?.simpleText ? ' | ' + v.publishedTimeText.simpleText : ''),
110
+ url: 'https://www.youtube.com/watch?v=' + v.videoId,
111
+ });
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ return {
119
+ name: metadata.title || '',
120
+ channelId: metadata.externalId || browseId,
121
+ handle: metadata.vanityChannelUrl?.split('/').pop() || '',
122
+ description: (metadata.description || '').substring(0, 500),
123
+ subscribers: subscriberCount,
124
+ url: metadata.channelUrl || 'https://www.youtube.com/channel/' + browseId,
125
+ keywords: metadata.keywords || '',
126
+ recentVideos,
127
+ };
128
+ })()
129
+ `);
130
+ if (!data || typeof data !== 'object')
131
+ throw new CommandExecutionError('Failed to fetch channel data');
132
+ if (data.error)
133
+ throw new CommandExecutionError(String(data.error));
134
+ const result = data;
135
+ const videos = result.recentVideos;
136
+ delete result.recentVideos;
137
+ // Channel info as field/value pairs + recent videos as table
138
+ const rows = Object.entries(result).map(([field, value]) => ({
139
+ field,
140
+ value: String(value),
141
+ }));
142
+ if (videos && videos.length > 0) {
143
+ rows.push({ field: '---', value: '--- Recent Videos ---' });
144
+ for (const v of videos) {
145
+ rows.push({ field: v.title, value: `${v.duration} | ${v.views} | ${v.url}` });
146
+ }
147
+ }
148
+ return rows;
149
+ },
150
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,95 @@
1
+ /**
2
+ * YouTube comments — get video comments via InnerTube API.
3
+ */
4
+ import { cli, Strategy } from '../../registry.js';
5
+ import { CommandExecutionError } from '../../errors.js';
6
+ import { parseVideoId } from './utils.js';
7
+ cli({
8
+ site: 'youtube',
9
+ name: 'comments',
10
+ description: 'Get YouTube video comments',
11
+ domain: 'www.youtube.com',
12
+ strategy: Strategy.COOKIE,
13
+ args: [
14
+ { name: 'url', required: true, positional: true, help: 'YouTube video URL or video ID' },
15
+ { name: 'limit', type: 'int', default: 20, help: 'Max comments (max 100)' },
16
+ ],
17
+ columns: ['rank', 'author', 'text', 'likes', 'replies', 'time'],
18
+ func: async (page, kwargs) => {
19
+ const videoId = parseVideoId(kwargs.url);
20
+ const limit = Math.min(kwargs.limit || 20, 100);
21
+ await page.goto(`https://www.youtube.com/watch?v=${videoId}`);
22
+ await page.wait(3);
23
+ const data = await page.evaluate(`
24
+ (async () => {
25
+ const videoId = ${JSON.stringify(videoId)};
26
+ const limit = ${limit};
27
+ const cfg = window.ytcfg?.data_ || {};
28
+ const apiKey = cfg.INNERTUBE_API_KEY;
29
+ const context = cfg.INNERTUBE_CONTEXT;
30
+ if (!apiKey || !context) return {error: 'YouTube config not found'};
31
+
32
+ // Step 1: Get comment continuation token
33
+ let continuationToken = null;
34
+
35
+ // Try from current page ytInitialData
36
+ if (window.ytInitialData) {
37
+ const results = window.ytInitialData.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];
38
+ const commentSection = results.find(i => i.itemSectionRenderer?.targetId === 'comments-section');
39
+ continuationToken = commentSection?.itemSectionRenderer?.contents?.[0]?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;
40
+ }
41
+
42
+ // Fallback: fetch via next API
43
+ if (!continuationToken) {
44
+ const nextResp = await fetch('/youtubei/v1/next?key=' + apiKey + '&prettyPrint=false', {
45
+ method: 'POST', credentials: 'include',
46
+ headers: {'Content-Type': 'application/json'},
47
+ body: JSON.stringify({context, videoId})
48
+ });
49
+ if (!nextResp.ok) return {error: 'Failed to get video data: HTTP ' + nextResp.status};
50
+ const nextData = await nextResp.json();
51
+ const results = nextData.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];
52
+ const commentSection = results.find(i => i.itemSectionRenderer?.targetId === 'comments-section');
53
+ continuationToken = commentSection?.itemSectionRenderer?.contents?.[0]?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;
54
+ }
55
+
56
+ if (!continuationToken) return {error: 'No comment section found — comments may be disabled'};
57
+
58
+ // Step 2: Fetch comments
59
+ const commentResp = await fetch('/youtubei/v1/next?key=' + apiKey + '&prettyPrint=false', {
60
+ method: 'POST', credentials: 'include',
61
+ headers: {'Content-Type': 'application/json'},
62
+ body: JSON.stringify({context, continuation: continuationToken})
63
+ });
64
+ if (!commentResp.ok) return {error: 'Failed to fetch comments: HTTP ' + commentResp.status};
65
+ const commentData = await commentResp.json();
66
+
67
+ // Parse from frameworkUpdates (new ViewModel format)
68
+ const mutations = commentData.frameworkUpdates?.entityBatchUpdate?.mutations || [];
69
+ const commentEntities = mutations.filter(m => m.payload?.commentEntityPayload);
70
+
71
+ return commentEntities.slice(0, limit).map((m, i) => {
72
+ const p = m.payload.commentEntityPayload;
73
+ const props = p.properties || {};
74
+ const author = p.author || {};
75
+ const toolbar = p.toolbar || {};
76
+ return {
77
+ rank: i + 1,
78
+ author: author.displayName || '',
79
+ text: (props.content?.content || '').substring(0, 300),
80
+ likes: toolbar.likeCountNotliked || '0',
81
+ replies: toolbar.replyCount || '0',
82
+ time: props.publishedTime || '',
83
+ };
84
+ });
85
+ })()
86
+ `);
87
+ if (!Array.isArray(data)) {
88
+ const errMsg = data && typeof data === 'object' ? String(data.error || '') : '';
89
+ if (errMsg)
90
+ throw new CommandExecutionError(errMsg);
91
+ return [];
92
+ }
93
+ return data;
94
+ },
95
+ });
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * YouTube search — innertube API via browser session.
3
- * Source: bb-sites/youtube/search.js
4
3
  */
5
4
  import { cli, Strategy } from '../../registry.js';
6
5
  cli({
@@ -52,7 +52,8 @@ pipeline:
52
52
  type: ${{ item.type }}
53
53
  author: ${{ item.author }}
54
54
  votes: ${{ item.votes }}
55
+ url: ${{ item.url }}
55
56
 
56
57
  - limit: ${{ args.limit }}
57
58
 
58
- columns: [rank, title, type, author, votes]
59
+ columns: [rank, title, type, author, votes, url]
@@ -14,14 +14,6 @@
14
14
  install:
15
15
  mac: "brew install --cask obsidian"
16
16
 
17
- - name: readwise
18
- binary: readwise
19
- description: "Readwise & Reader CLI — highlights, annotations, reading list"
20
- homepage: "https://github.com/readwiseio/readwise-cli"
21
- tags: [reading, highlights]
22
- install:
23
- default: "npm install -g @readwiseio/readwise-cli"
24
-
25
17
  - name: docker
26
18
  binary: docker
27
19
  description: "Docker command-line interface"
@@ -29,12 +21,3 @@
29
21
  tags: [docker, containers, devops]
30
22
  install:
31
23
  mac: "brew install --cask docker"
32
-
33
- - name: gws
34
- binary: gws
35
- description: "Google Workspace CLI — Docs, Sheets, Drive, Gmail, Calendar"
36
- homepage: "https://github.com/nicholasgasior/gws"
37
- tags: [google, docs, sheets, drive, workspace]
38
- install:
39
- mac: "brew install gws"
40
- default: "npm install -g @nicholasgasior/gws"
@@ -0,0 +1 @@
1
+ import './clis/weread/search.js';
@@ -0,0 +1,39 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from './registry.js';
3
+ import './clis/weread/search.js';
4
+ describe('weread/search regression', () => {
5
+ beforeEach(() => {
6
+ vi.restoreAllMocks();
7
+ });
8
+ it('uses the query argument for the search API and returns urls', async () => {
9
+ const command = getRegistry().get('weread/search');
10
+ expect(command?.func).toBeTypeOf('function');
11
+ const fetchMock = vi.fn().mockResolvedValue({
12
+ ok: true,
13
+ json: () => Promise.resolve({
14
+ books: [
15
+ {
16
+ bookInfo: {
17
+ title: 'Deep Work',
18
+ author: 'Cal Newport',
19
+ bookId: 'abc123',
20
+ },
21
+ },
22
+ ],
23
+ }),
24
+ });
25
+ vi.stubGlobal('fetch', fetchMock);
26
+ const result = await command.func(null, { query: 'deep work', limit: 5 });
27
+ expect(fetchMock).toHaveBeenCalledTimes(1);
28
+ expect(String(fetchMock.mock.calls[0][0])).toContain('keyword=deep+work');
29
+ expect(result).toEqual([
30
+ {
31
+ rank: 1,
32
+ title: 'Deep Work',
33
+ author: 'Cal Newport',
34
+ bookId: 'abc123',
35
+ url: 'https://weread.qq.com/web/bookDetail/abc123',
36
+ },
37
+ ]);
38
+ });
39
+ });
@@ -74,6 +74,16 @@ export default defineConfig({
74
74
  { text: 'Sina Blog', link: '/adapters/browser/sinablog' },
75
75
  { text: 'Substack', link: '/adapters/browser/substack' },
76
76
  { text: 'Pixiv', link: '/adapters/browser/pixiv' },
77
+ { text: 'Douban', link: '/adapters/browser/douban' },
78
+ { text: 'Doubao', link: '/adapters/browser/doubao' },
79
+ { text: 'Facebook', link: '/adapters/browser/facebook' },
80
+ { text: 'Google', link: '/adapters/browser/google' },
81
+ { text: 'Instagram', link: '/adapters/browser/instagram' },
82
+ { text: 'JD.com', link: '/adapters/browser/jd' },
83
+ { text: 'Medium', link: '/adapters/browser/medium' },
84
+ { text: 'TikTok', link: '/adapters/browser/tiktok' },
85
+ { text: 'Web (Generic)', link: '/adapters/browser/web' },
86
+ { text: 'Weixin', link: '/adapters/browser/weixin' },
77
87
  ],
78
88
  },
79
89
  {
@@ -93,6 +103,8 @@ export default defineConfig({
93
103
  { text: 'Sina Finance', link: '/adapters/browser/sinafinance' },
94
104
  { text: 'Stack Overflow', link: '/adapters/browser/stackoverflow' },
95
105
  { text: 'Wikipedia', link: '/adapters/browser/wikipedia' },
106
+ { text: 'Lobsters', link: '/adapters/browser/lobsters' },
107
+ { text: 'Steam', link: '/adapters/browser/steam' },
96
108
  ],
97
109
  },
98
110
  {
@@ -106,6 +118,7 @@ export default defineConfig({
106
118
  { text: 'ChatWise', link: '/adapters/desktop/chatwise' },
107
119
  { text: 'Notion', link: '/adapters/desktop/notion' },
108
120
  { text: 'Discord', link: '/adapters/desktop/discord' },
121
+ { text: 'Doubao App', link: '/adapters/desktop/doubao-app' },
109
122
  ],
110
123
  },
111
124
  ],
@@ -0,0 +1,75 @@
1
+ # Douyin (抖音创作者中心)
2
+
3
+ **Mode**: 🔐 Browser · **Domain**: `creator.douyin.com`
4
+
5
+ ## Commands
6
+
7
+ | Command | Description |
8
+ |---------|-------------|
9
+ | `opencli douyin profile` | 获取账号信息 |
10
+ | `opencli douyin videos` | 获取作品列表 |
11
+ | `opencli douyin drafts` | 获取草稿列表 |
12
+ | `opencli douyin draft` | 上传视频并保存为草稿 |
13
+ | `opencli douyin publish` | 定时发布视频到抖音 |
14
+ | `opencli douyin update` | 更新视频信息 |
15
+ | `opencli douyin delete` | 删除作品 |
16
+ | `opencli douyin stats` | 查询作品数据分析 |
17
+ | `opencli douyin collections` | 获取合集列表 |
18
+ | `opencli douyin activities` | 获取官方活动列表 |
19
+ | `opencli douyin location` | 搜索发布可用的地理位置 |
20
+ | `opencli douyin hashtag search` | 按关键词搜索话题 |
21
+ | `opencli douyin hashtag suggest` | 基于封面 URI 推荐话题 |
22
+ | `opencli douyin hashtag hot` | 获取热点词 |
23
+
24
+ ## Usage Examples
25
+
26
+ ```bash
27
+ # 账号与作品
28
+ opencli douyin profile
29
+ opencli douyin videos --limit 10
30
+ opencli douyin videos --status scheduled
31
+ opencli douyin drafts
32
+
33
+ # 发布前辅助信息
34
+ opencli douyin collections
35
+ opencli douyin activities
36
+ opencli douyin location "东京塔"
37
+ opencli douyin hashtag search "春游"
38
+ opencli douyin hashtag hot --limit 10
39
+
40
+ # 保存草稿
41
+ opencli douyin draft ./video.mp4 \
42
+ --title "春游 vlog" \
43
+ --caption "#春游 先存草稿"
44
+
45
+ # 定时发布
46
+ opencli douyin publish ./video.mp4 \
47
+ --title "春游 vlog" \
48
+ --caption "#春游 今天去看樱花" \
49
+ --schedule "2026-04-08T12:00:00+09:00"
50
+
51
+ # 也支持 Unix 秒字符串
52
+ opencli douyin publish ./video.mp4 \
53
+ --title "春游 vlog" \
54
+ --schedule 1775617200
55
+
56
+ # 更新与删除
57
+ opencli douyin update 1234567890 --caption "更新后的文案"
58
+ opencli douyin update 1234567890 --reschedule "2026-04-09T20:00:00+09:00"
59
+ opencli douyin delete 1234567890
60
+
61
+ # JSON 输出
62
+ opencli douyin profile -f json
63
+ ```
64
+
65
+ ## Prerequisites
66
+
67
+ - Chrome running and **logged into** `creator.douyin.com`
68
+ - The logged-in account must have access to Douyin Creator Center publishing features
69
+ - [Browser Bridge extension](/guide/browser-bridge) installed
70
+
71
+ ## Notes
72
+
73
+ - `publish` requires `--schedule` to be at least 2 hours later and no more than 14 days later
74
+ - `draft` and `publish` upload the video through Douyin/ByteDance browser-authenticated APIs, so cookies in the active browser session must be valid
75
+ - `hashtag suggest` expects a valid `cover`/`cover_uri` value produced during the publish pipeline; for normal manual use, `hashtag search` and `hashtag hot` are usually more convenient
@@ -37,6 +37,12 @@
37
37
  # Quick start
38
38
  opencli twitter trending --limit 5
39
39
 
40
+ # Search top tweets (default)
41
+ opencli twitter search "react 19"
42
+
43
+ # Search latest/live tweets
44
+ opencli twitter search "react 19" --filter live
45
+
40
46
  # JSON output
41
47
  opencli twitter trending -f json
42
48
 
@@ -11,7 +11,7 @@ Run `opencli list` for the live registry.
11
11
  | **[bilibili](/adapters/browser/bilibili)** | `hot` `search` `me` `favorite` `history` `feed` `subtitle` `dynamic` `ranking` `following` `user-videos` `download` | 🔐 Browser |
12
12
  | **[zhihu](/adapters/browser/zhihu)** | `hot` `search` `question` `download` | 🔐 Browser |
13
13
  | **[xiaohongshu](/adapters/browser/xiaohongshu)** | `search` `notifications` `feed` `user` `download` `publish` `creator-notes` `creator-note-detail` `creator-notes-summary` `creator-profile` `creator-stats` | 🔐 Browser |
14
- | **[xueqiu](/adapters/browser/xueqiu)** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` `earnings-date` | 🔐 Browser |
14
+ | **[xueqiu](/adapters/browser/xueqiu)** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` `earnings-date` `fund-holdings` `fund-snapshot` | 🔐 Browser |
15
15
  | **[youtube](/adapters/browser/youtube)** | `search` `video` `transcript` | 🔐 Browser |
16
16
  | **[v2ex](/adapters/browser/v2ex)** | `hot` `latest` `topic` `node` `user` `member` `replies` `nodes` `daily` `me` `notifications` | 🌐 / 🔐 |
17
17
  | **[bloomberg](/adapters/browser/bloomberg)** | `main` `markets` `economics` `industries` `tech` `politics` `businessweek` `opinions` `feeds` `news` | 🌐 / 🔐 |
@@ -38,6 +38,10 @@ Run `opencli list` for the live registry.
38
38
  | **[substack](/adapters/browser/substack)** | `feed` `search` `publication` | 🔐 Browser |
39
39
  | **[pixiv](/adapters/browser/pixiv)** | `ranking` `search` `user` `illusts` `detail` `download` | 🔐 Browser |
40
40
  | **[tiktok](/adapters/browser/tiktok)** | `explore` `search` `profile` `user` `following` `follow` `unfollow` `like` `unlike` `comment` `save` `unsave` `live` `notifications` `friends` | 🔐 Browser |
41
+ | **[google](/adapters/browser/google)** | `news` `search` `suggest` `trends` | 🌐 / 🔐 |
42
+ | **[jd](/adapters/browser/jd)** | `item` | 🔐 Browser |
43
+ | **[web](/adapters/browser/web)** | `read` | 🔐 Browser |
44
+ | **[weixin](/adapters/browser/weixin)** | `download` | 🔐 Browser |
41
45
 
42
46
  ## Public API Adapters
43
47
 
@@ -57,6 +61,7 @@ Run `opencli list` for the live registry.
57
61
  | **[stackoverflow](/adapters/browser/stackoverflow)** | `hot` `search` `bounties` `unanswered` | 🌐 Public |
58
62
  | **[wikipedia](/adapters/browser/wikipedia)** | `search` `summary` `random` `trending` | 🌐 Public |
59
63
  | **[lobsters](/adapters/browser/lobsters)** | `hot` `newest` `active` `tag` | 🌐 Public |
64
+ | **[steam](/adapters/browser/steam)** | `top-sellers` | 🌐 Public |
60
65
 
61
66
  ## Desktop Adapters
62
67