@skills-store/rednote 0.1.0

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.
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env -S node --experimental-strip-types
2
+
3
+ import { runCli } from './utils/browser-cli.ts';
4
+
5
+ function printRootHelp() {
6
+ process.stdout.write(`rednote
7
+
8
+ Usage:
9
+ npx -y @skills-store/rednote browser <command> [...args]
10
+ npx -y @skills-store/rednote <command> [...args]
11
+
12
+ Commands:
13
+ browser <list|create|remove|connect>
14
+ env [--format md|json]
15
+ status [--instance NAME]
16
+ check-login [--instance NAME]
17
+ login [--instance NAME]
18
+ home [--instance NAME] [--format md|json] [--save [PATH]]
19
+ search [--instance NAME] --keyword TEXT [--format md|json] [--save [PATH]]
20
+ get-feed-detail [--instance NAME] --url URL [--format md|json]
21
+ get-profile [--instance NAME] --id USER_ID [--format md|json]
22
+
23
+ Examples:
24
+ npx -y @skills-store/rednote browser list
25
+ npx -y @skills-store/rednote browser create --name seller-main --browser chrome
26
+ npx -y @skills-store/rednote browser connect --instance seller-main
27
+ npx -y @skills-store/rednote env
28
+ npx -y @skills-store/rednote search --instance seller-main --keyword 护肤
29
+ `);
30
+ }
31
+
32
+ export async function runRootCli(argv: string[] = process.argv.slice(2)) {
33
+ const [command, ...restArgv] = argv;
34
+
35
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
36
+ printRootHelp();
37
+ return;
38
+ }
39
+
40
+ if (command === 'browser') {
41
+ const { runBrowserCli } = await import('./browser/index.ts');
42
+ await runBrowserCli(restArgv);
43
+ return;
44
+ }
45
+
46
+ const { runRednoteCli } = await import('./rednote/index.ts');
47
+ await runRednoteCli(argv);
48
+ }
49
+
50
+ async function main() {
51
+ await runRootCli();
52
+ }
53
+
54
+ runCli(import.meta.url, main);
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env -S node --experimental-strip-types
2
+
3
+ import { parseArgs } from 'node:util';
4
+ import type { Browser, BrowserContext, Page } from 'playwright-core';
5
+ import { initBrowser, resolveConnectOptions } from '../browser/connect-browser.ts';
6
+ import { connectOverCdp, updateLastConnect } from '../utils/browser-core.ts';
7
+ import { debugLog, printJson, runCli } from '../utils/browser-cli.ts';
8
+ import { resolveStatusTarget, type RednoteAccountStatus, type RednoteStatusTarget } from './status.ts';
9
+
10
+ export type CheckLoginCliValues = {
11
+ instance?: string;
12
+ help?: boolean;
13
+ };
14
+
15
+ export type CheckLoginResult = {
16
+ ok: true;
17
+ rednote: RednoteAccountStatus & {
18
+ needLogin: boolean;
19
+ checkedAt: string;
20
+ };
21
+ };
22
+
23
+ export type RednoteSession = {
24
+ browser: Browser;
25
+ browserContext: BrowserContext;
26
+ page: Page;
27
+ };
28
+
29
+ function printCheckLoginHelp() {
30
+ process.stdout.write(`rednote check-login
31
+
32
+ Usage:
33
+ npx -y @skills-store/rednote check-login [--instance NAME]
34
+ node --experimental-strip-types ./scripts/rednote/checkLogin.ts --instance NAME
35
+ bun ./scripts/rednote/checkLogin.ts --instance NAME
36
+
37
+ Options:
38
+ --instance NAME Optional. Defaults to the saved lastConnect instance
39
+ -h, --help Show this help
40
+ `);
41
+ }
42
+
43
+ export async function createRednoteSession(target: RednoteStatusTarget): Promise<RednoteSession> {
44
+ debugLog('checkLogin', 'create session start', { target });
45
+ const resolved = await resolveConnectOptions(
46
+ target.scope === 'custom'
47
+ ? { instanceName: target.instanceName }
48
+ : { browser: target.browser },
49
+ );
50
+ const launched = await initBrowser(resolved.connectOptions);
51
+ debugLog('checkLogin', 'initBrowser resolved', {
52
+ target,
53
+ connectOptions: resolved.connectOptions,
54
+ launched,
55
+ });
56
+
57
+ if (resolved.lastConnect) {
58
+ updateLastConnect(resolved.lastConnect);
59
+ }
60
+
61
+ const browser = await connectOverCdp(launched.wsUrl);
62
+ debugLog('checkLogin', 'connected over cdp', { wsUrl: launched.wsUrl, remoteDebuggingPort: launched.remoteDebuggingPort });
63
+ const browserContext = browser.contexts()[0];
64
+
65
+ if (!browserContext) {
66
+ throw new Error(`No browser context found for instance: ${target.instanceName}`);
67
+ }
68
+
69
+ const page = browserContext.pages().find((candidate) => candidate.url().startsWith('https://www.xiaohongshu.com/')) ?? await browserContext.newPage();
70
+ debugLog('checkLogin', 'session page resolved', { pageUrl: page.url(), totalPages: browserContext.pages().length });
71
+
72
+ return {
73
+ browser,
74
+ browserContext,
75
+ page,
76
+ };
77
+ }
78
+
79
+ export function disconnectRednoteSession(session: RednoteSession) {
80
+ try {
81
+ (session.browser as Browser & { _connection?: { close: () => void } })._connection?.close();
82
+ } catch {
83
+ }
84
+ }
85
+
86
+ export async function checkRednoteLogin(
87
+ target: RednoteStatusTarget,
88
+ session?: RednoteSession,
89
+ ): Promise<RednoteAccountStatus & {
90
+ needLogin: boolean;
91
+ checkedAt: string;
92
+ }> {
93
+ const ownsSession = !session;
94
+ const activeSession = session ?? await createRednoteSession(target);
95
+
96
+ try {
97
+ debugLog('checkLogin', 'check login start', { target, reusedSession: Boolean(session) });
98
+ const page = activeSession.page;
99
+
100
+ if (!page.url().startsWith('https://www.xiaohongshu.com/')) {
101
+ debugLog('checkLogin', 'page is not on xiaohongshu, navigating', { currentUrl: page.url() });
102
+ await page.goto('https://www.xiaohongshu.com/explore', {
103
+ waitUntil: 'domcontentloaded',
104
+ });
105
+ }
106
+
107
+ await page.waitForTimeout(2_000);
108
+ const needLogin = (await page.locator('#login-btn').count()) > 0;
109
+ debugLog('checkLogin', 'login state checked', { pageUrl: page.url(), needLogin });
110
+
111
+ return {
112
+ loginStatus: needLogin ? 'logged-out' : 'logged-in',
113
+ lastLoginAt: null,
114
+ needLogin,
115
+ checkedAt: new Date().toISOString(),
116
+ };
117
+ } finally {
118
+ if (ownsSession) {
119
+ disconnectRednoteSession(activeSession);
120
+ }
121
+ }
122
+ }
123
+
124
+ export async function ensureRednoteLoggedIn(target: RednoteStatusTarget, action = 'continue', session?: RednoteSession) {
125
+ const rednote = await checkRednoteLogin(target, session);
126
+
127
+ if (rednote.needLogin) {
128
+ throw new Error(`Xiaohongshu login is required before ${action}. Run \`rednote login --instance ${target.instanceName}\` first.`);
129
+ }
130
+
131
+ return rednote;
132
+ }
133
+
134
+ export async function runCheckLoginCommand(values: CheckLoginCliValues = {}) {
135
+ if (values.help) {
136
+ printCheckLoginHelp();
137
+ return;
138
+ }
139
+
140
+
141
+ const target = resolveStatusTarget(values.instance);
142
+ const rednote = await checkRednoteLogin(target);
143
+
144
+ const result: CheckLoginResult = {
145
+ ok: true,
146
+ rednote,
147
+ };
148
+
149
+ printJson(result);
150
+ }
151
+
152
+ async function main() {
153
+ const { values } = parseArgs({
154
+ args: process.argv.slice(2),
155
+ allowPositionals: true,
156
+ strict: false,
157
+ options: {
158
+ instance: { type: 'string' },
159
+ help: { type: 'boolean', short: 'h' },
160
+ },
161
+ });
162
+
163
+ if (values.help) {
164
+ printCheckLoginHelp();
165
+ return;
166
+ }
167
+
168
+ await runCheckLoginCommand(values);
169
+ }
170
+
171
+ runCli(import.meta.url, main);
File without changes
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env -S node --experimental-strip-types
2
+
3
+ import { parseArgs } from 'node:util';
4
+ import { printJson, runCli } from '../utils/browser-cli.ts';
5
+ import { getRednoteEnvironmentInfo } from '../utils/browser-core.ts';
6
+
7
+ export type EnvCliValues = {
8
+ format?: 'md' | 'json';
9
+ help?: boolean;
10
+ };
11
+
12
+ function printEnvHelp() {
13
+ process.stdout.write(`rednote env
14
+
15
+ Usage:
16
+ npx -y @skills-store/rednote env [--format md|json]
17
+ node --experimental-strip-types ./scripts/rednote/env.ts [--format md|json]
18
+ bun ./scripts/rednote/env.ts [--format md|json]
19
+
20
+ Options:
21
+ --format FORMAT Output format: md | json. Default: md
22
+ -h, --help Show this help
23
+ `);
24
+ }
25
+
26
+ function renderEnvironmentMarkdown() {
27
+ const info = getRednoteEnvironmentInfo();
28
+ return [
29
+ '## Environment',
30
+ '',
31
+ `- Platform: ${info.platform}`,
32
+ `- Node: ${info.nodeVersion}`,
33
+ `- Home: ${info.homeDir}`,
34
+ `- Package Root: ${info.packageRoot}`,
35
+ `- Storage Home: ${info.storageHome}`,
36
+ `- Storage Root: ${info.storageRoot}`,
37
+ `- Instances Dir: ${info.instancesDir}`,
38
+ `- Instance Store: ${info.instanceStorePath}`,
39
+ `- Legacy Package Instances: ${info.legacyPackageInstancesDir}`,
40
+ '',
41
+ 'Custom browser instances and metadata are stored under `~/.skills-router/rednote/instances`.',
42
+ '',
43
+ ].join('\n');
44
+ }
45
+
46
+ export async function runEnvCommand(values: EnvCliValues = {}) {
47
+ if (values.help) {
48
+ printEnvHelp();
49
+ return;
50
+ }
51
+
52
+ const format = values.format ?? 'md';
53
+ if (format === 'json') {
54
+ printJson(getRednoteEnvironmentInfo());
55
+ return;
56
+ }
57
+
58
+ process.stdout.write(renderEnvironmentMarkdown());
59
+ }
60
+
61
+ async function main() {
62
+ const { values } = parseArgs({
63
+ args: process.argv.slice(2),
64
+ allowPositionals: true,
65
+ strict: false,
66
+ options: {
67
+ format: { type: 'string' },
68
+ help: { type: 'boolean', short: 'h' },
69
+ },
70
+ });
71
+
72
+ if (values.format && values.format !== 'md' && values.format !== 'json') {
73
+ throw new Error(`Invalid --format value: ${String(values.format)}`);
74
+ }
75
+
76
+ await runEnvCommand(values as EnvCliValues);
77
+ }
78
+
79
+ runCli(import.meta.url, main);
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env -S node --experimental-strip-types
2
+
3
+ import * as cheerio from 'cheerio';
4
+ import { parseArgs } from 'node:util';
5
+ import vm from 'node:vm';
6
+ import type { Page, Response } from 'playwright-core';
7
+ import { printJson, runCli } from '../utils/browser-cli.ts';
8
+ import { resolveStatusTarget } from './status.ts';
9
+ import { createRednoteSession, disconnectRednoteSession, ensureRednoteLoggedIn, type RednoteSession } from './checkLogin.ts';
10
+
11
+ export type FeedDetailFormat = 'json' | 'md';
12
+
13
+ export type FeedDetailCliValues = {
14
+ instance?: string;
15
+ urls: string[];
16
+ format: FeedDetailFormat;
17
+ help?: boolean;
18
+ };
19
+
20
+ export type RednoteComment = {
21
+ id: string | null;
22
+ content: string | null;
23
+ userId: string | null;
24
+ nickname: string | null;
25
+ likedCount: string | null;
26
+ subCommentCount: number | null;
27
+ raw: unknown;
28
+ };
29
+
30
+ export type RednoteDetailNote = {
31
+ noteId: string | null;
32
+ title: string | null;
33
+ desc: string | null;
34
+ type: string | null;
35
+ interactInfo: {
36
+ likedCount: string | null;
37
+ commentCount: string | null;
38
+ collectedCount: string | null;
39
+ shareCount: string | null;
40
+ };
41
+ tagList: Array<{
42
+ name: string | null;
43
+ }>;
44
+ imageList: Array<{
45
+ urlDefault: string | null;
46
+ urlPre: string | null;
47
+ width: number | null;
48
+ height: number | null;
49
+ }>;
50
+ video: {
51
+ url: string | null;
52
+ raw: unknown;
53
+ } | null;
54
+ raw: unknown;
55
+ };
56
+
57
+ export type RednoteFeedDetailItem = {
58
+ url: string;
59
+ note: RednoteDetailNote;
60
+ comments: RednoteComment[];
61
+ };
62
+
63
+ export type FeedDetailResult = {
64
+ ok: true;
65
+ detail: {
66
+ fetchedAt: string;
67
+ total: number;
68
+ items: RednoteFeedDetailItem[];
69
+ };
70
+ };
71
+
72
+ function printGetFeedDetailHelp() {
73
+ process.stdout.write(`rednote get-feed-detail
74
+
75
+ Usage:
76
+ npx -y @skills-store/rednote get-feed-detail [--instance NAME] --url URL [--url URL] [--format md|json]
77
+ node --experimental-strip-types ./scripts/rednote/getFeedDetail.ts --instance NAME --url URL [--url URL] [--format md|json]
78
+ bun ./scripts/rednote/getFeedDetail.ts --instance NAME --url URL [--url URL] [--format md|json]
79
+
80
+ Options:
81
+ --instance NAME Optional. Defaults to the saved lastConnect instance
82
+ --url URL Required. Xiaohongshu explore url, repeatable
83
+ --format FORMAT Output format: md | json. Default: md
84
+ -h, --help Show this help
85
+ `);
86
+ }
87
+
88
+ export function parseGetFeedDetailCliArgs(argv: string[]): FeedDetailCliValues {
89
+ const { values, positionals } = parseArgs({
90
+ args: argv,
91
+ allowPositionals: true,
92
+ strict: false,
93
+ options: {
94
+ instance: { type: 'string' },
95
+ url: { type: 'string', multiple: true },
96
+ format: { type: 'string' },
97
+ help: { type: 'boolean', short: 'h' },
98
+ },
99
+ });
100
+
101
+ if (positionals.length > 0) {
102
+ throw new Error(`Unexpected positional arguments: ${positionals.join(' ')}`);
103
+ }
104
+
105
+ const format = values.format ?? 'md';
106
+ if (format !== 'md' && format !== 'json') {
107
+ throw new Error(`Invalid --format value: ${String(format)}`);
108
+ }
109
+
110
+ return {
111
+ instance: values.instance,
112
+ urls: values.url ?? [],
113
+ format,
114
+ help: values.help,
115
+ };
116
+ }
117
+
118
+ function validateFeedDetailUrl(url: string) {
119
+ try {
120
+ const parsed = new URL(url);
121
+ if (!parsed.href.startsWith('https://www.xiaohongshu.com/explore/')) {
122
+ throw new Error(`url is not valid: ${url},must start with "https://www.xiaohongshu.com/explore/"`);
123
+ }
124
+ if (!parsed.searchParams.get('xsec_token')) {
125
+ throw new Error(`url is not valid: ${url},must include "xsec_token="`);
126
+ }
127
+ } catch (error) {
128
+ if (error instanceof TypeError) {
129
+ throw new Error(`url is not valid: ${url}`);
130
+ }
131
+ throw error;
132
+ }
133
+ }
134
+
135
+ async function getOrCreateXiaohongshuPage(session: RednoteSession) {
136
+ return session.page;
137
+ }
138
+
139
+ function extractVideoUrl(note: any) {
140
+ const streams = Object.values(note?.video?.media?.stream ?? {}) as any[];
141
+ const firstAvailable = streams.find((items) => Array.isArray(items) && items.length > 0);
142
+ return firstAvailable?.[0]?.backupUrls?.[0] ?? null;
143
+ }
144
+
145
+ function normalizeDetailNote(note: any): RednoteDetailNote {
146
+ return {
147
+ noteId: note?.noteId ?? null,
148
+ title: note?.title ?? null,
149
+ desc: note?.desc ?? null,
150
+ type: note?.type ?? null,
151
+ interactInfo: {
152
+ likedCount: note?.interactInfo?.likedCount ?? null,
153
+ commentCount: note?.interactInfo?.commentCount ?? null,
154
+ collectedCount: note?.interactInfo?.collectedCount ?? null,
155
+ shareCount: note?.interactInfo?.shareCount ?? null,
156
+ },
157
+ tagList: Array.isArray(note?.tagList)
158
+ ? note.tagList.map((tag: any) => ({ name: tag?.name ?? null }))
159
+ : [],
160
+ imageList: Array.isArray(note?.imageList)
161
+ ? note.imageList.map((image: any) => ({
162
+ urlDefault: image?.urlDefault ?? null,
163
+ urlPre: image?.urlPre ?? null,
164
+ width: image?.width ?? null,
165
+ height: image?.height ?? null,
166
+ }))
167
+ : [],
168
+ video: note?.video
169
+ ? {
170
+ url: extractVideoUrl(note),
171
+ raw: note.video,
172
+ }
173
+ : null,
174
+ raw: note,
175
+ };
176
+ }
177
+
178
+ function normalizeComments(comments: any[]): RednoteComment[] {
179
+ return comments.map((comment) => ({
180
+ id: comment?.id ?? comment?.commentId ?? null,
181
+ content: comment?.content ?? null,
182
+ userId: comment?.userInfo?.userId ?? null,
183
+ nickname: comment?.userInfo?.nickname ?? null,
184
+ likedCount: comment?.interactInfo?.likedCount ?? null,
185
+ subCommentCount: typeof comment?.subCommentCount === 'number' ? comment.subCommentCount : null,
186
+ raw: comment,
187
+ }));
188
+ }
189
+
190
+ function renderDetailMarkdown(items: RednoteFeedDetailItem[]) {
191
+ if (items.length === 0) {
192
+ return '没有获取到帖子详情。\n';
193
+ }
194
+
195
+ return `${items.map((item) => {
196
+ const lines: string[] = ['<note>'];
197
+ lines.push(`### Url: ${item.url}`);
198
+ lines.push(`### 标题:${item.note.title ?? ''}`);
199
+ lines.push(`### 内容\n${item.note.desc ?? ''}`);
200
+
201
+ if (item.note.interactInfo.likedCount) {
202
+ lines.push(`### 点赞: ${item.note.interactInfo.likedCount}`);
203
+ }
204
+ if (item.note.interactInfo.commentCount) {
205
+ lines.push(`### 评论: ${item.note.interactInfo.commentCount}`);
206
+ }
207
+ if (item.note.interactInfo.collectedCount) {
208
+ lines.push(`### 收藏: ${item.note.interactInfo.collectedCount}`);
209
+ }
210
+ if (item.note.interactInfo.shareCount) {
211
+ lines.push(`### 分享: ${item.note.interactInfo.shareCount}`);
212
+ }
213
+ if (item.note.tagList.length > 0) {
214
+ lines.push(`### 标签: ${item.note.tagList.map((tag) => tag.name ? `#${tag.name}` : '').filter(Boolean).join(' ')}`);
215
+ }
216
+ if (item.note.imageList.length > 0) {
217
+ lines.push(`### 图片\n${item.note.imageList.map((image) => image.urlDefault ? `![](${image.urlDefault})` : '').filter(Boolean).join('\n')}`);
218
+ }
219
+ if (item.note.video?.url) {
220
+ lines.push(`### 视频\n[](${item.note.video.url})`);
221
+ }
222
+ if (item.comments.length > 0) {
223
+ lines.push('### 评论:');
224
+ for (const comment of item.comments) {
225
+ lines.push(`- ${comment.content ?? ''}`);
226
+ }
227
+ }
228
+
229
+ lines.push('</note>');
230
+ return lines.join('\n');
231
+ }).join('\n\n---\n\n')}\n`;
232
+ }
233
+
234
+ async function captureFeedDetail(page: Page, targetUrl: string): Promise<RednoteFeedDetailItem> {
235
+ let note: any = null;
236
+ let comments: any[] | null = null;
237
+
238
+ const handleResponse = async (response: Response) => {
239
+ try {
240
+ const url = new URL(response.url());
241
+ if (response.status() !== 200) {
242
+ return;
243
+ }
244
+
245
+ if (url.href.includes('/explore/')) {
246
+ const html = await response.text();
247
+ const $ = cheerio.load(html);
248
+
249
+ $('script').each((_, element) => {
250
+ const scriptContent = $(element).html();
251
+ if (!scriptContent?.includes('window.__INITIAL_STATE__')) {
252
+ return;
253
+ }
254
+
255
+ const scriptText = scriptContent.substring(scriptContent.indexOf('=') + 1);
256
+ const sandbox: { info?: any } = {};
257
+ vm.createContext(sandbox);
258
+ vm.runInContext(`var info = ${scriptText}`, sandbox);
259
+ const noteState = sandbox.info?.note;
260
+ if (noteState?.noteDetailMap && noteState?.currentNoteId) {
261
+ note = noteState.noteDetailMap[noteState.currentNoteId]?.note ?? note;
262
+ }
263
+ });
264
+ } else if (url.href.includes('comment/page?')) {
265
+ const data = await response.json() as { data?: { comments?: any[] } };
266
+ comments = Array.isArray(data?.data?.comments) ? data.data.comments : [];
267
+ }
268
+ } catch {
269
+ }
270
+ };
271
+
272
+ page.on('response', handleResponse);
273
+ try {
274
+ await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
275
+
276
+ const deadline = Date.now() + 15_000;
277
+ while (Date.now() < deadline) {
278
+ if (note && comments !== null) {
279
+ break;
280
+ }
281
+ await page.waitForTimeout(200);
282
+ }
283
+
284
+ if (!note) {
285
+ throw new Error(`Failed to capture note detail: ${targetUrl}`);
286
+ }
287
+
288
+ return {
289
+ url: targetUrl,
290
+ note: normalizeDetailNote(note),
291
+ comments: normalizeComments(comments ?? []),
292
+ };
293
+ } finally {
294
+ page.off('response', handleResponse);
295
+ }
296
+ }
297
+
298
+ export async function getFeedDetails(session: RednoteSession, urls: string[]): Promise<FeedDetailResult> {
299
+ const page = await getOrCreateXiaohongshuPage(session);
300
+ const items: RednoteFeedDetailItem[] = [];
301
+ for (const url of urls) {
302
+ validateFeedDetailUrl(url);
303
+ items.push(await captureFeedDetail(page, url));
304
+ }
305
+
306
+ return {
307
+ ok: true,
308
+ detail: {
309
+ fetchedAt: new Date().toISOString(),
310
+ total: items.length,
311
+ items,
312
+ },
313
+ };
314
+ }
315
+
316
+ function writeFeedDetailOutput(result: FeedDetailResult, format: FeedDetailFormat) {
317
+ if (format === 'json') {
318
+ printJson(result);
319
+ return;
320
+ }
321
+
322
+ process.stdout.write(renderDetailMarkdown(result.detail.items));
323
+ }
324
+
325
+ export async function runGetFeedDetailCommand(values: FeedDetailCliValues = { urls: [], format: 'md' }) {
326
+ if (values.help) {
327
+ printGetFeedDetailHelp();
328
+ return;
329
+ }
330
+ if (values.urls.length === 0) {
331
+ throw new Error('Missing required option: --url');
332
+ }
333
+
334
+ const target = resolveStatusTarget(values.instance);
335
+ const session = await createRednoteSession(target);
336
+
337
+ try {
338
+ await ensureRednoteLoggedIn(target, 'fetching feed detail', session);
339
+ const result = await getFeedDetails(session, values.urls);
340
+ writeFeedDetailOutput(result, values.format);
341
+ } finally {
342
+ disconnectRednoteSession(session);
343
+ }
344
+ }
345
+
346
+ async function main() {
347
+ const values = parseGetFeedDetailCliArgs(process.argv.slice(2));
348
+ await runGetFeedDetailCommand(values);
349
+ }
350
+
351
+ runCli(import.meta.url, main);