@skills-store/rednote 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -333,6 +333,7 @@ Use these shapes as the success model when a command returns JSON.
333
333
  "loginClicked": true,
334
334
  "pageUrl": "string",
335
335
  "waitingForPhoneLogin": true,
336
+ "qrCodePath": "string|null",
336
337
  "message": "string"
337
338
  }
338
339
  }
@@ -490,7 +491,7 @@ If a command fails, check these in order:
490
491
 
491
492
  ## Repository
492
493
 
493
- - Homepage: https://github.com/skills-router/skills-store/tree/main/packages/rednote
494
+ - Homepage: https://github.com/skills-router/skills-store/tree/main/packages/rednote-cli
494
495
  - Issues: https://github.com/skills-router/skills-store/issues
495
496
 
496
497
  ## License
@@ -1,17 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from 'node:util';
3
- import { printJson, runCli } from '../utils/browser-cli.js';
3
+ import { runCli } from '../utils/browser-cli.js';
4
4
  import { getRednoteEnvironmentInfo } from '../utils/browser-core.js';
5
+ import { ensureJsonSavePath, renderJsonSaveSummary, resolveJsonSavePath, writeJsonFile } from './output-format.js';
5
6
  function printEnvHelp() {
6
7
  process.stdout.write(`rednote env
7
8
 
8
9
  Usage:
9
- npx -y @skills-store/rednote env [--format md|json]
10
- node --experimental-strip-types ./scripts/rednote/env.ts [--format md|json]
11
- bun ./scripts/rednote/env.ts [--format md|json]
10
+ npx -y @skills-store/rednote env [--format md|json] [--save PATH]
11
+ node --experimental-strip-types ./scripts/rednote/env.ts [--format md|json] [--save PATH]
12
+ bun ./scripts/rednote/env.ts [--format md|json] [--save PATH]
12
13
 
13
14
  Options:
14
15
  --format FORMAT Output format: md | json. Default: md
16
+ --save PATH Required when --format json is used. Saves the full result as JSON
15
17
  -h, --help Show this help
16
18
  `);
17
19
  }
@@ -40,8 +42,12 @@ export async function runEnvCommand(values = {}) {
40
42
  return;
41
43
  }
42
44
  const format = values.format ?? 'md';
45
+ ensureJsonSavePath(format, values.savePath);
43
46
  if (format === 'json') {
44
- printJson(getRednoteEnvironmentInfo());
47
+ const result = getRednoteEnvironmentInfo();
48
+ const savedPath = resolveJsonSavePath(values.savePath);
49
+ writeJsonFile(result, savedPath);
50
+ process.stdout.write(renderJsonSaveSummary(savedPath, result));
45
51
  return;
46
52
  }
47
53
  process.stdout.write(renderEnvironmentMarkdown());
@@ -55,6 +61,9 @@ async function main() {
55
61
  format: {
56
62
  type: 'string'
57
63
  },
64
+ save: {
65
+ type: 'string'
66
+ },
58
67
  help: {
59
68
  type: 'boolean',
60
69
  short: 'h'
@@ -64,6 +73,10 @@ async function main() {
64
73
  if (values.format && values.format !== 'md' && values.format !== 'json') {
65
74
  throw new Error(`Invalid --format value: ${String(values.format)}`);
66
75
  }
67
- await runEnvCommand(values);
76
+ await runEnvCommand({
77
+ format: values.format,
78
+ savePath: values.save,
79
+ help: values.help
80
+ });
68
81
  }
69
82
  runCli(import.meta.url, main);
@@ -2,59 +2,125 @@
2
2
  import * as cheerio from 'cheerio';
3
3
  import { parseArgs } from 'node:util';
4
4
  import vm from 'node:vm';
5
- import { printJson, runCli } from '../utils/browser-cli.js';
5
+ import { runCli } from '../utils/browser-cli.js';
6
6
  import { resolveStatusTarget } from './status.js';
7
7
  import { createRednoteSession, disconnectRednoteSession, ensureRednoteLoggedIn } from './checkLogin.js';
8
+ import { ensureJsonSavePath, renderJsonSaveSummary, resolveJsonSavePath, writeJsonFile } from './output-format.js';
8
9
  function printGetFeedDetailHelp() {
9
10
  process.stdout.write(`rednote get-feed-detail
10
11
 
11
12
  Usage:
12
- npx -y @skills-store/rednote get-feed-detail [--instance NAME] --url URL [--url URL] [--format md|json]
13
- node --experimental-strip-types ./scripts/rednote/getFeedDetail.ts --instance NAME --url URL [--url URL] [--format md|json]
14
- bun ./scripts/rednote/getFeedDetail.ts --instance NAME --url URL [--url URL] [--format md|json]
13
+ npx -y @skills-store/rednote get-feed-detail [--instance NAME] --url URL [--url URL] [--comments [COUNT]] [--format md|json] [--save PATH]
14
+ node --experimental-strip-types ./scripts/rednote/getFeedDetail.ts --instance NAME --url URL [--url URL] [--comments [COUNT]] [--format md|json] [--save PATH]
15
+ bun ./scripts/rednote/getFeedDetail.ts --instance NAME --url URL [--url URL] [--comments [COUNT]] [--format md|json] [--save PATH]
15
16
 
16
17
  Options:
17
18
  --instance NAME Optional. Defaults to the saved lastConnect instance
18
19
  --url URL Required. Xiaohongshu explore url, repeatable
20
+ --comments [COUNT] Optional. Include comment data. When COUNT is provided, scroll \`.note-scroller\` until COUNT comments, the end, or timeout
19
21
  --format FORMAT Output format: md | json. Default: md
22
+ --save PATH Required when --format json is used. Saves the selected result array as JSON
20
23
  -h, --help Show this help
21
24
  `);
22
25
  }
26
+ function parseOptionWithEquals(arg) {
27
+ const equalIndex = arg.indexOf('=');
28
+ if (equalIndex === -1) {
29
+ return null;
30
+ }
31
+ return {
32
+ key: arg.slice(0, equalIndex),
33
+ value: arg.slice(equalIndex + 1)
34
+ };
35
+ }
36
+ function parseCommentsValue(value) {
37
+ if (!value) {
38
+ throw new Error('Missing value for --comments');
39
+ }
40
+ const parsed = Number(value);
41
+ if (!Number.isInteger(parsed) || parsed <= 0) {
42
+ throw new Error(`Invalid --comments value: ${String(value)}`);
43
+ }
44
+ return parsed;
45
+ }
23
46
  export function parseGetFeedDetailCliArgs(argv) {
24
- const { values, positionals } = parseArgs({
25
- args: argv,
26
- allowPositionals: true,
27
- strict: false,
28
- options: {
29
- instance: {
30
- type: 'string'
31
- },
32
- url: {
33
- type: 'string',
34
- multiple: true
35
- },
36
- format: {
37
- type: 'string'
38
- },
39
- help: {
40
- type: 'boolean',
41
- short: 'h'
47
+ const values = {
48
+ urls: [],
49
+ format: 'md',
50
+ comments: undefined,
51
+ help: false
52
+ };
53
+ const positionals = [];
54
+ for(let index = 0; index < argv.length; index += 1){
55
+ const arg = argv[index];
56
+ const withEquals = parseOptionWithEquals(arg);
57
+ if (arg === '-h' || arg === '--help') {
58
+ values.help = true;
59
+ continue;
60
+ }
61
+ if (withEquals?.key === '--instance') {
62
+ values.instance = withEquals.value;
63
+ continue;
64
+ }
65
+ if (arg === '--instance') {
66
+ values.instance = argv[index + 1];
67
+ index += 1;
68
+ continue;
69
+ }
70
+ if (withEquals?.key === '--url') {
71
+ values.urls.push(withEquals.value);
72
+ continue;
73
+ }
74
+ if (arg === '--url') {
75
+ const nextArg = argv[index + 1];
76
+ if (!nextArg || nextArg.startsWith('-')) {
77
+ throw new Error('Missing required option value: --url');
78
+ }
79
+ values.urls.push(nextArg);
80
+ index += 1;
81
+ continue;
82
+ }
83
+ if (withEquals?.key === '--format') {
84
+ values.format = withEquals.value;
85
+ continue;
86
+ }
87
+ if (arg === '--format') {
88
+ values.format = argv[index + 1];
89
+ index += 1;
90
+ continue;
91
+ }
92
+ if (withEquals?.key === '--comments') {
93
+ values.comments = withEquals.value ? parseCommentsValue(withEquals.value) : null;
94
+ continue;
95
+ }
96
+ if (arg === '--comments') {
97
+ const nextArg = argv[index + 1];
98
+ if (nextArg && !nextArg.startsWith('-')) {
99
+ values.comments = parseCommentsValue(nextArg);
100
+ index += 1;
101
+ } else {
102
+ values.comments = null;
42
103
  }
104
+ continue;
43
105
  }
44
- });
106
+ if (withEquals?.key === '--save') {
107
+ values.savePath = withEquals.value;
108
+ continue;
109
+ }
110
+ if (arg === '--save') {
111
+ values.savePath = argv[index + 1];
112
+ index += 1;
113
+ continue;
114
+ }
115
+ positionals.push(arg);
116
+ }
45
117
  if (positionals.length > 0) {
46
118
  throw new Error(`Unexpected positional arguments: ${positionals.join(' ')}`);
47
119
  }
48
- const format = values.format ?? 'md';
49
- if (format !== 'md' && format !== 'json') {
50
- throw new Error(`Invalid --format value: ${String(format)}`);
120
+ if (values.format !== 'md' && values.format !== 'json') {
121
+ throw new Error(`Invalid --format value: ${String(values.format)}`);
51
122
  }
52
- return {
53
- instance: values.instance,
54
- urls: values.url ?? [],
55
- format,
56
- help: values.help
57
- };
123
+ return values;
58
124
  }
59
125
  function validateFeedDetailUrl(url) {
60
126
  try {
@@ -94,103 +160,150 @@ function extractVideoUrl(note) {
94
160
  const firstAvailable = streams.find((items)=>Array.isArray(items) && items.length > 0);
95
161
  return firstAvailable?.[0]?.backupUrls?.[0] ?? null;
96
162
  }
163
+ const COMMENTS_CONTAINER_SELECTOR = '.note-scroller';
164
+ const COMMENT_SCROLL_TIMEOUT_MS = 20_000;
165
+ const COMMENT_SCROLL_IDLE_LIMIT = 4;
166
+ function hasCommentsEnabled(comments) {
167
+ return comments !== undefined;
168
+ }
169
+ function buildCommentKey(comment) {
170
+ return String(comment?.id ?? comment?.commentId ?? comment?.comment_id ?? `${comment?.userInfo?.userId ?? comment?.user_info?.user_id ?? 'unknown'}:${comment?.createTime ?? comment?.create_time ?? 'unknown'}:${comment?.content ?? ''}`);
171
+ }
172
+ function getCommentCount(commentsMap) {
173
+ return commentsMap.size;
174
+ }
175
+ async function scrollCommentsContainer(page, targetCount, getCount) {
176
+ const container = page.locator(COMMENTS_CONTAINER_SELECTOR).first();
177
+ const visible = await container.isVisible().catch(()=>false);
178
+ if (!visible) {
179
+ return;
180
+ }
181
+ await container.scrollIntoViewIfNeeded().catch(()=>{});
182
+ await container.hover().catch(()=>{});
183
+ const getMetrics = async ()=>await container.evaluate((element)=>{
184
+ const htmlElement = element;
185
+ const atBottom = htmlElement.scrollTop + htmlElement.clientHeight >= htmlElement.scrollHeight - 8;
186
+ return {
187
+ scrollTop: htmlElement.scrollTop,
188
+ scrollHeight: htmlElement.scrollHeight,
189
+ clientHeight: htmlElement.clientHeight,
190
+ atBottom
191
+ };
192
+ }).catch(()=>null);
193
+ const deadline = Date.now() + COMMENT_SCROLL_TIMEOUT_MS;
194
+ let idleRounds = 0;
195
+ while(Date.now() < deadline){
196
+ if (getCount() >= targetCount) {
197
+ return;
198
+ }
199
+ const beforeMetrics = await getMetrics();
200
+ if (!beforeMetrics) {
201
+ return;
202
+ }
203
+ const beforeCount = getCount();
204
+ const delta = Math.max(Math.floor(beforeMetrics.clientHeight * 0.85), 480);
205
+ await page.mouse.wheel(0, delta).catch(()=>{});
206
+ await page.waitForTimeout(900);
207
+ const afterMetrics = await getMetrics();
208
+ await page.waitForTimeout(400);
209
+ const afterCount = getCount();
210
+ const countChanged = afterCount > beforeCount;
211
+ const scrollMoved = Boolean(afterMetrics) && afterMetrics.scrollTop > beforeMetrics.scrollTop;
212
+ const reachedBottom = Boolean(afterMetrics?.atBottom);
213
+ if (countChanged || scrollMoved) {
214
+ idleRounds = 0;
215
+ continue;
216
+ }
217
+ idleRounds += 1;
218
+ if (reachedBottom && idleRounds >= 2 || idleRounds >= COMMENT_SCROLL_IDLE_LIMIT) {
219
+ return;
220
+ }
221
+ }
222
+ }
97
223
  function normalizeDetailNote(note) {
98
224
  return {
99
225
  noteId: note?.noteId ?? null,
100
226
  title: note?.title ?? null,
101
227
  desc: note?.desc ?? null,
102
228
  type: note?.type ?? null,
103
- interactInfo: {
104
- liked: note?.interactInfo?.liked ?? null,
105
- likedCount: note?.interactInfo?.likedCount ?? null,
106
- commentCount: note?.interactInfo?.commentCount ?? null,
107
- collected: note?.interactInfo?.collected ?? null,
108
- collectedCount: note?.interactInfo?.collectedCount ?? null,
109
- shareCount: note?.interactInfo?.shareCount ?? null,
110
- followed: note?.interactInfo?.followed ?? null
111
- },
112
- tagList: Array.isArray(note?.tagList) ? note.tagList.map((tag)=>({
113
- name: tag?.name ?? null
114
- })) : [],
115
- imageList: Array.isArray(note?.imageList) ? note.imageList.map((image)=>({
116
- urlDefault: image?.urlDefault ?? null,
117
- urlPre: image?.urlPre ?? null,
118
- width: image?.width ?? null,
119
- height: image?.height ?? null
120
- })) : [],
121
- video: note?.video ? {
122
- url: extractVideoUrl(note),
123
- raw: note.video
124
- } : null,
125
- raw: note
229
+ liked: note?.interactInfo?.liked ?? null,
230
+ likedCount: note?.interactInfo?.likedCount ?? null,
231
+ commentCount: note?.interactInfo?.commentCount ?? null,
232
+ collected: note?.interactInfo?.collected ?? null,
233
+ collectedCount: note?.interactInfo?.collectedCount ?? null,
234
+ shareCount: note?.interactInfo?.shareCount ?? null,
235
+ followed: note?.interactInfo?.followed ?? null,
236
+ tagList: Array.isArray(note?.tagList) ? note.tagList.map((tag)=>tag?.name ?? null).filter((tag)=>Boolean(tag)) : [],
237
+ imageList: Array.isArray(note?.imageList) ? note.imageList.map((image)=>image?.urlDefault ?? null).filter((imageUrl)=>Boolean(imageUrl)) : [],
238
+ video: extractVideoUrl(note)
126
239
  };
127
240
  }
128
241
  function normalizeComments(comments) {
129
242
  return comments.map((comment)=>({
130
- id: comment?.id ?? comment?.commentId ?? null,
131
243
  content: comment?.content ?? null,
132
- userId: comment?.userInfo?.userId ?? null,
133
- nickname: comment?.userInfo?.nickname ?? null,
134
- likedCount: comment?.interactInfo?.likedCount ?? null,
135
- subCommentCount: typeof comment?.subCommentCount === 'number' ? comment.subCommentCount : null,
136
- raw: comment
244
+ userId: comment?.userInfo?.userId ?? comment?.user_info?.user_id ?? null,
245
+ nickname: comment?.userInfo?.nickname ?? comment?.user_info?.nickname ?? null,
246
+ create_time: comment?.createTime ?? comment?.create_time ?? null,
247
+ like_count: comment?.likeCount ?? comment?.like_count ?? comment?.interactInfo?.likedCount ?? null,
248
+ sub_comment_count: typeof (comment?.subCommentCount ?? comment?.sub_comment_count) === 'string' ? comment?.subCommentCount ?? comment?.sub_comment_count : null
137
249
  }));
138
250
  }
139
251
  function formatDetailField(value) {
140
252
  return value ?? '';
141
253
  }
142
- function renderDetailMarkdown(items) {
254
+ function renderDetailMarkdown(items, includeComments = false) {
143
255
  if (items.length === 0) {
144
- return '没有获取到帖子详情。\n';
256
+ return 'No feed details were captured.\n';
145
257
  }
146
258
  return `${items.map((item)=>{
147
259
  const lines = [];
148
260
  lines.push('## Note');
149
261
  lines.push('');
150
- lines.push(`- Url: ${item.url}`);
151
262
  lines.push(`- Title: ${formatDetailField(item.note.title)}`);
152
- lines.push(`- Type: ${formatDetailField(item.note.type)}`);
153
- lines.push(`- Liked: ${formatDetailField(item.note.interactInfo.liked)}`);
154
- lines.push(`- Collected: ${formatDetailField(item.note.interactInfo.collected)}`);
155
- lines.push(`- LikedCount: ${formatDetailField(item.note.interactInfo.likedCount)}`);
156
- lines.push(`- CommentCount: ${formatDetailField(item.note.interactInfo.commentCount)}`);
157
- lines.push(`- CollectedCount: ${formatDetailField(item.note.interactInfo.collectedCount)}`);
158
- lines.push(`- ShareCount: ${formatDetailField(item.note.interactInfo.shareCount)}`);
159
- lines.push(`- Tags: ${item.note.tagList.map((tag)=>tag.name ? `#${tag.name}` : '').filter(Boolean).join(' ')}`);
263
+ lines.push(`- Liked: ${formatDetailField(item.note.liked)}`);
264
+ lines.push(`- Collected: ${formatDetailField(item.note.collected)}`);
265
+ lines.push(`- LikedCount: ${formatDetailField(item.note.likedCount)}`);
266
+ lines.push(`- CommentCount: ${formatDetailField(item.note.commentCount)}`);
267
+ lines.push(`- CollectedCount: ${formatDetailField(item.note.collectedCount)}`);
268
+ lines.push(`- ShareCount: ${formatDetailField(item.note.shareCount)}`);
269
+ lines.push(`- Tags: ${item.note.tagList.map((tag)=>`#${tag}`).join(' ')}`);
160
270
  lines.push('');
161
271
  lines.push('## Content');
162
272
  lines.push('');
163
273
  lines.push(item.note.desc ?? '');
164
- if (item.note.imageList.length > 0 || item.note.video?.url) {
274
+ if (item.note.imageList.length > 0 || item.note.video) {
165
275
  lines.push('');
166
276
  lines.push('## Media');
167
277
  lines.push('');
168
- item.note.imageList.forEach((image, index)=>{
169
- if (image.urlDefault) {
170
- lines.push(`- Image${index + 1}: ${image.urlDefault}`);
171
- }
278
+ item.note.imageList.forEach((imageUrl, index)=>{
279
+ lines.push(`- Image${index + 1}: ${imageUrl}`);
172
280
  });
173
- if (item.note.video?.url) {
174
- lines.push(`- Video: ${item.note.video.url}`);
281
+ if (item.note.video) {
282
+ lines.push(`- Video: ${item.note.video}`);
175
283
  }
176
284
  }
177
- lines.push('');
178
- lines.push('## Comments');
179
- lines.push('');
180
- if (item.comments.length === 0) {
181
- lines.push('- Comments not found');
182
- } else {
183
- item.comments.forEach((comment)=>{
184
- const prefix = comment.nickname ? `${comment.nickname}: ` : '';
185
- lines.push(`- ${prefix}${comment.content ?? ''}`);
186
- });
285
+ if (includeComments) {
286
+ lines.push('');
287
+ lines.push('## Comments');
288
+ lines.push('');
289
+ if (!item.comments || item.comments.length === 0) {
290
+ lines.push('- Comments not found');
291
+ } else {
292
+ item.comments.forEach((comment)=>{
293
+ const prefix = comment.nickname ? `${comment.nickname}: ` : '';
294
+ lines.push(`- ${prefix}${comment.content ?? ''}`);
295
+ });
296
+ }
187
297
  }
188
298
  return lines.join('\n');
189
299
  }).join('\n\n---\n\n')}\n`;
190
300
  }
191
- async function captureFeedDetail(page, targetUrl) {
301
+ async function captureFeedDetail(page, targetUrl, commentsOption = undefined) {
302
+ const includeComments = hasCommentsEnabled(commentsOption);
303
+ const commentsTarget = typeof commentsOption === 'number' ? commentsOption : null;
192
304
  let note = null;
193
- let comments = null;
305
+ let commentsLoaded = !includeComments;
306
+ const commentsMap = new Map();
194
307
  const handleResponse = async (response)=>{
195
308
  try {
196
309
  const url = new URL(response.url());
@@ -214,9 +327,13 @@ async function captureFeedDetail(page, targetUrl) {
214
327
  note = noteState.noteDetailMap[noteState.currentNoteId]?.note ?? note;
215
328
  }
216
329
  });
217
- } else if (url.href.includes('comment/page?')) {
330
+ } else if (includeComments && url.href.includes('comment/page?')) {
218
331
  const data = await response.json();
219
- comments = Array.isArray(data?.data?.comments) ? data.data.comments : [];
332
+ const nextComments = Array.isArray(data?.data?.comments) ? data.data.comments : [];
333
+ commentsLoaded = true;
334
+ for (const comment of nextComments){
335
+ commentsMap.set(buildCommentKey(comment), comment);
336
+ }
220
337
  }
221
338
  } catch {}
222
339
  };
@@ -227,7 +344,7 @@ async function captureFeedDetail(page, targetUrl) {
227
344
  });
228
345
  const deadline = Date.now() + 15_000;
229
346
  while(Date.now() < deadline){
230
- if (note && comments !== null) {
347
+ if (note && commentsLoaded) {
231
348
  break;
232
349
  }
233
350
  await page.waitForTimeout(200);
@@ -235,22 +352,29 @@ async function captureFeedDetail(page, targetUrl) {
235
352
  if (!note) {
236
353
  throw new Error(`Failed to capture note detail: ${targetUrl}`);
237
354
  }
355
+ if (includeComments && commentsTarget) {
356
+ await scrollCommentsContainer(page, commentsTarget, ()=>getCommentCount(commentsMap));
357
+ }
238
358
  return {
239
359
  url: targetUrl,
240
360
  note: normalizeDetailNote(note),
241
- comments: normalizeComments(comments ?? [])
361
+ ...includeComments ? {
362
+ comments: normalizeComments([
363
+ ...commentsMap.values()
364
+ ])
365
+ } : {}
242
366
  };
243
367
  } finally{
244
368
  page.off('response', handleResponse);
245
369
  }
246
370
  }
247
- export async function getFeedDetails(session, urls) {
371
+ export async function getFeedDetails(session, urls, commentsOption = undefined) {
248
372
  const page = await getOrCreateXiaohongshuPage(session);
249
373
  const items = [];
250
374
  for (const url of urls){
251
375
  const normalizedUrl = normalizeFeedDetailUrl(url);
252
376
  validateFeedDetailUrl(normalizedUrl);
253
- items.push(await captureFeedDetail(page, normalizedUrl));
377
+ items.push(await captureFeedDetail(page, normalizedUrl, commentsOption));
254
378
  }
255
379
  return {
256
380
  ok: true,
@@ -261,12 +385,18 @@ export async function getFeedDetails(session, urls) {
261
385
  }
262
386
  };
263
387
  }
264
- function writeFeedDetailOutput(result, format) {
265
- if (format === 'json') {
266
- printJson(result);
388
+ function selectFeedDetailOutput(result) {
389
+ return result.detail.items;
390
+ }
391
+ function writeFeedDetailOutput(result, values) {
392
+ const output = selectFeedDetailOutput(result);
393
+ if (values.format === 'json') {
394
+ const savedPath = resolveJsonSavePath(values.savePath);
395
+ writeJsonFile(output, savedPath);
396
+ process.stdout.write(renderJsonSaveSummary(savedPath, output));
267
397
  return;
268
398
  }
269
- process.stdout.write(renderDetailMarkdown(result.detail.items));
399
+ process.stdout.write(renderDetailMarkdown(result.detail.items, hasCommentsEnabled(values.comments)));
270
400
  }
271
401
  export async function runGetFeedDetailCommand(values = {
272
402
  urls: [],
@@ -276,6 +406,7 @@ export async function runGetFeedDetailCommand(values = {
276
406
  printGetFeedDetailHelp();
277
407
  return;
278
408
  }
409
+ ensureJsonSavePath(values.format, values.savePath);
279
410
  if (values.urls.length === 0) {
280
411
  throw new Error('Missing required option: --url');
281
412
  }
@@ -283,8 +414,8 @@ export async function runGetFeedDetailCommand(values = {
283
414
  const session = await createRednoteSession(target);
284
415
  try {
285
416
  await ensureRednoteLoggedIn(target, 'fetching feed detail', session);
286
- const result = await getFeedDetails(session, values.urls);
287
- writeFeedDetailOutput(result, values.format);
417
+ const result = await getFeedDetails(session, values.urls, values.comments);
418
+ writeFeedDetailOutput(result, values);
288
419
  } finally{
289
420
  await disconnectRednoteSession(session);
290
421
  }