@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
@@ -339,6 +339,22 @@ export class Page implements IPage {
339
339
  return Array.isArray(result) ? result : [];
340
340
  }
341
341
 
342
+ /**
343
+ * Set local file paths on a file input element via CDP DOM.setFileInputFiles.
344
+ * Chrome reads the files directly from the local filesystem, avoiding the
345
+ * payload size limits of base64-in-evaluate.
346
+ */
347
+ async setFileInput(files: string[], selector?: string): Promise<void> {
348
+ const result = await sendCommand('set-file-input', {
349
+ files,
350
+ selector,
351
+ ...this._cmdOpts(),
352
+ }) as { count?: number };
353
+ if (!result?.count) {
354
+ throw new Error('setFileInput returned no count — command may not be supported by the extension');
355
+ }
356
+ }
357
+
342
358
  async waitForCapture(timeout: number = 10): Promise<void> {
343
359
  const maxMs = timeout * 1000;
344
360
  await sendCommand('exec', {
package/src/cli.ts CHANGED
@@ -15,7 +15,7 @@ import { PKG_VERSION } from './version.js';
15
15
  import { printCompletionScript } from './completion.js';
16
16
  import { loadExternalClis, executeExternalCli, installExternalCli, registerExternalCli, isBinaryInstalled } from './external.js';
17
17
  import { registerAllCommands } from './commanderAdapter.js';
18
- import { getErrorMessage } from './errors.js';
18
+ import { EXIT_CODES, getErrorMessage } from './errors.js';
19
19
 
20
20
  export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
21
21
  const program = new Command();
@@ -120,7 +120,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
120
120
  const { verifyClis, renderVerifyReport } = await import('./verify.js');
121
121
  const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke });
122
122
  console.log(renderVerifyReport(r));
123
- process.exitCode = r.ok ? 0 : 1;
123
+ process.exitCode = r.ok ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERIC_ERROR;
124
124
  });
125
125
 
126
126
  // ── Built-in: explore / synthesize / generate / cascade ───────────────────
@@ -180,7 +180,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
180
180
  workspace,
181
181
  });
182
182
  console.log(renderGenerateSummary(r));
183
- process.exitCode = r.ok ? 0 : 1;
183
+ process.exitCode = r.ok ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERIC_ERROR;
184
184
  });
185
185
 
186
186
  // ── Built-in: record ─────────────────────────────────────────────────────
@@ -204,7 +204,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
204
204
  timeoutMs: parseInt(opts.timeout, 10),
205
205
  });
206
206
  console.log(renderRecordSummary(result));
207
- process.exitCode = result.candidateCount > 0 ? 0 : 1;
207
+ process.exitCode = result.candidateCount > 0 ? EXIT_CODES.SUCCESS : EXIT_CODES.EMPTY_RESULT;
208
208
  });
209
209
 
210
210
  program
@@ -272,7 +272,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
272
272
  }
273
273
  } catch (err) {
274
274
  console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
275
- process.exitCode = 1;
275
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
276
276
  }
277
277
  });
278
278
 
@@ -287,7 +287,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
287
287
  console.log(chalk.green(`✅ Plugin "${name}" uninstalled.`));
288
288
  } catch (err) {
289
289
  console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
290
- process.exitCode = 1;
290
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
291
291
  }
292
292
  });
293
293
 
@@ -299,12 +299,12 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
299
299
  .action(async (name: string | undefined, opts: { all?: boolean }) => {
300
300
  if (!name && !opts.all) {
301
301
  console.error(chalk.red('Error: Please specify a plugin name or use the --all flag.'));
302
- process.exitCode = 1;
302
+ process.exitCode = EXIT_CODES.USAGE_ERROR;
303
303
  return;
304
304
  }
305
305
  if (name && opts.all) {
306
306
  console.error(chalk.red('Error: Cannot specify both a plugin name and --all.'));
307
- process.exitCode = 1;
307
+ process.exitCode = EXIT_CODES.USAGE_ERROR;
308
308
  return;
309
309
  }
310
310
 
@@ -335,7 +335,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
335
335
  console.log();
336
336
  if (hasErrors) {
337
337
  console.error(chalk.red('Completed with some errors.'));
338
- process.exitCode = 1;
338
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
339
339
  } else {
340
340
  console.log(chalk.green('✅ All plugins updated successfully.'));
341
341
  }
@@ -348,7 +348,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
348
348
  console.log(chalk.green(`✅ Plugin "${name}" updated successfully.`));
349
349
  } catch (err) {
350
350
  console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
351
- process.exitCode = 1;
351
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
352
352
  }
353
353
  });
354
354
 
@@ -438,7 +438,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
438
438
  console.log(chalk.dim(` opencli ${name} hello`));
439
439
  } catch (err) {
440
440
  console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
441
- process.exitCode = 1;
441
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
442
442
  }
443
443
  });
444
444
 
@@ -454,7 +454,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
454
454
  const ext = externalClis.find(e => e.name === name);
455
455
  if (!ext) {
456
456
  console.error(chalk.red(`External CLI '${name}' not found in registry.`));
457
- process.exitCode = 1;
457
+ process.exitCode = EXIT_CODES.USAGE_ERROR;
458
458
  return;
459
459
  }
460
460
  installExternalCli(ext);
@@ -480,7 +480,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
480
480
  executeExternalCli(name, args, externalClis);
481
481
  } catch (err) {
482
482
  console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
483
- process.exitCode = 1;
483
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
484
484
  }
485
485
  }
486
486
 
@@ -525,7 +525,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
525
525
  console.error(chalk.dim(` Tip: '${binary}' exists on your PATH. Use 'opencli register ${binary}' to add it as an external CLI.`));
526
526
  }
527
527
  program.outputHelp();
528
- process.exitCode = 1;
528
+ process.exitCode = EXIT_CODES.USAGE_ERROR;
529
529
  });
530
530
 
531
531
  program.parse();
@@ -13,7 +13,7 @@
13
13
  import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
14
14
  import { CDPBridge } from '../../browser/cdp.js';
15
15
  import type { IPage } from '../../types.js';
16
- import { getErrorMessage } from '../../errors.js';
16
+ import { EXIT_CODES, getErrorMessage } from '../../errors.js';
17
17
 
18
18
  // ─── Types ───────────────────────────────────────────────────────────
19
19
 
@@ -594,7 +594,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
594
594
  console.error('\n[serve] Shutting down...');
595
595
  cdp?.close().catch(() => {});
596
596
  server.close();
597
- process.exit(0);
597
+ process.exit(EXIT_CODES.SUCCESS);
598
598
  };
599
599
  process.on('SIGTERM', shutdown);
600
600
  process.on('SIGINT', shutdown);
@@ -0,0 +1,76 @@
1
+ import { AuthRequiredError, EmptyResultError } from '../../errors.js';
2
+ import { cli, Strategy } from '../../registry.js';
3
+
4
+ /**
5
+ * band bands — List all Bands you belong to.
6
+ *
7
+ * Band.us renders the full band list in the left sidebar of the home page for
8
+ * logged-in users, so we can extract everything we need from the DOM without
9
+ * XHR interception or any secondary navigation.
10
+ *
11
+ * Each sidebar item is an <a href="/band/{band_no}/..."> link whose text and
12
+ * data attributes carry the band name and member count.
13
+ */
14
+ cli({
15
+ site: 'band',
16
+ name: 'bands',
17
+ description: 'List all Bands you belong to',
18
+ domain: 'www.band.us',
19
+ strategy: Strategy.COOKIE,
20
+ browser: true,
21
+ args: [],
22
+ columns: ['band_no', 'name', 'members'],
23
+
24
+ func: async (page, _kwargs) => {
25
+ const cookies = await page.getCookies({ domain: 'band.us' });
26
+ const isLoggedIn = cookies.some(c => c.name === 'band_session');
27
+ if (!isLoggedIn) throw new AuthRequiredError('band.us', 'Not logged in to Band');
28
+
29
+ // Extract the band list from the sidebar. Poll until at least one band card
30
+ // appears (React hydration may take a moment after navigation).
31
+ // Sidebar band cards use class "bandCover _link" with hrefs like /band/{id}/post.
32
+ const bands: { band_no: number; name: string; members: number }[] = await page.evaluate(`
33
+ (async () => {
34
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
35
+
36
+ // Wait up to 9 s for sidebar band cards to render.
37
+ for (let i = 0; i < 30; i++) {
38
+ if (document.querySelector('a.bandCover._link')) break;
39
+ await sleep(300);
40
+ }
41
+
42
+ const norm = s => (s || '').replace(/\\s+/g, ' ').trim();
43
+ const seen = new Set();
44
+ const results = [];
45
+
46
+ for (const a of Array.from(document.querySelectorAll('a.bandCover._link'))) {
47
+ // Extract band_no from href: /band/{id} or /band/{id}/post only.
48
+ const m = (a.getAttribute('href') || '').match(/^\\/band\\/(\\d+)(?:\\/post)?\\/?$/);
49
+ if (!m) continue;
50
+ const bandNo = Number(m[1]);
51
+ if (seen.has(bandNo)) continue;
52
+ seen.add(bandNo);
53
+
54
+ // Band name lives in p.uriText inside div.bandName.
55
+ const nameEl = a.querySelector('p.uriText');
56
+ const name = nameEl ? norm(nameEl.textContent) : '';
57
+ if (!name) continue;
58
+
59
+ // Member count is the <em> inside span.member.
60
+ const memberEl = a.querySelector('span.member em');
61
+ const members = memberEl ? parseInt((memberEl.textContent || '').replace(/[^0-9]/g, ''), 10) || 0 : 0;
62
+
63
+ results.push({ band_no: bandNo, name, members });
64
+ }
65
+
66
+ return results;
67
+ })()
68
+ `);
69
+
70
+ if (!bands || bands.length === 0) {
71
+ throw new EmptyResultError('band bands', 'No bands found in sidebar — are you logged in?');
72
+ }
73
+
74
+ return bands;
75
+ },
76
+ });
@@ -0,0 +1,134 @@
1
+ import { AuthRequiredError, EmptyResultError, SelectorError } from '../../errors.js';
2
+ import { cli, Strategy } from '../../registry.js';
3
+
4
+ /**
5
+ * band mentions — Show Band notifications where you were @mentioned.
6
+ *
7
+ * Band.us signs every API request with a per-request HMAC (`md` header) generated
8
+ * by its own JavaScript, so we cannot replicate it externally. Instead we use
9
+ * Strategy.INTERCEPT: install an XHR interceptor, open the notification panel by
10
+ * clicking the bell to trigger the get_news XHR call, then apply client-side
11
+ * filtering to extract notifications matching the requested filter/unread options.
12
+ */
13
+ cli({
14
+ site: 'band',
15
+ name: 'mentions',
16
+ description: 'Show Band notifications where you are @mentioned',
17
+ domain: 'www.band.us',
18
+ strategy: Strategy.INTERCEPT,
19
+ browser: true,
20
+ args: [
21
+ {
22
+ name: 'filter',
23
+ default: 'mentioned',
24
+ choices: ['mentioned', 'all', 'post', 'comment'],
25
+ help: 'Filter: mentioned (default) | all | post | comment',
26
+ },
27
+ { name: 'limit', type: 'int', default: 20, help: 'Max results' },
28
+ { name: 'unread', type: 'bool', default: false, help: 'Show only unread notifications' },
29
+ ],
30
+ columns: ['time', 'band', 'type', 'from', 'text', 'url'],
31
+
32
+ func: async (page, kwargs) => {
33
+ const filter = kwargs.filter as string;
34
+ const limit = kwargs.limit as number;
35
+ const unreadOnly = kwargs.unread as boolean;
36
+
37
+ // Navigate with a timestamp param to force a fresh page load each run.
38
+ // Without this, same-URL navigation may skip the reload (preserving the JS context
39
+ // and leaving the notification panel open from a previous run).
40
+ await page.goto(`https://www.band.us/?_=${Date.now()}`);
41
+
42
+ const cookies = await page.getCookies({ domain: 'band.us' });
43
+ const isLoggedIn = cookies.some(c => c.name === 'band_session');
44
+ if (!isLoggedIn) throw new AuthRequiredError('band.us', 'Not logged in to Band');
45
+
46
+ // Install XHR interceptor before any clicks so all get_news responses are captured.
47
+ await page.installInterceptor('get_news');
48
+
49
+ // Wait for the bell button to appear (React hydration) instead of a fixed sleep.
50
+ let bellReady = false;
51
+ for (let i = 0; i < 20; i++) {
52
+ const exists = await page.evaluate(`() => !!document.querySelector('button._btnWidgetIcon')`);
53
+ if (exists) { bellReady = true; break; }
54
+ await page.wait(0.5);
55
+ }
56
+ if (!bellReady) {
57
+ throw new SelectorError('button._btnWidgetIcon', 'Notification bell not found. The Band.us UI may have changed.');
58
+ }
59
+
60
+ // Poll until a capture containing result_data.news arrives, up to maxSecs seconds.
61
+ // getInterceptedRequests() clears the array on each call, so captures are accumulated
62
+ // locally. The interceptor pattern 'get_news' also matches 'get_news_count' responses
63
+ // which don't have result_data.news — keep polling until the real news response arrives.
64
+ const waitForOneCapture = async (maxSecs = 8): Promise<any[]> => {
65
+ const captures: any[] = [];
66
+ for (let i = 0; i < maxSecs * 2; i++) {
67
+ await page.wait(0.5); // 0.5 seconds per iteration (page.wait takes seconds)
68
+ const reqs = await page.getInterceptedRequests();
69
+ if (reqs.length > 0) {
70
+ captures.push(...reqs);
71
+ if (captures.some((r: any) => Array.isArray(r?.result_data?.news))) return captures;
72
+ }
73
+ }
74
+ return captures;
75
+ };
76
+
77
+ // Click the bell. Guard against the element disappearing between the readiness
78
+ // check and the click (e.g. due to a React re-render) to surface a clear error.
79
+ const bellClicked = await page.evaluate(`() => {
80
+ const el = document.querySelector('button._btnWidgetIcon');
81
+ if (!el) return false;
82
+ el.click();
83
+ return true;
84
+ }`);
85
+ if (!bellClicked) {
86
+ throw new SelectorError('button._btnWidgetIcon', 'Notification bell disappeared before click. The Band.us UI may have changed.');
87
+ }
88
+
89
+ const requests = await waitForOneCapture();
90
+
91
+ // Find the get_news response (has result_data.news); get_news_count responses do not.
92
+ const newsReq = requests.find((r: any) => Array.isArray(r?.result_data?.news)) as any;
93
+ if (!newsReq) {
94
+ throw new EmptyResultError('band mentions', 'Failed to capture get_news response from Band.us. Try running the command again.');
95
+ }
96
+ let items: any[] = newsReq.result_data.news ?? [];
97
+
98
+ if (items.length === 0) {
99
+ throw new EmptyResultError('band mentions', 'No notifications found');
100
+ }
101
+
102
+ // Apply filters client-side from the full notification list.
103
+ if (unreadOnly) {
104
+ items = items.filter((n: any) => n.is_new === true);
105
+ }
106
+ if (filter === 'mentioned') {
107
+ // 'filters' is Band's server-side tag array; 'referred' means you were @mentioned.
108
+ items = items.filter((n: any) => n.filters?.includes('referred'));
109
+ } else if (filter === 'post') {
110
+ items = items.filter((n: any) => n.category === 'post');
111
+ } else if (filter === 'comment') {
112
+ items = items.filter((n: any) => n.category === 'comment');
113
+ }
114
+
115
+ // Band markup tags (<band:mention uid="...">, <band:sticker>, etc.) appear in
116
+ // notification text; strip them to get plain readable content.
117
+ const stripBandTags = (s: string) => s.replace(/<\/?band:[^>]+>/g, '');
118
+
119
+ return items.slice(0, limit).map((n: any) => {
120
+ const ts = n.created_at ? new Date(n.created_at) : null;
121
+ return {
122
+ time: ts
123
+ ? ts.toLocaleString('ja-JP', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
124
+ : '',
125
+ band: n.band?.name ?? '',
126
+ // 'filters' is Band's server-side tag array; 'referred' means you were @mentioned.
127
+ type: n.filters?.includes('referred') ? '@mention' : n.category ?? '',
128
+ from: n.actor?.name ?? '',
129
+ text: stripBandTags(n.subtext ?? '').slice(0, 100),
130
+ url: n.action?.pc ?? '',
131
+ };
132
+ });
133
+ },
134
+ });
@@ -0,0 +1,187 @@
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
+ /**
7
+ * band post — Export full content of a Band post: body, comments, and optional photo download.
8
+ *
9
+ * Navigates directly to the post URL and extracts everything from the DOM.
10
+ * No XHR interception needed — Band renders the full post for logged-in users.
11
+ *
12
+ * Output rows:
13
+ * type=post → the post itself (author, date, body text)
14
+ * type=comment → top-level comment
15
+ * type=reply → reply to a comment (nested under its parent)
16
+ *
17
+ * Photo thumbnail URLs carry a ?type=sNNN suffix; stripping it yields full-res.
18
+ */
19
+ cli({
20
+ site: 'band',
21
+ name: 'post',
22
+ description: 'Export full content of a post including comments',
23
+ domain: 'www.band.us',
24
+ strategy: Strategy.COOKIE,
25
+ navigateBefore: false,
26
+ browser: true,
27
+ args: [
28
+ { name: 'band_no', positional: true, required: true, type: 'int', help: 'Band number' },
29
+ { name: 'post_no', positional: true, required: true, type: 'int', help: 'Post number' },
30
+ { name: 'output', type: 'str', default: '', help: 'Directory to save attached photos' },
31
+ { name: 'comments', type: 'bool', default: true, help: 'Include comments (default: true)' },
32
+ ],
33
+ columns: ['type', 'author', 'date', 'text'],
34
+
35
+ func: async (page, kwargs) => {
36
+ const bandNo = Number(kwargs.band_no);
37
+ const postNo = Number(kwargs.post_no);
38
+ const outputDir = kwargs.output as string;
39
+ const withComments = kwargs.comments as boolean;
40
+
41
+ await page.goto(`https://www.band.us/band/${bandNo}/post/${postNo}`);
42
+
43
+ const cookies = await page.getCookies({ domain: 'band.us' });
44
+ const isLoggedIn = cookies.some(c => c.name === 'band_session');
45
+ if (!isLoggedIn) throw new AuthRequiredError('band.us', 'Not logged in to Band');
46
+
47
+ const data: {
48
+ author: string;
49
+ date: string;
50
+ text: string;
51
+ photos: string[];
52
+ comments: { depth: number; author: string; date: string; text: string }[];
53
+ } = await page.evaluate(`
54
+ (async () => {
55
+ const withComments = ${withComments};
56
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
57
+ const norm = s => (s || '').replace(/\\s+/g, ' ').trim();
58
+ // Band embeds <band:mention>, <band:sticker>, etc. in content — strip to plain text.
59
+ const stripTags = s => s.replace(/<\\/?band:[^>]+>/g, '');
60
+
61
+ // Wait up to 9 s for the post content to render (poll for the author link,
62
+ // which appears after React hydration fills the post header).
63
+ for (let i = 0; i < 30; i++) {
64
+ if (document.querySelector('._postWrapper a.text')) break;
65
+ await sleep(300);
66
+ }
67
+
68
+ const postCard = document.querySelector('._postWrapper');
69
+ const commentSection = postCard?.querySelector('.dPostCommentMainView');
70
+
71
+ // Author and date live in the post header, above the comment section.
72
+ // Exclude any matches inside the comment section to avoid picking up comment authors.
73
+ let author = '', date = '';
74
+ for (const el of (postCard?.querySelectorAll('a.text') || [])) {
75
+ if (!commentSection?.contains(el)) { author = norm(el.textContent); break; }
76
+ }
77
+ for (const el of (postCard?.querySelectorAll('time.time') || [])) {
78
+ if (!commentSection?.contains(el)) { date = norm(el.textContent); break; }
79
+ }
80
+
81
+ const bodyEl = postCard?.querySelector('.postText._postText');
82
+ const text = bodyEl ? stripTags(norm(bodyEl.innerText || bodyEl.textContent)) : '';
83
+
84
+ // Photo thumbnails have a ?type=sNNN query param; strip it for full-res URL.
85
+ // Use location.href as base so protocol-relative or relative URLs resolve correctly.
86
+ const photos = Array.from(postCard?.querySelectorAll('img._imgRecentPhoto, img._imgPhoto') || [])
87
+ .map(img => {
88
+ const src = img.getAttribute('src') || '';
89
+ if (!src) return '';
90
+ try { const u = new URL(src, location.href); return u.origin + u.pathname; }
91
+ catch { return ''; }
92
+ })
93
+ .filter(Boolean);
94
+
95
+ if (!withComments) return { author, date, text, photos, comments: [] };
96
+
97
+ // Wait up to 6 s for the comment list container to render.
98
+ // Wait for the container itself (not .cComment) so posts with zero comments
99
+ // don't incur a fixed 6s delay waiting for an element that never appears.
100
+ for (let i = 0; i < 20; i++) {
101
+ if (postCard?.querySelector('.sCommentList._heightDetectAreaForComment')) break;
102
+ await sleep(300);
103
+ }
104
+
105
+ // Recursively collect comments and their replies.
106
+ // Replies live in .sReplyList > .sCommentList, not in ._replyRegion.
107
+ function extractComments(container, depth) {
108
+ const results = [];
109
+ for (const el of container.querySelectorAll(':scope > .cComment')) {
110
+ results.push({
111
+ depth,
112
+ author: norm(el.querySelector('strong.name')?.textContent),
113
+ date: norm(el.querySelector('time.time')?.textContent),
114
+ text: stripTags(norm(el.querySelector('p.txt._commentContent')?.innerText || '')),
115
+ });
116
+ const replyList = el.querySelector('.sReplyList .sCommentList._heightDetectAreaForComment');
117
+ if (replyList) results.push(...extractComments(replyList, depth + 1));
118
+ }
119
+ return results;
120
+ }
121
+
122
+ const commentList = postCard?.querySelector('.sCommentList._heightDetectAreaForComment');
123
+ const comments = commentList ? extractComments(commentList, 0) : [];
124
+
125
+ return { author, date, text, photos, comments };
126
+ })()
127
+ `);
128
+
129
+ if (!data?.text && !data?.comments?.length && !data?.photos?.length) {
130
+ throw new EmptyResultError('band post', 'Post not found or not accessible');
131
+ }
132
+
133
+ const photos: string[] = data.photos ?? [];
134
+
135
+ // Download photos when --output is specified, using the shared downloadMedia utility
136
+ // which handles redirects, timeouts, and stream errors correctly.
137
+ // Pass browser cookies so Band's login-protected photo URLs don't fail with 401/403.
138
+ if (outputDir && photos.length > 0) {
139
+ // Only send Band cookies to Band-hosted URLs; avoid leaking auth cookies to third-party CDNs.
140
+ // Use a global index across both batches so filenames don't collide (photo_1, photo_2, ...).
141
+ const cookieHeader = formatCookieHeader(await page.getCookies({ url: 'https://www.band.us' }));
142
+ const isBandUrl = (u: string) => { try { const h = new URL(u).hostname; return h === 'band.us' || h.endsWith('.band.us'); } catch { return false; } };
143
+ // Derive extension from URL path so downloaded files have correct extensions (e.g. photo_1.jpg).
144
+ const urlExt = (u: string) => { try { return new URL(u).pathname.match(/\.(\w+)$/)?.[1] ?? 'jpg'; } catch { return 'jpg'; } };
145
+ let globalIndex = 1;
146
+ const bandPhotos = photos.filter(isBandUrl);
147
+ const otherPhotos = photos.filter(u => !isBandUrl(u));
148
+ if (bandPhotos.length > 0) {
149
+ await downloadMedia(
150
+ bandPhotos.map(url => ({ type: 'image' as const, url, filename: `photo_${globalIndex++}.${urlExt(url)}` })),
151
+ { output: outputDir, verbose: false, cookies: cookieHeader },
152
+ );
153
+ }
154
+ if (otherPhotos.length > 0) {
155
+ await downloadMedia(
156
+ otherPhotos.map(url => ({ type: 'image' as const, url, filename: `photo_${globalIndex++}.${urlExt(url)}` })),
157
+ { output: outputDir, verbose: false },
158
+ );
159
+ }
160
+ }
161
+
162
+ const rows: Record<string, string>[] = [];
163
+
164
+ // Post row — append photo URLs inline when not downloading to disk.
165
+ rows.push({
166
+ type: 'post',
167
+ author: data.author ?? '',
168
+ date: data.date ?? '',
169
+ text: [
170
+ data.text ?? '',
171
+ ...(outputDir ? [] : photos.map((u, i) => `[photo${i + 1}] ${u}`)),
172
+ ].filter(Boolean).join('\n'),
173
+ });
174
+
175
+ // Comment rows — depth=0 → type 'comment', depth≥1 → type 'reply'.
176
+ for (const c of data.comments ?? []) {
177
+ rows.push({
178
+ type: c.depth === 0 ? 'comment' : 'reply',
179
+ author: c.author ?? '',
180
+ date: c.date ?? '',
181
+ text: c.depth > 0 ? ' '.repeat(c.depth) + '└ ' + (c.text ?? '') : (c.text ?? ''),
182
+ });
183
+ }
184
+
185
+ return rows;
186
+ },
187
+ });
@@ -0,0 +1,106 @@
1
+ import { AuthRequiredError, EmptyResultError } from '../../errors.js';
2
+ import { cli, Strategy } from '../../registry.js';
3
+
4
+ /**
5
+ * band posts — List posts from a specific Band.
6
+ *
7
+ * Band.us renders the post list in the DOM for logged-in users, so we navigate
8
+ * directly to the band's post page and extract everything from the DOM — no XHR
9
+ * interception or home-page detour required.
10
+ */
11
+ cli({
12
+ site: 'band',
13
+ name: 'posts',
14
+ description: 'List posts from a Band',
15
+ domain: 'www.band.us',
16
+ strategy: Strategy.COOKIE,
17
+ navigateBefore: false,
18
+ browser: true,
19
+ args: [
20
+ {
21
+ name: 'band_no',
22
+ positional: true,
23
+ required: true,
24
+ type: 'int',
25
+ help: 'Band number (get it from: band bands)',
26
+ },
27
+ { name: 'limit', type: 'int', default: 20, help: 'Max results' },
28
+ ],
29
+ columns: ['date', 'author', 'content', 'comments', 'url'],
30
+
31
+ func: async (page, kwargs) => {
32
+ const bandNo = Number(kwargs.band_no);
33
+ const limit = Number(kwargs.limit);
34
+
35
+ // Navigate directly to the band's post page — no home-page detour needed.
36
+ await page.goto(`https://www.band.us/band/${bandNo}/post`);
37
+
38
+ const cookies = await page.getCookies({ domain: 'band.us' });
39
+ const isLoggedIn = cookies.some(c => c.name === 'band_session');
40
+ if (!isLoggedIn) throw new AuthRequiredError('band.us', 'Not logged in to Band');
41
+
42
+ // Extract post list from the DOM. Poll until post items appear (React hydration).
43
+ const posts: {
44
+ date: string;
45
+ author: string;
46
+ content: string;
47
+ comments: number;
48
+ url: string;
49
+ }[] = await page.evaluate(`
50
+ (async () => {
51
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
52
+ const norm = s => (s || '').replace(/\\s+/g, ' ').trim();
53
+ const limit = ${limit};
54
+
55
+ // Wait up to 9 s for post items to render.
56
+ for (let i = 0; i < 30; i++) {
57
+ if (document.querySelector('article.cContentsCard._postMainWrap')) break;
58
+ await sleep(300);
59
+ }
60
+
61
+ // Band embeds custom <band:mention>, <band:sticker>, etc. tags in content.
62
+ const stripTags = s => s.replace(/<\\/?band:[^>]+>/g, '');
63
+
64
+ const results = [];
65
+ const postEls = Array.from(
66
+ document.querySelectorAll('article.cContentsCard._postMainWrap')
67
+ );
68
+
69
+ for (const el of postEls) {
70
+ // URL: first post permalink link (absolute or relative).
71
+ const linkEl = el.querySelector('a[href*="/post/"]');
72
+ const href = linkEl?.getAttribute('href') || '';
73
+ if (!href) continue;
74
+ const url = href.startsWith('http') ? href : 'https://www.band.us' + href;
75
+
76
+ // Author name — a.text in the post header area.
77
+ const author = norm(el.querySelector('a.text')?.textContent);
78
+
79
+ // Date / timestamp.
80
+ const date = norm(el.querySelector('time')?.textContent);
81
+
82
+ // Post body text (strip Band markup tags, truncate for listing).
83
+ const bodyEl = el.querySelector('.postText._postText');
84
+ const content = bodyEl
85
+ ? stripTags(norm(bodyEl.innerText || bodyEl.textContent)).slice(0, 120)
86
+ : '';
87
+
88
+ // Comment count is in span.count inside the count area.
89
+ const commentEl = el.querySelector('span.count');
90
+ const comments = commentEl ? parseInt((commentEl.textContent || '').replace(/[^0-9]/g, ''), 10) || 0 : 0;
91
+
92
+ if (results.length >= limit) break;
93
+ results.push({ date, author, content, comments, url });
94
+ }
95
+
96
+ return results;
97
+ })()
98
+ `);
99
+
100
+ if (!posts || posts.length === 0) {
101
+ throw new EmptyResultError('band posts', 'No posts found in this Band');
102
+ }
103
+
104
+ return posts;
105
+ },
106
+ });