@jackwener/opencli 1.5.4 → 1.5.6

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 (256) hide show
  1. package/README.md +27 -2
  2. package/README.zh-CN.md +36 -4
  3. package/dist/browser/daemon-client.d.ts +5 -1
  4. package/dist/browser/page.d.ts +6 -0
  5. package/dist/browser/page.js +15 -0
  6. package/dist/cli-manifest.json +1284 -67
  7. package/dist/cli.js +14 -14
  8. package/dist/clis/antigravity/serve.js +2 -2
  9. package/dist/clis/band/bands.d.ts +1 -0
  10. package/dist/clis/band/bands.js +72 -0
  11. package/dist/clis/band/mentions.d.ts +1 -0
  12. package/dist/clis/band/mentions.js +127 -0
  13. package/dist/clis/band/post.d.ts +1 -0
  14. package/dist/clis/band/post.js +175 -0
  15. package/dist/clis/band/posts.d.ts +1 -0
  16. package/dist/clis/band/posts.js +94 -0
  17. package/dist/clis/doubao/detail.d.ts +1 -0
  18. package/dist/clis/doubao/detail.js +33 -0
  19. package/dist/clis/doubao/detail.test.d.ts +1 -0
  20. package/dist/clis/doubao/detail.test.js +42 -0
  21. package/dist/clis/doubao/history.d.ts +1 -0
  22. package/dist/clis/doubao/history.js +28 -0
  23. package/dist/clis/doubao/history.test.d.ts +1 -0
  24. package/dist/clis/doubao/history.test.js +37 -0
  25. package/dist/clis/doubao/meeting-summary.d.ts +1 -0
  26. package/dist/clis/doubao/meeting-summary.js +39 -0
  27. package/dist/clis/doubao/meeting-transcript.d.ts +1 -0
  28. package/dist/clis/doubao/meeting-transcript.js +36 -0
  29. package/dist/clis/doubao/utils.d.ts +27 -0
  30. package/dist/clis/doubao/utils.js +317 -0
  31. package/dist/clis/doubao/utils.test.d.ts +1 -0
  32. package/dist/clis/doubao/utils.test.js +24 -0
  33. package/dist/clis/douyin/_shared/public-api.d.ts +33 -0
  34. package/dist/clis/douyin/_shared/public-api.js +29 -0
  35. package/dist/clis/douyin/user-videos.d.ts +5 -0
  36. package/dist/clis/douyin/user-videos.js +74 -0
  37. package/dist/clis/douyin/user-videos.test.d.ts +1 -0
  38. package/dist/clis/douyin/user-videos.test.js +108 -0
  39. package/dist/clis/ones/common.d.ts +32 -0
  40. package/dist/clis/ones/common.js +144 -0
  41. package/dist/clis/ones/enrich-tasks.d.ts +5 -0
  42. package/dist/clis/ones/enrich-tasks.js +37 -0
  43. package/dist/clis/ones/login.d.ts +1 -0
  44. package/dist/clis/ones/login.js +80 -0
  45. package/dist/clis/ones/logout.d.ts +1 -0
  46. package/dist/clis/ones/logout.js +17 -0
  47. package/dist/clis/ones/me.d.ts +1 -0
  48. package/dist/clis/ones/me.js +30 -0
  49. package/dist/clis/ones/my-tasks.d.ts +1 -0
  50. package/dist/clis/ones/my-tasks.js +120 -0
  51. package/dist/clis/ones/resolve-labels.d.ts +10 -0
  52. package/dist/clis/ones/resolve-labels.js +64 -0
  53. package/dist/clis/ones/task-helpers.d.ts +29 -0
  54. package/dist/clis/ones/task-helpers.js +212 -0
  55. package/dist/clis/ones/task-helpers.test.d.ts +1 -0
  56. package/dist/clis/ones/task-helpers.test.js +12 -0
  57. package/dist/clis/ones/task.d.ts +1 -0
  58. package/dist/clis/ones/task.js +66 -0
  59. package/dist/clis/ones/tasks.d.ts +1 -0
  60. package/dist/clis/ones/tasks.js +79 -0
  61. package/dist/clis/ones/token-info.d.ts +1 -0
  62. package/dist/clis/ones/token-info.js +42 -0
  63. package/dist/clis/ones/worklog.d.ts +11 -0
  64. package/dist/clis/ones/worklog.js +267 -0
  65. package/dist/clis/ones/worklog.test.d.ts +1 -0
  66. package/dist/clis/ones/worklog.test.js +20 -0
  67. package/dist/clis/sinafinance/rolling-news.d.ts +4 -0
  68. package/dist/clis/sinafinance/rolling-news.js +40 -0
  69. package/dist/clis/sinafinance/stock.d.ts +8 -0
  70. package/dist/clis/sinafinance/stock.js +117 -0
  71. package/dist/clis/spotify/spotify.d.ts +1 -0
  72. package/dist/clis/spotify/spotify.js +316 -0
  73. package/dist/clis/spotify/utils.d.ts +21 -0
  74. package/dist/clis/spotify/utils.js +66 -0
  75. package/dist/clis/spotify/utils.test.d.ts +1 -0
  76. package/dist/clis/spotify/utils.test.js +67 -0
  77. package/dist/clis/tieba/commands.test.d.ts +4 -0
  78. package/dist/clis/tieba/commands.test.js +79 -0
  79. package/dist/clis/tieba/hot.d.ts +1 -0
  80. package/dist/clis/tieba/hot.js +48 -0
  81. package/dist/clis/tieba/posts.d.ts +1 -0
  82. package/dist/clis/tieba/posts.js +85 -0
  83. package/dist/clis/tieba/read.d.ts +1 -0
  84. package/dist/clis/tieba/read.js +140 -0
  85. package/dist/clis/tieba/search.d.ts +1 -0
  86. package/dist/clis/tieba/search.js +108 -0
  87. package/dist/clis/tieba/utils.d.ts +101 -0
  88. package/dist/clis/tieba/utils.js +240 -0
  89. package/dist/clis/tieba/utils.test.d.ts +1 -0
  90. package/dist/clis/tieba/utils.test.js +290 -0
  91. package/dist/clis/weread/book.js +100 -13
  92. package/dist/clis/weread/commands.test.js +221 -0
  93. package/dist/clis/weread/private-api-regression.test.d.ts +1 -0
  94. package/dist/{weread-private-api-regression.test.js → clis/weread/private-api-regression.test.js} +92 -30
  95. package/dist/clis/weread/search-regression.test.d.ts +1 -0
  96. package/dist/clis/weread/search-regression.test.js +407 -0
  97. package/dist/clis/weread/search.js +143 -7
  98. package/dist/clis/weread/shelf.js +13 -95
  99. package/dist/clis/weread/utils.d.ts +46 -0
  100. package/dist/clis/weread/utils.js +214 -7
  101. package/dist/clis/weread/utils.test.js +71 -1
  102. package/dist/clis/xiaohongshu/publish.d.ts +1 -1
  103. package/dist/clis/xiaohongshu/publish.js +78 -31
  104. package/dist/clis/xiaohongshu/publish.test.js +66 -1
  105. package/dist/clis/xiaohongshu/user-helpers.d.ts +1 -0
  106. package/dist/clis/xiaohongshu/user-helpers.js +2 -0
  107. package/dist/clis/xiaohongshu/user-helpers.test.js +18 -0
  108. package/dist/clis/xueqiu/comments.d.ts +118 -0
  109. package/dist/clis/xueqiu/comments.js +354 -0
  110. package/dist/clis/xueqiu/comments.test.d.ts +1 -0
  111. package/dist/clis/xueqiu/comments.test.js +696 -0
  112. package/dist/clis/youtube/transcript.js +2 -4
  113. package/dist/clis/youtube/utils.d.ts +9 -0
  114. package/dist/clis/youtube/utils.js +67 -3
  115. package/dist/clis/youtube/utils.test.d.ts +1 -0
  116. package/dist/clis/youtube/utils.test.js +37 -0
  117. package/dist/clis/youtube/video.js +16 -15
  118. package/dist/clis/zsxq/dynamics.d.ts +1 -0
  119. package/dist/clis/zsxq/dynamics.js +47 -0
  120. package/dist/clis/zsxq/groups.d.ts +1 -0
  121. package/dist/clis/zsxq/groups.js +32 -0
  122. package/dist/clis/zsxq/search.d.ts +1 -0
  123. package/dist/clis/zsxq/search.js +43 -0
  124. package/dist/clis/zsxq/search.test.d.ts +1 -0
  125. package/dist/clis/zsxq/search.test.js +24 -0
  126. package/dist/clis/zsxq/topic.d.ts +1 -0
  127. package/dist/clis/zsxq/topic.js +47 -0
  128. package/dist/clis/zsxq/topic.test.d.ts +1 -0
  129. package/dist/clis/zsxq/topic.test.js +29 -0
  130. package/dist/clis/zsxq/topics.d.ts +1 -0
  131. package/dist/clis/zsxq/topics.js +25 -0
  132. package/dist/clis/zsxq/topics.test.d.ts +1 -0
  133. package/dist/clis/zsxq/topics.test.js +24 -0
  134. package/dist/clis/zsxq/utils.d.ts +97 -0
  135. package/dist/clis/zsxq/utils.js +230 -0
  136. package/dist/commanderAdapter.js +27 -4
  137. package/dist/commanderAdapter.test.js +39 -0
  138. package/dist/daemon.js +5 -4
  139. package/dist/errors.d.ts +29 -1
  140. package/dist/errors.js +49 -11
  141. package/dist/external-clis.yaml +17 -0
  142. package/dist/external.js +3 -3
  143. package/dist/main.js +2 -1
  144. package/dist/tui.js +2 -1
  145. package/dist/types.d.ts +5 -0
  146. package/docs/.vitepress/config.mts +3 -0
  147. package/docs/adapters/browser/band.md +63 -0
  148. package/docs/adapters/browser/ones.md +59 -0
  149. package/docs/adapters/browser/sinafinance.md +56 -6
  150. package/docs/adapters/browser/spotify.md +62 -0
  151. package/docs/adapters/browser/tieba.md +45 -0
  152. package/docs/adapters/browser/xueqiu.md +5 -0
  153. package/docs/adapters/browser/zsxq.md +49 -0
  154. package/docs/adapters/index.md +5 -2
  155. package/docs/adapters-doc/ones.md +32 -0
  156. package/extension/dist/background.js +1 -2
  157. package/extension/manifest.json +1 -1
  158. package/extension/package.json +1 -1
  159. package/extension/src/background.ts +17 -1
  160. package/extension/src/cdp.ts +42 -0
  161. package/extension/src/protocol.ts +5 -1
  162. package/package.json +1 -1
  163. package/scripts/postinstall.js +16 -0
  164. package/src/browser/daemon-client.ts +5 -1
  165. package/src/browser/page.ts +16 -0
  166. package/src/cli.ts +14 -14
  167. package/src/clis/antigravity/serve.ts +2 -2
  168. package/src/clis/band/bands.ts +76 -0
  169. package/src/clis/band/mentions.ts +134 -0
  170. package/src/clis/band/post.ts +187 -0
  171. package/src/clis/band/posts.ts +106 -0
  172. package/src/clis/doubao/detail.test.ts +53 -0
  173. package/src/clis/doubao/detail.ts +41 -0
  174. package/src/clis/doubao/history.test.ts +45 -0
  175. package/src/clis/doubao/history.ts +32 -0
  176. package/src/clis/doubao/meeting-summary.ts +53 -0
  177. package/src/clis/doubao/meeting-transcript.ts +48 -0
  178. package/src/clis/doubao/utils.test.ts +45 -0
  179. package/src/clis/doubao/utils.ts +371 -0
  180. package/src/clis/douyin/_shared/public-api.ts +84 -0
  181. package/src/clis/douyin/user-videos.test.ts +122 -0
  182. package/src/clis/douyin/user-videos.ts +101 -0
  183. package/src/clis/ones/common.ts +187 -0
  184. package/src/clis/ones/enrich-tasks.ts +47 -0
  185. package/src/clis/ones/login.ts +103 -0
  186. package/src/clis/ones/logout.ts +19 -0
  187. package/src/clis/ones/me.ts +34 -0
  188. package/src/clis/ones/my-tasks.ts +148 -0
  189. package/src/clis/ones/resolve-labels.ts +80 -0
  190. package/src/clis/ones/task-helpers.test.ts +14 -0
  191. package/src/clis/ones/task-helpers.ts +214 -0
  192. package/src/clis/ones/task.ts +79 -0
  193. package/src/clis/ones/tasks.ts +92 -0
  194. package/src/clis/ones/token-info.ts +46 -0
  195. package/src/clis/ones/worklog.test.ts +24 -0
  196. package/src/clis/ones/worklog.ts +306 -0
  197. package/src/clis/sinafinance/rolling-news.ts +42 -0
  198. package/src/clis/sinafinance/stock.ts +127 -0
  199. package/src/clis/spotify/spotify.ts +328 -0
  200. package/src/clis/spotify/utils.test.ts +87 -0
  201. package/src/clis/spotify/utils.ts +92 -0
  202. package/src/clis/tieba/commands.test.ts +86 -0
  203. package/src/clis/tieba/hot.ts +52 -0
  204. package/src/clis/tieba/posts.ts +108 -0
  205. package/src/clis/tieba/read.ts +158 -0
  206. package/src/clis/tieba/search.ts +119 -0
  207. package/src/clis/tieba/utils.test.ts +322 -0
  208. package/src/clis/tieba/utils.ts +348 -0
  209. package/src/clis/weread/book.ts +116 -13
  210. package/src/clis/weread/commands.test.ts +249 -0
  211. package/src/{weread-private-api-regression.test.ts → clis/weread/private-api-regression.test.ts} +108 -30
  212. package/src/clis/weread/search-regression.test.ts +440 -0
  213. package/src/clis/weread/search.ts +189 -9
  214. package/src/clis/weread/shelf.ts +20 -122
  215. package/src/clis/weread/utils.test.ts +81 -1
  216. package/src/clis/weread/utils.ts +264 -7
  217. package/src/clis/xiaohongshu/publish.test.ts +79 -1
  218. package/src/clis/xiaohongshu/publish.ts +84 -30
  219. package/src/clis/xiaohongshu/user-helpers.test.ts +23 -0
  220. package/src/clis/xiaohongshu/user-helpers.ts +4 -0
  221. package/src/clis/xueqiu/comments.test.ts +823 -0
  222. package/src/clis/xueqiu/comments.ts +461 -0
  223. package/src/clis/youtube/transcript.ts +2 -4
  224. package/src/clis/youtube/utils.test.ts +43 -0
  225. package/src/clis/youtube/utils.ts +69 -0
  226. package/src/clis/youtube/video.ts +16 -15
  227. package/src/clis/zsxq/dynamics.ts +60 -0
  228. package/src/clis/zsxq/groups.ts +41 -0
  229. package/src/clis/zsxq/search.test.ts +29 -0
  230. package/src/clis/zsxq/search.ts +54 -0
  231. package/src/clis/zsxq/topic.test.ts +34 -0
  232. package/src/clis/zsxq/topic.ts +68 -0
  233. package/src/clis/zsxq/topics.test.ts +29 -0
  234. package/src/clis/zsxq/topics.ts +36 -0
  235. package/src/clis/zsxq/utils.ts +351 -0
  236. package/src/commanderAdapter.test.ts +47 -0
  237. package/src/commanderAdapter.ts +26 -3
  238. package/src/daemon.ts +5 -4
  239. package/src/errors.ts +71 -10
  240. package/src/external-clis.yaml +17 -0
  241. package/src/external.ts +3 -3
  242. package/src/main.ts +2 -1
  243. package/src/tui.ts +2 -1
  244. package/src/types.ts +5 -0
  245. package/tests/e2e/band-auth.test.ts +20 -0
  246. package/tests/e2e/browser-auth-helpers.ts +18 -0
  247. package/tests/e2e/browser-auth.test.ts +35 -47
  248. package/tests/e2e/browser-public.test.ts +288 -0
  249. package/tests/e2e/management.test.ts +1 -1
  250. package/tests/e2e/plugin-management.test.ts +1 -1
  251. package/vitest.config.ts +1 -0
  252. package/SKILL.md +0 -879
  253. package/dist/weread-private-api-regression.test.d.ts +0 -1
  254. package/dist/weread-search-regression.test.d.ts +0 -1
  255. package/dist/weread-search-regression.test.js +0 -39
  256. package/src/weread-search-regression.test.ts +0 -44
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import { PKG_VERSION } from './version.js';
14
14
  import { printCompletionScript } from './completion.js';
15
15
  import { loadExternalClis, executeExternalCli, installExternalCli, registerExternalCli, isBinaryInstalled } from './external.js';
16
16
  import { registerAllCommands } from './commanderAdapter.js';
17
- import { getErrorMessage } from './errors.js';
17
+ import { EXIT_CODES, getErrorMessage } from './errors.js';
18
18
  export function runCli(BUILTIN_CLIS, USER_CLIS) {
19
19
  const program = new Command();
20
20
  // enablePositionalOptions: prevents parent from consuming flags meant for subcommands;
@@ -108,7 +108,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
108
108
  const { verifyClis, renderVerifyReport } = await import('./verify.js');
109
109
  const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke });
110
110
  console.log(renderVerifyReport(r));
111
- process.exitCode = r.ok ? 0 : 1;
111
+ process.exitCode = r.ok ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERIC_ERROR;
112
112
  });
113
113
  // ── Built-in: explore / synthesize / generate / cascade ───────────────────
114
114
  program
@@ -164,7 +164,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
164
164
  workspace,
165
165
  });
166
166
  console.log(renderGenerateSummary(r));
167
- process.exitCode = r.ok ? 0 : 1;
167
+ process.exitCode = r.ok ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERIC_ERROR;
168
168
  });
169
169
  // ── Built-in: record ─────────────────────────────────────────────────────
170
170
  program
@@ -186,7 +186,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
186
186
  timeoutMs: parseInt(opts.timeout, 10),
187
187
  });
188
188
  console.log(renderRecordSummary(result));
189
- process.exitCode = result.candidateCount > 0 ? 0 : 1;
189
+ process.exitCode = result.candidateCount > 0 ? EXIT_CODES.SUCCESS : EXIT_CODES.EMPTY_RESULT;
190
190
  });
191
191
  program
192
192
  .command('cascade')
@@ -251,7 +251,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
251
251
  }
252
252
  catch (err) {
253
253
  console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
254
- process.exitCode = 1;
254
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
255
255
  }
256
256
  });
257
257
  pluginCmd
@@ -266,7 +266,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
266
266
  }
267
267
  catch (err) {
268
268
  console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
269
- process.exitCode = 1;
269
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
270
270
  }
271
271
  });
272
272
  pluginCmd
@@ -277,12 +277,12 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
277
277
  .action(async (name, opts) => {
278
278
  if (!name && !opts.all) {
279
279
  console.error(chalk.red('Error: Please specify a plugin name or use the --all flag.'));
280
- process.exitCode = 1;
280
+ process.exitCode = EXIT_CODES.USAGE_ERROR;
281
281
  return;
282
282
  }
283
283
  if (name && opts.all) {
284
284
  console.error(chalk.red('Error: Cannot specify both a plugin name and --all.'));
285
- process.exitCode = 1;
285
+ process.exitCode = EXIT_CODES.USAGE_ERROR;
286
286
  return;
287
287
  }
288
288
  const { updatePlugin, updateAllPlugins } = await import('./plugin.js');
@@ -309,7 +309,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
309
309
  console.log();
310
310
  if (hasErrors) {
311
311
  console.error(chalk.red('Completed with some errors.'));
312
- process.exitCode = 1;
312
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
313
313
  }
314
314
  else {
315
315
  console.log(chalk.green('✅ All plugins updated successfully.'));
@@ -323,7 +323,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
323
323
  }
324
324
  catch (err) {
325
325
  console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
326
- process.exitCode = 1;
326
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
327
327
  }
328
328
  });
329
329
  pluginCmd
@@ -408,7 +408,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
408
408
  }
409
409
  catch (err) {
410
410
  console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
411
- process.exitCode = 1;
411
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
412
412
  }
413
413
  });
414
414
  // ── External CLIs ─────────────────────────────────────────────────────────
@@ -421,7 +421,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
421
421
  const ext = externalClis.find(e => e.name === name);
422
422
  if (!ext) {
423
423
  console.error(chalk.red(`External CLI '${name}' not found in registry.`));
424
- process.exitCode = 1;
424
+ process.exitCode = EXIT_CODES.USAGE_ERROR;
425
425
  return;
426
426
  }
427
427
  installExternalCli(ext);
@@ -446,7 +446,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
446
446
  }
447
447
  catch (err) {
448
448
  console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
449
- process.exitCode = 1;
449
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
450
450
  }
451
451
  }
452
452
  for (const ext of externalClis) {
@@ -485,7 +485,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
485
485
  console.error(chalk.dim(` Tip: '${binary}' exists on your PATH. Use 'opencli register ${binary}' to add it as an external CLI.`));
486
486
  }
487
487
  program.outputHelp();
488
- process.exitCode = 1;
488
+ process.exitCode = EXIT_CODES.USAGE_ERROR;
489
489
  });
490
490
  program.parse();
491
491
  }
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { createServer } from 'node:http';
13
13
  import { CDPBridge } from '../../browser/cdp.js';
14
- import { getErrorMessage } from '../../errors.js';
14
+ import { EXIT_CODES, getErrorMessage } from '../../errors.js';
15
15
  // ─── Helpers ─────────────────────────────────────────────────────────
16
16
  function generateMsgId() {
17
17
  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
@@ -506,7 +506,7 @@ export async function startServe(opts = {}) {
506
506
  console.error('\n[serve] Shutting down...');
507
507
  cdp?.close().catch(() => { });
508
508
  server.close();
509
- process.exit(0);
509
+ process.exit(EXIT_CODES.SUCCESS);
510
510
  };
511
511
  process.on('SIGTERM', shutdown);
512
512
  process.on('SIGINT', shutdown);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,72 @@
1
+ import { AuthRequiredError, EmptyResultError } from '../../errors.js';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ /**
4
+ * band bands — List all Bands you belong to.
5
+ *
6
+ * Band.us renders the full band list in the left sidebar of the home page for
7
+ * logged-in users, so we can extract everything we need from the DOM without
8
+ * XHR interception or any secondary navigation.
9
+ *
10
+ * Each sidebar item is an <a href="/band/{band_no}/..."> link whose text and
11
+ * data attributes carry the band name and member count.
12
+ */
13
+ cli({
14
+ site: 'band',
15
+ name: 'bands',
16
+ description: 'List all Bands you belong to',
17
+ domain: 'www.band.us',
18
+ strategy: Strategy.COOKIE,
19
+ browser: true,
20
+ args: [],
21
+ columns: ['band_no', 'name', 'members'],
22
+ func: async (page, _kwargs) => {
23
+ const cookies = await page.getCookies({ domain: 'band.us' });
24
+ const isLoggedIn = cookies.some(c => c.name === 'band_session');
25
+ if (!isLoggedIn)
26
+ throw new AuthRequiredError('band.us', 'Not logged in to Band');
27
+ // Extract the band list from the sidebar. Poll until at least one band card
28
+ // appears (React hydration may take a moment after navigation).
29
+ // Sidebar band cards use class "bandCover _link" with hrefs like /band/{id}/post.
30
+ const bands = await page.evaluate(`
31
+ (async () => {
32
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
33
+
34
+ // Wait up to 9 s for sidebar band cards to render.
35
+ for (let i = 0; i < 30; i++) {
36
+ if (document.querySelector('a.bandCover._link')) break;
37
+ await sleep(300);
38
+ }
39
+
40
+ const norm = s => (s || '').replace(/\\s+/g, ' ').trim();
41
+ const seen = new Set();
42
+ const results = [];
43
+
44
+ for (const a of Array.from(document.querySelectorAll('a.bandCover._link'))) {
45
+ // Extract band_no from href: /band/{id} or /band/{id}/post only.
46
+ const m = (a.getAttribute('href') || '').match(/^\\/band\\/(\\d+)(?:\\/post)?\\/?$/);
47
+ if (!m) continue;
48
+ const bandNo = Number(m[1]);
49
+ if (seen.has(bandNo)) continue;
50
+ seen.add(bandNo);
51
+
52
+ // Band name lives in p.uriText inside div.bandName.
53
+ const nameEl = a.querySelector('p.uriText');
54
+ const name = nameEl ? norm(nameEl.textContent) : '';
55
+ if (!name) continue;
56
+
57
+ // Member count is the <em> inside span.member.
58
+ const memberEl = a.querySelector('span.member em');
59
+ const members = memberEl ? parseInt((memberEl.textContent || '').replace(/[^0-9]/g, ''), 10) || 0 : 0;
60
+
61
+ results.push({ band_no: bandNo, name, members });
62
+ }
63
+
64
+ return results;
65
+ })()
66
+ `);
67
+ if (!bands || bands.length === 0) {
68
+ throw new EmptyResultError('band bands', 'No bands found in sidebar — are you logged in?');
69
+ }
70
+ return bands;
71
+ },
72
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,127 @@
1
+ import { AuthRequiredError, EmptyResultError, SelectorError } from '../../errors.js';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ /**
4
+ * band mentions — Show Band notifications where you were @mentioned.
5
+ *
6
+ * Band.us signs every API request with a per-request HMAC (`md` header) generated
7
+ * by its own JavaScript, so we cannot replicate it externally. Instead we use
8
+ * Strategy.INTERCEPT: install an XHR interceptor, open the notification panel by
9
+ * clicking the bell to trigger the get_news XHR call, then apply client-side
10
+ * filtering to extract notifications matching the requested filter/unread options.
11
+ */
12
+ cli({
13
+ site: 'band',
14
+ name: 'mentions',
15
+ description: 'Show Band notifications where you are @mentioned',
16
+ domain: 'www.band.us',
17
+ strategy: Strategy.INTERCEPT,
18
+ browser: true,
19
+ args: [
20
+ {
21
+ name: 'filter',
22
+ default: 'mentioned',
23
+ choices: ['mentioned', 'all', 'post', 'comment'],
24
+ help: 'Filter: mentioned (default) | all | post | comment',
25
+ },
26
+ { name: 'limit', type: 'int', default: 20, help: 'Max results' },
27
+ { name: 'unread', type: 'bool', default: false, help: 'Show only unread notifications' },
28
+ ],
29
+ columns: ['time', 'band', 'type', 'from', 'text', 'url'],
30
+ func: async (page, kwargs) => {
31
+ const filter = kwargs.filter;
32
+ const limit = kwargs.limit;
33
+ const unreadOnly = kwargs.unread;
34
+ // Navigate with a timestamp param to force a fresh page load each run.
35
+ // Without this, same-URL navigation may skip the reload (preserving the JS context
36
+ // and leaving the notification panel open from a previous run).
37
+ await page.goto(`https://www.band.us/?_=${Date.now()}`);
38
+ const cookies = await page.getCookies({ domain: 'band.us' });
39
+ const isLoggedIn = cookies.some(c => c.name === 'band_session');
40
+ if (!isLoggedIn)
41
+ throw new AuthRequiredError('band.us', 'Not logged in to Band');
42
+ // Install XHR interceptor before any clicks so all get_news responses are captured.
43
+ await page.installInterceptor('get_news');
44
+ // Wait for the bell button to appear (React hydration) instead of a fixed sleep.
45
+ let bellReady = false;
46
+ for (let i = 0; i < 20; i++) {
47
+ const exists = await page.evaluate(`() => !!document.querySelector('button._btnWidgetIcon')`);
48
+ if (exists) {
49
+ bellReady = true;
50
+ break;
51
+ }
52
+ await page.wait(0.5);
53
+ }
54
+ if (!bellReady) {
55
+ throw new SelectorError('button._btnWidgetIcon', 'Notification bell not found. The Band.us UI may have changed.');
56
+ }
57
+ // Poll until a capture containing result_data.news arrives, up to maxSecs seconds.
58
+ // getInterceptedRequests() clears the array on each call, so captures are accumulated
59
+ // locally. The interceptor pattern 'get_news' also matches 'get_news_count' responses
60
+ // which don't have result_data.news — keep polling until the real news response arrives.
61
+ const waitForOneCapture = async (maxSecs = 8) => {
62
+ const captures = [];
63
+ for (let i = 0; i < maxSecs * 2; i++) {
64
+ await page.wait(0.5); // 0.5 seconds per iteration (page.wait takes seconds)
65
+ const reqs = await page.getInterceptedRequests();
66
+ if (reqs.length > 0) {
67
+ captures.push(...reqs);
68
+ if (captures.some((r) => Array.isArray(r?.result_data?.news)))
69
+ return captures;
70
+ }
71
+ }
72
+ return captures;
73
+ };
74
+ // Click the bell. Guard against the element disappearing between the readiness
75
+ // check and the click (e.g. due to a React re-render) to surface a clear error.
76
+ const bellClicked = await page.evaluate(`() => {
77
+ const el = document.querySelector('button._btnWidgetIcon');
78
+ if (!el) return false;
79
+ el.click();
80
+ return true;
81
+ }`);
82
+ if (!bellClicked) {
83
+ throw new SelectorError('button._btnWidgetIcon', 'Notification bell disappeared before click. The Band.us UI may have changed.');
84
+ }
85
+ const requests = await waitForOneCapture();
86
+ // Find the get_news response (has result_data.news); get_news_count responses do not.
87
+ const newsReq = requests.find((r) => Array.isArray(r?.result_data?.news));
88
+ if (!newsReq) {
89
+ throw new EmptyResultError('band mentions', 'Failed to capture get_news response from Band.us. Try running the command again.');
90
+ }
91
+ let items = newsReq.result_data.news ?? [];
92
+ if (items.length === 0) {
93
+ throw new EmptyResultError('band mentions', 'No notifications found');
94
+ }
95
+ // Apply filters client-side from the full notification list.
96
+ if (unreadOnly) {
97
+ items = items.filter((n) => n.is_new === true);
98
+ }
99
+ if (filter === 'mentioned') {
100
+ // 'filters' is Band's server-side tag array; 'referred' means you were @mentioned.
101
+ items = items.filter((n) => n.filters?.includes('referred'));
102
+ }
103
+ else if (filter === 'post') {
104
+ items = items.filter((n) => n.category === 'post');
105
+ }
106
+ else if (filter === 'comment') {
107
+ items = items.filter((n) => n.category === 'comment');
108
+ }
109
+ // Band markup tags (<band:mention uid="...">, <band:sticker>, etc.) appear in
110
+ // notification text; strip them to get plain readable content.
111
+ const stripBandTags = (s) => s.replace(/<\/?band:[^>]+>/g, '');
112
+ return items.slice(0, limit).map((n) => {
113
+ const ts = n.created_at ? new Date(n.created_at) : null;
114
+ return {
115
+ time: ts
116
+ ? ts.toLocaleString('ja-JP', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
117
+ : '',
118
+ band: n.band?.name ?? '',
119
+ // 'filters' is Band's server-side tag array; 'referred' means you were @mentioned.
120
+ type: n.filters?.includes('referred') ? '@mention' : n.category ?? '',
121
+ from: n.actor?.name ?? '',
122
+ text: stripBandTags(n.subtext ?? '').slice(0, 100),
123
+ url: n.action?.pc ?? '',
124
+ };
125
+ });
126
+ },
127
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,175 @@
1
+ import { AuthRequiredError, EmptyResultError } from '../../errors.js';
2
+ import { formatCookieHeader } from '../../download/index.js';
3
+ import { downloadMedia } from '../../download/media-download.js';
4
+ import { cli, Strategy } from '../../registry.js';
5
+ /**
6
+ * band post — Export full content of a Band post: body, comments, and optional photo download.
7
+ *
8
+ * Navigates directly to the post URL and extracts everything from the DOM.
9
+ * No XHR interception needed — Band renders the full post for logged-in users.
10
+ *
11
+ * Output rows:
12
+ * type=post → the post itself (author, date, body text)
13
+ * type=comment → top-level comment
14
+ * type=reply → reply to a comment (nested under its parent)
15
+ *
16
+ * Photo thumbnail URLs carry a ?type=sNNN suffix; stripping it yields full-res.
17
+ */
18
+ cli({
19
+ site: 'band',
20
+ name: 'post',
21
+ description: 'Export full content of a post including comments',
22
+ domain: 'www.band.us',
23
+ strategy: Strategy.COOKIE,
24
+ navigateBefore: false,
25
+ browser: true,
26
+ args: [
27
+ { name: 'band_no', positional: true, required: true, type: 'int', help: 'Band number' },
28
+ { name: 'post_no', positional: true, required: true, type: 'int', help: 'Post number' },
29
+ { name: 'output', type: 'str', default: '', help: 'Directory to save attached photos' },
30
+ { name: 'comments', type: 'bool', default: true, help: 'Include comments (default: true)' },
31
+ ],
32
+ columns: ['type', 'author', 'date', 'text'],
33
+ func: async (page, kwargs) => {
34
+ const bandNo = Number(kwargs.band_no);
35
+ const postNo = Number(kwargs.post_no);
36
+ const outputDir = kwargs.output;
37
+ const withComments = kwargs.comments;
38
+ await page.goto(`https://www.band.us/band/${bandNo}/post/${postNo}`);
39
+ const cookies = await page.getCookies({ domain: 'band.us' });
40
+ const isLoggedIn = cookies.some(c => c.name === 'band_session');
41
+ if (!isLoggedIn)
42
+ throw new AuthRequiredError('band.us', 'Not logged in to Band');
43
+ const data = await page.evaluate(`
44
+ (async () => {
45
+ const withComments = ${withComments};
46
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
47
+ const norm = s => (s || '').replace(/\\s+/g, ' ').trim();
48
+ // Band embeds <band:mention>, <band:sticker>, etc. in content — strip to plain text.
49
+ const stripTags = s => s.replace(/<\\/?band:[^>]+>/g, '');
50
+
51
+ // Wait up to 9 s for the post content to render (poll for the author link,
52
+ // which appears after React hydration fills the post header).
53
+ for (let i = 0; i < 30; i++) {
54
+ if (document.querySelector('._postWrapper a.text')) break;
55
+ await sleep(300);
56
+ }
57
+
58
+ const postCard = document.querySelector('._postWrapper');
59
+ const commentSection = postCard?.querySelector('.dPostCommentMainView');
60
+
61
+ // Author and date live in the post header, above the comment section.
62
+ // Exclude any matches inside the comment section to avoid picking up comment authors.
63
+ let author = '', date = '';
64
+ for (const el of (postCard?.querySelectorAll('a.text') || [])) {
65
+ if (!commentSection?.contains(el)) { author = norm(el.textContent); break; }
66
+ }
67
+ for (const el of (postCard?.querySelectorAll('time.time') || [])) {
68
+ if (!commentSection?.contains(el)) { date = norm(el.textContent); break; }
69
+ }
70
+
71
+ const bodyEl = postCard?.querySelector('.postText._postText');
72
+ const text = bodyEl ? stripTags(norm(bodyEl.innerText || bodyEl.textContent)) : '';
73
+
74
+ // Photo thumbnails have a ?type=sNNN query param; strip it for full-res URL.
75
+ // Use location.href as base so protocol-relative or relative URLs resolve correctly.
76
+ const photos = Array.from(postCard?.querySelectorAll('img._imgRecentPhoto, img._imgPhoto') || [])
77
+ .map(img => {
78
+ const src = img.getAttribute('src') || '';
79
+ if (!src) return '';
80
+ try { const u = new URL(src, location.href); return u.origin + u.pathname; }
81
+ catch { return ''; }
82
+ })
83
+ .filter(Boolean);
84
+
85
+ if (!withComments) return { author, date, text, photos, comments: [] };
86
+
87
+ // Wait up to 6 s for the comment list container to render.
88
+ // Wait for the container itself (not .cComment) so posts with zero comments
89
+ // don't incur a fixed 6s delay waiting for an element that never appears.
90
+ for (let i = 0; i < 20; i++) {
91
+ if (postCard?.querySelector('.sCommentList._heightDetectAreaForComment')) break;
92
+ await sleep(300);
93
+ }
94
+
95
+ // Recursively collect comments and their replies.
96
+ // Replies live in .sReplyList > .sCommentList, not in ._replyRegion.
97
+ function extractComments(container, depth) {
98
+ const results = [];
99
+ for (const el of container.querySelectorAll(':scope > .cComment')) {
100
+ results.push({
101
+ depth,
102
+ author: norm(el.querySelector('strong.name')?.textContent),
103
+ date: norm(el.querySelector('time.time')?.textContent),
104
+ text: stripTags(norm(el.querySelector('p.txt._commentContent')?.innerText || '')),
105
+ });
106
+ const replyList = el.querySelector('.sReplyList .sCommentList._heightDetectAreaForComment');
107
+ if (replyList) results.push(...extractComments(replyList, depth + 1));
108
+ }
109
+ return results;
110
+ }
111
+
112
+ const commentList = postCard?.querySelector('.sCommentList._heightDetectAreaForComment');
113
+ const comments = commentList ? extractComments(commentList, 0) : [];
114
+
115
+ return { author, date, text, photos, comments };
116
+ })()
117
+ `);
118
+ if (!data?.text && !data?.comments?.length && !data?.photos?.length) {
119
+ throw new EmptyResultError('band post', 'Post not found or not accessible');
120
+ }
121
+ const photos = data.photos ?? [];
122
+ // Download photos when --output is specified, using the shared downloadMedia utility
123
+ // which handles redirects, timeouts, and stream errors correctly.
124
+ // Pass browser cookies so Band's login-protected photo URLs don't fail with 401/403.
125
+ if (outputDir && photos.length > 0) {
126
+ // Only send Band cookies to Band-hosted URLs; avoid leaking auth cookies to third-party CDNs.
127
+ // Use a global index across both batches so filenames don't collide (photo_1, photo_2, ...).
128
+ const cookieHeader = formatCookieHeader(await page.getCookies({ url: 'https://www.band.us' }));
129
+ const isBandUrl = (u) => { try {
130
+ const h = new URL(u).hostname;
131
+ return h === 'band.us' || h.endsWith('.band.us');
132
+ }
133
+ catch {
134
+ return false;
135
+ } };
136
+ // Derive extension from URL path so downloaded files have correct extensions (e.g. photo_1.jpg).
137
+ const urlExt = (u) => { try {
138
+ return new URL(u).pathname.match(/\.(\w+)$/)?.[1] ?? 'jpg';
139
+ }
140
+ catch {
141
+ return 'jpg';
142
+ } };
143
+ let globalIndex = 1;
144
+ const bandPhotos = photos.filter(isBandUrl);
145
+ const otherPhotos = photos.filter(u => !isBandUrl(u));
146
+ if (bandPhotos.length > 0) {
147
+ await downloadMedia(bandPhotos.map(url => ({ type: 'image', url, filename: `photo_${globalIndex++}.${urlExt(url)}` })), { output: outputDir, verbose: false, cookies: cookieHeader });
148
+ }
149
+ if (otherPhotos.length > 0) {
150
+ await downloadMedia(otherPhotos.map(url => ({ type: 'image', url, filename: `photo_${globalIndex++}.${urlExt(url)}` })), { output: outputDir, verbose: false });
151
+ }
152
+ }
153
+ const rows = [];
154
+ // Post row — append photo URLs inline when not downloading to disk.
155
+ rows.push({
156
+ type: 'post',
157
+ author: data.author ?? '',
158
+ date: data.date ?? '',
159
+ text: [
160
+ data.text ?? '',
161
+ ...(outputDir ? [] : photos.map((u, i) => `[photo${i + 1}] ${u}`)),
162
+ ].filter(Boolean).join('\n'),
163
+ });
164
+ // Comment rows — depth=0 → type 'comment', depth≥1 → type 'reply'.
165
+ for (const c of data.comments ?? []) {
166
+ rows.push({
167
+ type: c.depth === 0 ? 'comment' : 'reply',
168
+ author: c.author ?? '',
169
+ date: c.date ?? '',
170
+ text: c.depth > 0 ? ' '.repeat(c.depth) + '└ ' + (c.text ?? '') : (c.text ?? ''),
171
+ });
172
+ }
173
+ return rows;
174
+ },
175
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
1
+ import { AuthRequiredError, EmptyResultError } from '../../errors.js';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ /**
4
+ * band posts — List posts from a specific Band.
5
+ *
6
+ * Band.us renders the post list in the DOM for logged-in users, so we navigate
7
+ * directly to the band's post page and extract everything from the DOM — no XHR
8
+ * interception or home-page detour required.
9
+ */
10
+ cli({
11
+ site: 'band',
12
+ name: 'posts',
13
+ description: 'List posts from a Band',
14
+ domain: 'www.band.us',
15
+ strategy: Strategy.COOKIE,
16
+ navigateBefore: false,
17
+ browser: true,
18
+ args: [
19
+ {
20
+ name: 'band_no',
21
+ positional: true,
22
+ required: true,
23
+ type: 'int',
24
+ help: 'Band number (get it from: band bands)',
25
+ },
26
+ { name: 'limit', type: 'int', default: 20, help: 'Max results' },
27
+ ],
28
+ columns: ['date', 'author', 'content', 'comments', 'url'],
29
+ func: async (page, kwargs) => {
30
+ const bandNo = Number(kwargs.band_no);
31
+ const limit = Number(kwargs.limit);
32
+ // Navigate directly to the band's post page — no home-page detour needed.
33
+ await page.goto(`https://www.band.us/band/${bandNo}/post`);
34
+ const cookies = await page.getCookies({ domain: 'band.us' });
35
+ const isLoggedIn = cookies.some(c => c.name === 'band_session');
36
+ if (!isLoggedIn)
37
+ throw new AuthRequiredError('band.us', 'Not logged in to Band');
38
+ // Extract post list from the DOM. Poll until post items appear (React hydration).
39
+ const posts = await page.evaluate(`
40
+ (async () => {
41
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
42
+ const norm = s => (s || '').replace(/\\s+/g, ' ').trim();
43
+ const limit = ${limit};
44
+
45
+ // Wait up to 9 s for post items to render.
46
+ for (let i = 0; i < 30; i++) {
47
+ if (document.querySelector('article.cContentsCard._postMainWrap')) break;
48
+ await sleep(300);
49
+ }
50
+
51
+ // Band embeds custom <band:mention>, <band:sticker>, etc. tags in content.
52
+ const stripTags = s => s.replace(/<\\/?band:[^>]+>/g, '');
53
+
54
+ const results = [];
55
+ const postEls = Array.from(
56
+ document.querySelectorAll('article.cContentsCard._postMainWrap')
57
+ );
58
+
59
+ for (const el of postEls) {
60
+ // URL: first post permalink link (absolute or relative).
61
+ const linkEl = el.querySelector('a[href*="/post/"]');
62
+ const href = linkEl?.getAttribute('href') || '';
63
+ if (!href) continue;
64
+ const url = href.startsWith('http') ? href : 'https://www.band.us' + href;
65
+
66
+ // Author name — a.text in the post header area.
67
+ const author = norm(el.querySelector('a.text')?.textContent);
68
+
69
+ // Date / timestamp.
70
+ const date = norm(el.querySelector('time')?.textContent);
71
+
72
+ // Post body text (strip Band markup tags, truncate for listing).
73
+ const bodyEl = el.querySelector('.postText._postText');
74
+ const content = bodyEl
75
+ ? stripTags(norm(bodyEl.innerText || bodyEl.textContent)).slice(0, 120)
76
+ : '';
77
+
78
+ // Comment count is in span.count inside the count area.
79
+ const commentEl = el.querySelector('span.count');
80
+ const comments = commentEl ? parseInt((commentEl.textContent || '').replace(/[^0-9]/g, ''), 10) || 0 : 0;
81
+
82
+ if (results.length >= limit) break;
83
+ results.push({ date, author, content, comments, url });
84
+ }
85
+
86
+ return results;
87
+ })()
88
+ `);
89
+ if (!posts || posts.length === 0) {
90
+ throw new EmptyResultError('band posts', 'No posts found in this Band');
91
+ }
92
+ return posts;
93
+ },
94
+ });
@@ -0,0 +1 @@
1
+ export declare const detailCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,33 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { DOUBAO_DOMAIN, getConversationDetail, parseDoubaoConversationId } from './utils.js';
3
+ export const detailCommand = cli({
4
+ site: 'doubao',
5
+ name: 'detail',
6
+ description: 'Read a specific Doubao conversation by ID',
7
+ domain: DOUBAO_DOMAIN,
8
+ strategy: Strategy.COOKIE,
9
+ browser: true,
10
+ navigateBefore: false,
11
+ args: [
12
+ { name: 'id', required: true, positional: true, help: 'Conversation ID (numeric or full URL)' },
13
+ ],
14
+ columns: ['Role', 'Text'],
15
+ func: async (page, kwargs) => {
16
+ const conversationId = parseDoubaoConversationId(kwargs.id);
17
+ const { messages, meeting } = await getConversationDetail(page, conversationId);
18
+ if (messages.length === 0 && !meeting) {
19
+ return [{ Role: 'System', Text: 'No messages found. Verify the conversation ID.' }];
20
+ }
21
+ const result = [];
22
+ if (meeting) {
23
+ result.push({
24
+ Role: 'Meeting',
25
+ Text: `${meeting.title}${meeting.time ? ` (${meeting.time})` : ''}`,
26
+ });
27
+ }
28
+ for (const m of messages) {
29
+ result.push({ Role: m.Role, Text: m.Text });
30
+ }
31
+ return result;
32
+ },
33
+ });
@@ -0,0 +1 @@
1
+ import './detail.js';