@jackwener/opencli 1.5.6 → 1.5.8

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 (338) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +4 -2
  3. package/README.zh-CN.md +4 -1
  4. package/SKILL.md +879 -0
  5. package/dist/browser/cdp.d.ts +1 -0
  6. package/dist/browser/cdp.js +30 -27
  7. package/dist/browser/daemon-client.d.ts +7 -1
  8. package/dist/browser/daemon-client.js +3 -0
  9. package/dist/browser/dom-helpers.js +1 -0
  10. package/dist/browser/dom-helpers.test.js +14 -1
  11. package/dist/browser/mcp.js +18 -13
  12. package/dist/browser/page.js +22 -2
  13. package/dist/browser/page.test.d.ts +1 -0
  14. package/dist/browser/page.test.js +44 -0
  15. package/dist/browser/stealth.js +198 -0
  16. package/dist/browser/stealth.test.d.ts +1 -0
  17. package/dist/browser/stealth.test.js +134 -0
  18. package/dist/browser.test.js +1 -1
  19. package/dist/build-manifest.d.ts +1 -0
  20. package/dist/build-manifest.js +5 -1
  21. package/dist/build-manifest.test.js +2 -0
  22. package/dist/cli-manifest.json +544 -137
  23. package/dist/cli.js +20 -3
  24. package/dist/clis/antigravity/serve.d.ts +1 -1
  25. package/dist/clis/antigravity/serve.js +5 -8
  26. package/dist/clis/bilibili/subtitle.js +4 -0
  27. package/dist/clis/bilibili/subtitle.test.d.ts +1 -0
  28. package/dist/clis/bilibili/subtitle.test.js +48 -0
  29. package/dist/clis/chatwise/ask.js +0 -2
  30. package/dist/clis/chatwise/export.js +0 -2
  31. package/dist/clis/chatwise/history.js +0 -2
  32. package/dist/clis/chatwise/model.js +0 -2
  33. package/dist/clis/chatwise/new.js +1 -2
  34. package/dist/clis/chatwise/read.js +0 -2
  35. package/dist/clis/chatwise/screenshot.js +1 -2
  36. package/dist/clis/chatwise/send.js +0 -2
  37. package/dist/clis/chatwise/status.js +1 -2
  38. package/dist/clis/ctrip/search.d.ts +13 -0
  39. package/dist/clis/ctrip/search.js +73 -48
  40. package/dist/clis/ctrip/search.test.d.ts +1 -0
  41. package/dist/clis/ctrip/search.test.js +64 -0
  42. package/dist/clis/douyin/_shared/sts2.js +8 -2
  43. package/dist/clis/douyin/_shared/sts2.test.d.ts +1 -0
  44. package/dist/clis/douyin/_shared/sts2.test.js +27 -0
  45. package/dist/clis/douyin/activities.js +4 -2
  46. package/dist/clis/douyin/activities.test.js +34 -1
  47. package/dist/clis/douyin/collections.js +1 -1
  48. package/dist/clis/douyin/collections.test.js +24 -2
  49. package/dist/clis/douyin/draft.d.ts +8 -11
  50. package/dist/clis/douyin/draft.js +302 -185
  51. package/dist/clis/douyin/draft.test.d.ts +1 -1
  52. package/dist/clis/douyin/draft.test.js +357 -2
  53. package/dist/clis/douyin/hashtag.js +9 -2
  54. package/dist/clis/douyin/hashtag.test.js +35 -2
  55. package/dist/clis/douyin/profile.js +1 -1
  56. package/dist/clis/douyin/profile.test.js +36 -1
  57. package/dist/clis/douyin/videos.js +22 -5
  58. package/dist/clis/douyin/videos.test.js +45 -2
  59. package/dist/clis/facebook/search.test.d.ts +5 -0
  60. package/dist/clis/facebook/search.test.js +60 -0
  61. package/dist/clis/facebook/search.yaml +4 -3
  62. package/dist/clis/instagram/download.d.ts +16 -0
  63. package/dist/clis/instagram/download.js +225 -0
  64. package/dist/clis/instagram/download.test.d.ts +1 -0
  65. package/dist/clis/instagram/download.test.js +118 -0
  66. package/dist/clis/notebooklm/bind-current.d.ts +1 -0
  67. package/dist/clis/notebooklm/bind-current.js +29 -0
  68. package/dist/clis/notebooklm/bind-current.test.d.ts +1 -0
  69. package/dist/clis/notebooklm/bind-current.test.js +35 -0
  70. package/dist/clis/notebooklm/binding.test.d.ts +1 -0
  71. package/dist/clis/notebooklm/binding.test.js +44 -0
  72. package/dist/clis/notebooklm/compat.test.d.ts +3 -0
  73. package/dist/clis/notebooklm/compat.test.js +16 -0
  74. package/dist/clis/notebooklm/current.d.ts +1 -0
  75. package/dist/clis/notebooklm/current.js +28 -0
  76. package/dist/clis/notebooklm/get.d.ts +1 -0
  77. package/dist/clis/notebooklm/get.js +37 -0
  78. package/dist/clis/notebooklm/history.d.ts +1 -0
  79. package/dist/clis/notebooklm/history.js +25 -0
  80. package/dist/clis/notebooklm/history.test.d.ts +1 -0
  81. package/dist/clis/notebooklm/history.test.js +58 -0
  82. package/dist/clis/notebooklm/list.d.ts +1 -0
  83. package/dist/clis/notebooklm/list.js +35 -0
  84. package/dist/clis/notebooklm/note-list.d.ts +1 -0
  85. package/dist/clis/notebooklm/note-list.js +28 -0
  86. package/dist/clis/notebooklm/note-list.test.d.ts +1 -0
  87. package/dist/clis/notebooklm/note-list.test.js +56 -0
  88. package/dist/clis/notebooklm/notes-get.d.ts +1 -0
  89. package/dist/clis/notebooklm/notes-get.js +47 -0
  90. package/dist/clis/notebooklm/notes-get.test.d.ts +1 -0
  91. package/dist/clis/notebooklm/notes-get.test.js +72 -0
  92. package/dist/clis/notebooklm/rpc.d.ts +36 -0
  93. package/dist/clis/notebooklm/rpc.js +189 -0
  94. package/dist/clis/notebooklm/rpc.test.d.ts +1 -0
  95. package/dist/clis/notebooklm/rpc.test.js +105 -0
  96. package/dist/clis/notebooklm/shared.d.ts +87 -0
  97. package/dist/clis/notebooklm/shared.js +3 -0
  98. package/dist/clis/notebooklm/source-fulltext.d.ts +1 -0
  99. package/dist/clis/notebooklm/source-fulltext.js +44 -0
  100. package/dist/clis/notebooklm/source-fulltext.test.d.ts +1 -0
  101. package/dist/clis/notebooklm/source-fulltext.test.js +106 -0
  102. package/dist/clis/notebooklm/source-get.d.ts +1 -0
  103. package/dist/clis/notebooklm/source-get.js +40 -0
  104. package/dist/clis/notebooklm/source-get.test.d.ts +1 -0
  105. package/dist/clis/notebooklm/source-get.test.js +84 -0
  106. package/dist/clis/notebooklm/source-guide.d.ts +1 -0
  107. package/dist/clis/notebooklm/source-guide.js +44 -0
  108. package/dist/clis/notebooklm/source-guide.test.d.ts +1 -0
  109. package/dist/clis/notebooklm/source-guide.test.js +104 -0
  110. package/dist/clis/notebooklm/source-list.d.ts +1 -0
  111. package/dist/clis/notebooklm/source-list.js +30 -0
  112. package/dist/clis/notebooklm/status.d.ts +1 -0
  113. package/dist/clis/notebooklm/status.js +31 -0
  114. package/dist/clis/notebooklm/summary.d.ts +1 -0
  115. package/dist/clis/notebooklm/summary.js +30 -0
  116. package/dist/clis/notebooklm/summary.test.d.ts +1 -0
  117. package/dist/clis/notebooklm/summary.test.js +78 -0
  118. package/dist/clis/notebooklm/utils.d.ts +37 -0
  119. package/dist/clis/notebooklm/utils.js +739 -0
  120. package/dist/clis/notebooklm/utils.test.d.ts +1 -0
  121. package/dist/clis/notebooklm/utils.test.js +390 -0
  122. package/dist/clis/substack/utils.d.ts +4 -0
  123. package/dist/clis/substack/utils.js +8 -2
  124. package/dist/clis/substack/utils.test.d.ts +1 -0
  125. package/dist/clis/substack/utils.test.js +46 -0
  126. package/dist/clis/v2ex/hot.yaml +4 -1
  127. package/dist/clis/v2ex/latest.yaml +4 -1
  128. package/dist/clis/v2ex/topic.yaml +6 -1
  129. package/dist/clis/weixin/download.d.ts +9 -0
  130. package/dist/clis/weixin/download.js +76 -6
  131. package/dist/clis/weread/book.js +108 -2
  132. package/dist/clis/weread/commands.test.js +262 -152
  133. package/dist/clis/weread/utils.d.ts +10 -0
  134. package/dist/clis/weread/utils.js +27 -7
  135. package/dist/clis/xiaohongshu/comments.d.ts +3 -0
  136. package/dist/clis/xiaohongshu/comments.js +76 -17
  137. package/dist/clis/xiaohongshu/comments.test.js +70 -9
  138. package/dist/clis/xiaohongshu/download.d.ts +4 -1
  139. package/dist/clis/xiaohongshu/download.js +83 -22
  140. package/dist/clis/xiaohongshu/download.test.d.ts +1 -0
  141. package/dist/clis/xiaohongshu/download.test.js +75 -0
  142. package/dist/clis/xiaohongshu/note-helpers.d.ts +12 -0
  143. package/dist/clis/xiaohongshu/note-helpers.js +23 -0
  144. package/dist/clis/xiaohongshu/note.d.ts +7 -0
  145. package/dist/clis/xiaohongshu/note.js +76 -0
  146. package/dist/clis/xiaohongshu/note.test.d.ts +1 -0
  147. package/dist/clis/xiaohongshu/note.test.js +136 -0
  148. package/dist/clis/xiaohongshu/search.js +9 -0
  149. package/dist/clis/xiaohongshu/search.test.js +10 -4
  150. package/dist/clis/youtube/search.js +57 -17
  151. package/dist/clis/zhihu/question.js +19 -17
  152. package/dist/clis/zhihu/question.test.d.ts +1 -0
  153. package/dist/clis/zhihu/question.test.js +54 -0
  154. package/dist/commanderAdapter.js +9 -0
  155. package/dist/commanderAdapter.test.js +25 -0
  156. package/dist/commands/daemon.d.ts +9 -0
  157. package/dist/commands/daemon.js +124 -0
  158. package/dist/commands/daemon.test.d.ts +1 -0
  159. package/dist/commands/daemon.test.js +185 -0
  160. package/dist/completion.js +3 -1
  161. package/dist/constants.d.ts +2 -0
  162. package/dist/constants.js +2 -0
  163. package/dist/daemon.d.ts +1 -1
  164. package/dist/daemon.js +25 -14
  165. package/dist/daemon.test.d.ts +1 -0
  166. package/dist/daemon.test.js +65 -0
  167. package/dist/discovery.d.ts +9 -0
  168. package/dist/discovery.js +47 -2
  169. package/dist/electron-apps.d.ts +29 -0
  170. package/dist/electron-apps.js +65 -0
  171. package/dist/electron-apps.test.d.ts +1 -0
  172. package/dist/electron-apps.test.js +43 -0
  173. package/dist/engine.test.js +41 -9
  174. package/dist/execution.js +20 -16
  175. package/dist/extension-manifest-regression.test.js +1 -0
  176. package/dist/idle-manager.d.ts +19 -0
  177. package/dist/idle-manager.js +54 -0
  178. package/dist/launcher.d.ts +36 -0
  179. package/dist/launcher.js +152 -0
  180. package/dist/launcher.test.d.ts +1 -0
  181. package/dist/launcher.test.js +57 -0
  182. package/dist/main.js +3 -3
  183. package/dist/registry.d.ts +1 -0
  184. package/dist/registry.js +31 -3
  185. package/dist/registry.test.js +13 -0
  186. package/dist/runtime.d.ts +5 -3
  187. package/dist/runtime.js +12 -5
  188. package/dist/serialization.d.ts +1 -0
  189. package/dist/serialization.js +3 -0
  190. package/dist/serialization.test.js +17 -1
  191. package/dist/tui.d.ts +7 -0
  192. package/dist/tui.js +52 -0
  193. package/dist/tui.test.d.ts +1 -0
  194. package/dist/tui.test.js +19 -0
  195. package/dist/weixin-download.test.js +14 -0
  196. package/docs/.vitepress/config.mts +1 -0
  197. package/docs/adapters/browser/notebooklm.md +69 -0
  198. package/docs/adapters/browser/xiaohongshu.md +19 -10
  199. package/docs/adapters/index.md +67 -66
  200. package/docs/guide/browser-bridge.md +12 -0
  201. package/docs/guide/troubleshooting.md +9 -4
  202. package/docs/superpowers/plans/2026-03-31-daemon-lifecycle-redesign.md +857 -0
  203. package/docs/superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md +208 -0
  204. package/docs/zh/guide/browser-bridge.md +12 -0
  205. package/extension/dist/background.js +250 -11
  206. package/extension/manifest.json +2 -1
  207. package/extension/src/background.test.ts +202 -2
  208. package/extension/src/background.ts +175 -10
  209. package/extension/src/cdp.test.ts +75 -0
  210. package/extension/src/cdp.ts +89 -3
  211. package/extension/src/protocol.ts +7 -5
  212. package/package.json +1 -1
  213. package/src/browser/cdp.ts +24 -17
  214. package/src/browser/daemon-client.ts +7 -1
  215. package/src/browser/dom-helpers.test.ts +15 -1
  216. package/src/browser/dom-helpers.ts +1 -0
  217. package/src/browser/mcp.ts +18 -13
  218. package/src/browser/page.test.ts +58 -0
  219. package/src/browser/page.ts +18 -2
  220. package/src/browser/stealth.test.ts +153 -0
  221. package/src/browser/stealth.ts +198 -0
  222. package/src/browser.test.ts +1 -1
  223. package/src/build-manifest.test.ts +2 -0
  224. package/src/build-manifest.ts +6 -1
  225. package/src/cli.ts +21 -3
  226. package/src/clis/antigravity/SKILL.md +3 -12
  227. package/src/clis/antigravity/serve.ts +5 -10
  228. package/src/clis/bilibili/subtitle.test.ts +60 -0
  229. package/src/clis/bilibili/subtitle.ts +4 -0
  230. package/src/clis/chatwise/ask.ts +0 -2
  231. package/src/clis/chatwise/export.ts +0 -2
  232. package/src/clis/chatwise/history.ts +0 -2
  233. package/src/clis/chatwise/model.ts +0 -2
  234. package/src/clis/chatwise/new.ts +1 -2
  235. package/src/clis/chatwise/read.ts +0 -2
  236. package/src/clis/chatwise/screenshot.ts +1 -2
  237. package/src/clis/chatwise/send.ts +0 -2
  238. package/src/clis/chatwise/status.ts +1 -2
  239. package/src/clis/ctrip/search.test.ts +73 -0
  240. package/src/clis/ctrip/search.ts +97 -47
  241. package/src/clis/douyin/_shared/sts2.test.ts +31 -0
  242. package/src/clis/douyin/_shared/sts2.ts +11 -3
  243. package/src/clis/douyin/activities.test.ts +41 -1
  244. package/src/clis/douyin/activities.ts +12 -3
  245. package/src/clis/douyin/collections.test.ts +35 -2
  246. package/src/clis/douyin/collections.ts +1 -1
  247. package/src/clis/douyin/draft.test.ts +444 -2
  248. package/src/clis/douyin/draft.ts +382 -218
  249. package/src/clis/douyin/hashtag.test.ts +42 -2
  250. package/src/clis/douyin/hashtag.ts +11 -3
  251. package/src/clis/douyin/profile.test.ts +43 -1
  252. package/src/clis/douyin/profile.ts +9 -2
  253. package/src/clis/douyin/videos.test.ts +52 -2
  254. package/src/clis/douyin/videos.ts +49 -15
  255. package/src/clis/facebook/search.test.ts +70 -0
  256. package/src/clis/facebook/search.yaml +4 -3
  257. package/src/clis/instagram/download.test.ts +159 -0
  258. package/src/clis/instagram/download.ts +286 -0
  259. package/src/clis/notebooklm/bind-current.test.ts +43 -0
  260. package/src/clis/notebooklm/bind-current.ts +36 -0
  261. package/src/clis/notebooklm/binding.test.ts +53 -0
  262. package/src/clis/notebooklm/compat.test.ts +19 -0
  263. package/src/clis/notebooklm/current.ts +38 -0
  264. package/src/clis/notebooklm/get.ts +53 -0
  265. package/src/clis/notebooklm/history.test.ts +70 -0
  266. package/src/clis/notebooklm/history.ts +36 -0
  267. package/src/clis/notebooklm/list.ts +40 -0
  268. package/src/clis/notebooklm/note-list.test.ts +64 -0
  269. package/src/clis/notebooklm/note-list.ts +42 -0
  270. package/src/clis/notebooklm/notes-get.test.ts +88 -0
  271. package/src/clis/notebooklm/notes-get.ts +67 -0
  272. package/src/clis/notebooklm/rpc.test.ts +126 -0
  273. package/src/clis/notebooklm/rpc.ts +286 -0
  274. package/src/clis/notebooklm/shared.ts +98 -0
  275. package/src/clis/notebooklm/source-fulltext.test.ts +123 -0
  276. package/src/clis/notebooklm/source-fulltext.ts +69 -0
  277. package/src/clis/notebooklm/source-get.test.ts +100 -0
  278. package/src/clis/notebooklm/source-get.ts +60 -0
  279. package/src/clis/notebooklm/source-guide.test.ts +121 -0
  280. package/src/clis/notebooklm/source-guide.ts +69 -0
  281. package/src/clis/notebooklm/source-list.ts +45 -0
  282. package/src/clis/notebooklm/status.ts +34 -0
  283. package/src/clis/notebooklm/summary.test.ts +94 -0
  284. package/src/clis/notebooklm/summary.ts +45 -0
  285. package/src/clis/notebooklm/utils.test.ts +446 -0
  286. package/src/clis/notebooklm/utils.ts +893 -0
  287. package/src/clis/substack/utils.test.ts +54 -0
  288. package/src/clis/substack/utils.ts +10 -2
  289. package/src/clis/v2ex/hot.yaml +4 -1
  290. package/src/clis/v2ex/latest.yaml +4 -1
  291. package/src/clis/v2ex/topic.yaml +6 -1
  292. package/src/clis/weixin/download.ts +95 -6
  293. package/src/clis/weread/book.ts +142 -2
  294. package/src/clis/weread/commands.test.ts +314 -154
  295. package/src/clis/weread/utils.ts +33 -4
  296. package/src/clis/xiaohongshu/comments.test.ts +85 -9
  297. package/src/clis/xiaohongshu/comments.ts +76 -17
  298. package/src/clis/xiaohongshu/download.test.ts +96 -0
  299. package/src/clis/xiaohongshu/download.ts +83 -22
  300. package/src/clis/xiaohongshu/note-helpers.ts +25 -0
  301. package/src/clis/xiaohongshu/note.test.ts +164 -0
  302. package/src/clis/xiaohongshu/note.ts +86 -0
  303. package/src/clis/xiaohongshu/search.test.ts +11 -4
  304. package/src/clis/xiaohongshu/search.ts +13 -0
  305. package/src/clis/youtube/search.ts +57 -17
  306. package/src/clis/zhihu/question.test.ts +71 -0
  307. package/src/clis/zhihu/question.ts +27 -15
  308. package/src/commanderAdapter.test.ts +30 -0
  309. package/src/commanderAdapter.ts +7 -0
  310. package/src/commands/daemon.test.ts +238 -0
  311. package/src/commands/daemon.ts +135 -0
  312. package/src/completion.ts +2 -1
  313. package/src/constants.ts +3 -0
  314. package/src/daemon.test.ts +88 -0
  315. package/src/daemon.ts +26 -14
  316. package/src/discovery.ts +52 -2
  317. package/src/electron-apps.test.ts +50 -0
  318. package/src/electron-apps.ts +89 -0
  319. package/src/engine.test.ts +45 -9
  320. package/src/execution.ts +24 -19
  321. package/src/extension-manifest-regression.test.ts +1 -0
  322. package/src/idle-manager.ts +60 -0
  323. package/src/launcher.test.ts +67 -0
  324. package/src/launcher.ts +185 -0
  325. package/src/main.ts +3 -2
  326. package/src/registry.test.ts +15 -0
  327. package/src/registry.ts +32 -3
  328. package/src/runtime.ts +13 -7
  329. package/src/serialization.test.ts +19 -1
  330. package/src/serialization.ts +2 -0
  331. package/src/tui.test.ts +23 -0
  332. package/src/tui.ts +65 -0
  333. package/src/weixin-download.test.ts +27 -0
  334. package/tests/e2e/browser-public-extended.test.ts +6 -2
  335. package/chatwise-opencli.ps1 +0 -82
  336. package/dist/clis/chatwise/shared.d.ts +0 -2
  337. package/dist/clis/chatwise/shared.js +0 -6
  338. package/src/clis/chatwise/shared.ts +0 -8
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Xiaohongshu note — read full note content from a public note page.
3
+ *
4
+ * Extracts title, author, description text, and engagement metrics
5
+ * (likes, collects, comment count) via DOM extraction.
6
+ */
7
+ import { cli, Strategy } from '../../registry.js';
8
+ import { AuthRequiredError, EmptyResultError } from '../../errors.js';
9
+ import { parseNoteId, buildNoteUrl } from './note-helpers.js';
10
+ cli({
11
+ site: 'xiaohongshu',
12
+ name: 'note',
13
+ description: '获取小红书笔记正文和互动数据',
14
+ domain: 'www.xiaohongshu.com',
15
+ strategy: Strategy.COOKIE,
16
+ args: [
17
+ { name: 'note-id', required: true, positional: true, help: 'Note ID or full URL (preserves xsec_token for access)' },
18
+ ],
19
+ columns: ['field', 'value'],
20
+ func: async (page, kwargs) => {
21
+ const raw = String(kwargs['note-id']);
22
+ const noteId = parseNoteId(raw);
23
+ const url = buildNoteUrl(raw);
24
+ await page.goto(url);
25
+ await page.wait(3);
26
+ const data = await page.evaluate(`
27
+ (() => {
28
+ const loginWall = /登录后查看|请登录/.test(document.body.innerText || '')
29
+ const notFound = /页面不见了|笔记不存在|无法浏览/.test(document.body.innerText || '')
30
+
31
+ const clean = (el) => (el?.textContent || '').replace(/\\s+/g, ' ').trim()
32
+
33
+ const title = clean(document.querySelector('#detail-title, .title'))
34
+ const desc = clean(document.querySelector('#detail-desc, .desc, .note-text'))
35
+ const author = clean(document.querySelector('.username, .author-wrapper .name'))
36
+ const likes = clean(document.querySelector('.like-wrapper .count'))
37
+ const collects = clean(document.querySelector('.collect-wrapper .count'))
38
+ const comments = clean(document.querySelector('.chat-wrapper .count'))
39
+
40
+ // Try to extract tags/topics
41
+ const tags = []
42
+ document.querySelectorAll('#detail-desc a.tag, #detail-desc a[href*="search_result"]').forEach(el => {
43
+ const t = (el.textContent || '').trim()
44
+ if (t) tags.push(t)
45
+ })
46
+
47
+ return { loginWall, notFound, title, desc, author, likes, collects, comments, tags }
48
+ })()
49
+ `);
50
+ if (!data || typeof data !== 'object') {
51
+ throw new EmptyResultError('xiaohongshu/note', 'Unexpected evaluate response');
52
+ }
53
+ if (data.loginWall) {
54
+ throw new AuthRequiredError('www.xiaohongshu.com', 'Note content requires login');
55
+ }
56
+ if (data.notFound) {
57
+ throw new EmptyResultError('xiaohongshu/note', `Note ${noteId} not found or unavailable — it may have been deleted or restricted`);
58
+ }
59
+ const d = data;
60
+ // XHS renders placeholder text like "赞"/"收藏"/"评论" when count is 0;
61
+ // normalize to '0' unless the value looks numeric.
62
+ const numOrZero = (v) => /^\d+/.test(v) ? v : '0';
63
+ const rows = [
64
+ { field: 'title', value: d.title || '' },
65
+ { field: 'author', value: d.author || '' },
66
+ { field: 'content', value: d.desc || '' },
67
+ { field: 'likes', value: numOrZero(d.likes || '') },
68
+ { field: 'collects', value: numOrZero(d.collects || '') },
69
+ { field: 'comments', value: numOrZero(d.comments || '') },
70
+ ];
71
+ if (d.tags?.length) {
72
+ rows.push({ field: 'tags', value: d.tags.join(', ') });
73
+ }
74
+ return rows;
75
+ },
76
+ });
@@ -0,0 +1 @@
1
+ import './note.js';
@@ -0,0 +1,136 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '../../registry.js';
3
+ import { parseNoteId, buildNoteUrl } from './note-helpers.js';
4
+ import './note.js';
5
+ function createPageMock(evaluateResult) {
6
+ return {
7
+ goto: vi.fn().mockResolvedValue(undefined),
8
+ evaluate: vi.fn().mockResolvedValue(evaluateResult),
9
+ snapshot: vi.fn().mockResolvedValue(undefined),
10
+ click: vi.fn().mockResolvedValue(undefined),
11
+ typeText: vi.fn().mockResolvedValue(undefined),
12
+ pressKey: vi.fn().mockResolvedValue(undefined),
13
+ scrollTo: vi.fn().mockResolvedValue(undefined),
14
+ getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
15
+ wait: vi.fn().mockResolvedValue(undefined),
16
+ tabs: vi.fn().mockResolvedValue([]),
17
+ closeTab: vi.fn().mockResolvedValue(undefined),
18
+ newTab: vi.fn().mockResolvedValue(undefined),
19
+ selectTab: vi.fn().mockResolvedValue(undefined),
20
+ networkRequests: vi.fn().mockResolvedValue([]),
21
+ consoleMessages: vi.fn().mockResolvedValue([]),
22
+ scroll: vi.fn().mockResolvedValue(undefined),
23
+ autoScroll: vi.fn().mockResolvedValue(undefined),
24
+ installInterceptor: vi.fn().mockResolvedValue(undefined),
25
+ getInterceptedRequests: vi.fn().mockResolvedValue([]),
26
+ getCookies: vi.fn().mockResolvedValue([]),
27
+ screenshot: vi.fn().mockResolvedValue(''),
28
+ waitForCapture: vi.fn().mockResolvedValue(undefined),
29
+ };
30
+ }
31
+ describe('parseNoteId', () => {
32
+ it('extracts ID from /explore/ URL', () => {
33
+ expect(parseNoteId('https://www.xiaohongshu.com/explore/69c131c9000000002800be4c')).toBe('69c131c9000000002800be4c');
34
+ });
35
+ it('extracts ID from /search_result/ URL with query params', () => {
36
+ expect(parseNoteId('https://www.xiaohongshu.com/search_result/69c131c9000000002800be4c?xsec_token=abc')).toBe('69c131c9000000002800be4c');
37
+ });
38
+ it('extracts ID from /note/ URL', () => {
39
+ expect(parseNoteId('https://www.xiaohongshu.com/note/69c131c9000000002800be4c')).toBe('69c131c9000000002800be4c');
40
+ });
41
+ it('returns raw string when no URL pattern matches', () => {
42
+ expect(parseNoteId('69c131c9000000002800be4c')).toBe('69c131c9000000002800be4c');
43
+ });
44
+ it('trims whitespace', () => {
45
+ expect(parseNoteId(' 69c131c9000000002800be4c ')).toBe('69c131c9000000002800be4c');
46
+ });
47
+ });
48
+ describe('buildNoteUrl', () => {
49
+ it('returns full URL as-is when given https URL', () => {
50
+ const url = 'https://www.xiaohongshu.com/search_result/abc123?xsec_token=tok';
51
+ expect(buildNoteUrl(url)).toBe(url);
52
+ });
53
+ it('constructs /explore/ URL for bare note ID', () => {
54
+ expect(buildNoteUrl('abc123')).toBe('https://www.xiaohongshu.com/explore/abc123');
55
+ });
56
+ });
57
+ describe('xiaohongshu note', () => {
58
+ const command = getRegistry().get('xiaohongshu/note');
59
+ it('is registered', () => {
60
+ expect(command).toBeDefined();
61
+ expect(command.func).toBeTypeOf('function');
62
+ });
63
+ it('returns note content as field/value rows', async () => {
64
+ const page = createPageMock({
65
+ loginWall: false,
66
+ notFound: false,
67
+ title: '尚界Z7实车体验',
68
+ desc: '今天去看了实车,外观很帅',
69
+ author: '小红薯用户',
70
+ likes: '257',
71
+ collects: '98',
72
+ comments: '45',
73
+ tags: ['#尚界Z7', '#鸿蒙智行'],
74
+ });
75
+ const result = (await command.func(page, { 'note-id': '69c131c9000000002800be4c' }));
76
+ expect(page.goto.mock.calls[0][0]).toContain('/explore/69c131c9000000002800be4c');
77
+ expect(result).toEqual([
78
+ { field: 'title', value: '尚界Z7实车体验' },
79
+ { field: 'author', value: '小红薯用户' },
80
+ { field: 'content', value: '今天去看了实车,外观很帅' },
81
+ { field: 'likes', value: '257' },
82
+ { field: 'collects', value: '98' },
83
+ { field: 'comments', value: '45' },
84
+ { field: 'tags', value: '#尚界Z7, #鸿蒙智行' },
85
+ ]);
86
+ });
87
+ it('parses note ID from full /explore/ URL', async () => {
88
+ const page = createPageMock({
89
+ loginWall: false, notFound: false,
90
+ title: 'Test', desc: '', author: '', likes: '0', collects: '0', comments: '0', tags: [],
91
+ });
92
+ await command.func(page, {
93
+ 'note-id': 'https://www.xiaohongshu.com/explore/69c131c9000000002800be4c?xsec_token=abc',
94
+ });
95
+ expect(page.goto.mock.calls[0][0]).toContain('/explore/69c131c9000000002800be4c');
96
+ });
97
+ it('preserves full search_result URL with xsec_token for navigation', async () => {
98
+ const page = createPageMock({
99
+ loginWall: false, notFound: false,
100
+ title: 'Test', desc: '', author: '', likes: '0', collects: '0', comments: '0', tags: [],
101
+ });
102
+ const fullUrl = 'https://www.xiaohongshu.com/search_result/69c131c9000000002800be4c?xsec_token=abc';
103
+ await command.func(page, { 'note-id': fullUrl });
104
+ // Should navigate to the full URL as-is, not strip the token
105
+ expect(page.goto.mock.calls[0][0]).toBe(fullUrl);
106
+ });
107
+ it('throws AuthRequiredError on login wall', async () => {
108
+ const page = createPageMock({ loginWall: true, notFound: false });
109
+ await expect(command.func(page, { 'note-id': 'abc123' })).rejects.toThrow('Note content requires login');
110
+ });
111
+ it('throws EmptyResultError when note is not found', async () => {
112
+ const page = createPageMock({ loginWall: false, notFound: true });
113
+ await expect(command.func(page, { 'note-id': 'abc123' })).rejects.toThrow('returned no data');
114
+ });
115
+ it('normalizes placeholder text to 0 for zero-count metrics', async () => {
116
+ const page = createPageMock({
117
+ loginWall: false, notFound: false,
118
+ title: 'New note', desc: 'Just posted', author: 'Author',
119
+ likes: '赞', collects: '收藏', comments: '评论', tags: [],
120
+ });
121
+ const result = (await command.func(page, { 'note-id': 'abc123' }));
122
+ expect(result.find((r) => r.field === 'likes').value).toBe('0');
123
+ expect(result.find((r) => r.field === 'collects').value).toBe('0');
124
+ expect(result.find((r) => r.field === 'comments').value).toBe('0');
125
+ });
126
+ it('omits tags row when no tags present', async () => {
127
+ const page = createPageMock({
128
+ loginWall: false, notFound: false,
129
+ title: 'No tags', desc: 'Content', author: 'Author',
130
+ likes: '1', collects: '2', comments: '3', tags: [],
131
+ });
132
+ const result = (await command.func(page, { 'note-id': 'abc123' }));
133
+ expect(result.find((r) => r.field === 'tags')).toBeUndefined();
134
+ expect(result).toHaveLength(6);
135
+ });
136
+ });
@@ -40,6 +40,15 @@ cli({
40
40
  const keyword = encodeURIComponent(kwargs.query);
41
41
  await page.goto(`https://www.xiaohongshu.com/search_result?keyword=${keyword}&source=web_search_result_notes`);
42
42
  await page.wait(3);
43
+ // Early login-wall detection: XHS may show a login gate instead of
44
+ // results. Check *before* autoScroll to avoid crashing on a page
45
+ // that has no meaningful content to scroll through.
46
+ const loginCheck = await page.evaluate(`
47
+ (() => /登录后查看搜索结果/.test(document.body?.innerText || ''))()
48
+ `);
49
+ if (loginCheck) {
50
+ throw new AuthRequiredError('www.xiaohongshu.com', 'Xiaohongshu search results are blocked behind a login wall');
51
+ }
43
52
  // Scroll a couple of times to load more results
44
53
  await page.autoScroll({ times: 2 });
45
54
  const payload = await page.evaluate(`
@@ -36,12 +36,12 @@ describe('xiaohongshu search', () => {
36
36
  const cmd = getRegistry().get('xiaohongshu/search');
37
37
  expect(cmd?.func).toBeTypeOf('function');
38
38
  const page = createPageMock([
39
- {
40
- loginWall: true,
41
- results: [],
42
- },
39
+ // First evaluate: early login-wall check (returns true)
40
+ true,
43
41
  ]);
44
42
  await expect(cmd.func(page, { query: '特斯拉', limit: 5 })).rejects.toThrow('Xiaohongshu search results are blocked behind a login wall');
43
+ // autoScroll must NOT be called when a login wall is detected early
44
+ expect(page.autoScroll).not.toHaveBeenCalled();
45
45
  });
46
46
  it('returns ranked results with search_result url and author_url preserved', async () => {
47
47
  const cmd = getRegistry().get('xiaohongshu/search');
@@ -49,6 +49,9 @@ describe('xiaohongshu search', () => {
49
49
  const detailUrl = 'https://www.xiaohongshu.com/search_result/68e90be80000000004022e66?xsec_token=test-token&xsec_source=';
50
50
  const authorUrl = 'https://www.xiaohongshu.com/user/profile/635a9c720000000018028b40?xsec_token=user-token&xsec_source=pc_search';
51
51
  const page = createPageMock([
52
+ // First evaluate: early login-wall check (returns false → no wall)
53
+ false,
54
+ // Second evaluate: main DOM extraction
52
55
  {
53
56
  loginWall: false,
54
57
  results: [
@@ -81,6 +84,9 @@ describe('xiaohongshu search', () => {
81
84
  const cmd = getRegistry().get('xiaohongshu/search');
82
85
  expect(cmd?.func).toBeTypeOf('function');
83
86
  const page = createPageMock([
87
+ // First evaluate: early login-wall check (returns false → no wall)
88
+ false,
89
+ // Second evaluate: main DOM extraction
84
90
  {
85
91
  loginWall: false,
86
92
  results: [
@@ -11,32 +11,60 @@ cli({
11
11
  args: [
12
12
  { name: 'query', required: true, positional: true, help: 'Search query' },
13
13
  { name: 'limit', type: 'int', default: 20, help: 'Max results (max 50)' },
14
+ { name: 'type', default: '', help: 'Filter type: shorts, video, channel, playlist' },
15
+ { name: 'upload', default: '', help: 'Upload date: hour, today, week, month, year' },
16
+ { name: 'sort', default: '', help: 'Sort by: relevance, date, views, rating' },
14
17
  ],
15
- columns: ['rank', 'title', 'channel', 'views', 'duration', 'url'],
18
+ columns: ['rank', 'title', 'channel', 'views', 'duration', 'published', 'url'],
16
19
  func: async (page, kwargs) => {
17
20
  const limit = Math.min(kwargs.limit || 20, 50);
18
- await page.goto('https://www.youtube.com');
19
- await page.wait(2);
21
+ const query = encodeURIComponent(kwargs.query);
22
+ // Build search URL with filter params
23
+ // YouTube uses sp= parameter for filters — we use the URL approach for reliability
24
+ const spMap = {
25
+ // type filters
26
+ 'shorts': 'EgIQCQ%3D%3D', // Shorts (type=9)
27
+ 'video': 'EgIQAQ%3D%3D',
28
+ 'channel': 'EgIQAg%3D%3D',
29
+ 'playlist': 'EgIQAw%3D%3D',
30
+ // upload date filters (can be combined with type via URL)
31
+ 'hour': 'EgIIAQ%3D%3D',
32
+ 'today': 'EgIIAg%3D%3D',
33
+ 'week': 'EgIIAw%3D%3D',
34
+ 'month': 'EgIIBA%3D%3D',
35
+ 'year': 'EgIIBQ%3D%3D',
36
+ };
37
+ const sortMap = {
38
+ 'date': 'CAI%3D',
39
+ 'views': 'CAM%3D',
40
+ 'rating': 'CAE%3D',
41
+ };
42
+ // YouTube only supports a single sp= parameter — pick the most specific filter.
43
+ // Priority: type > upload > sort (type is the most common use case)
44
+ let sp = '';
45
+ if (kwargs.type && spMap[kwargs.type])
46
+ sp = spMap[kwargs.type];
47
+ else if (kwargs.upload && spMap[kwargs.upload])
48
+ sp = spMap[kwargs.upload];
49
+ else if (kwargs.sort && sortMap[kwargs.sort])
50
+ sp = sortMap[kwargs.sort];
51
+ let url = `https://www.youtube.com/results?search_query=${query}`;
52
+ if (sp)
53
+ url += `&sp=${sp}`;
54
+ await page.goto(url);
55
+ await page.wait(3);
20
56
  const data = await page.evaluate(`
21
57
  (async () => {
22
- const cfg = window.ytcfg?.data_ || {};
23
- const apiKey = cfg.INNERTUBE_API_KEY;
24
- const context = cfg.INNERTUBE_CONTEXT;
25
- if (!apiKey || !context) return {error: 'YouTube config not found'};
58
+ const data = window.ytInitialData;
59
+ if (!data) return {error: 'YouTube data not found'};
26
60
 
27
- const resp = await fetch('/youtubei/v1/search?key=' + apiKey + '&prettyPrint=false', {
28
- method: 'POST', credentials: 'include',
29
- headers: {'Content-Type': 'application/json'},
30
- body: JSON.stringify({context, query: '${kwargs.query.replace(/'/g, "\\'")}'})
31
- });
32
- if (!resp.ok) return {error: 'HTTP ' + resp.status};
33
-
34
- const data = await resp.json();
35
61
  const contents = data.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents || [];
36
62
  const videos = [];
37
63
  for (const section of contents) {
38
- for (const item of (section.itemSectionRenderer?.contents || [])) {
39
- if (item.videoRenderer && videos.length < ${limit}) {
64
+ const items = section.itemSectionRenderer?.contents || section.reelShelfRenderer?.items || [];
65
+ for (const item of items) {
66
+ if (videos.length >= ${limit}) break;
67
+ if (item.videoRenderer) {
40
68
  const v = item.videoRenderer;
41
69
  videos.push({
42
70
  rank: videos.length + 1,
@@ -44,8 +72,20 @@ cli({
44
72
  channel: v.ownerText?.runs?.[0]?.text || '',
45
73
  views: v.viewCountText?.simpleText || v.shortViewCountText?.simpleText || '',
46
74
  duration: v.lengthText?.simpleText || 'LIVE',
75
+ published: v.publishedTimeText?.simpleText || '',
47
76
  url: 'https://www.youtube.com/watch?v=' + v.videoId
48
77
  });
78
+ } else if (item.reelItemRenderer) {
79
+ const r = item.reelItemRenderer;
80
+ videos.push({
81
+ rank: videos.length + 1,
82
+ title: r.headline?.simpleText || '',
83
+ channel: r.navigationEndpoint?.reelWatchEndpoint?.overlay?.reelPlayerOverlayRenderer?.reelPlayerHeaderSupportedRenderers?.reelPlayerHeaderRenderer?.channelTitleText?.runs?.[0]?.text || '',
84
+ views: r.viewCountText?.simpleText || '',
85
+ duration: 'SHORT',
86
+ published: r.publishedTimeText?.simpleText || '',
87
+ url: 'https://www.youtube.com/shorts/' + r.videoId
88
+ });
49
89
  }
50
90
  }
51
91
  }
@@ -1,5 +1,5 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
- import { AuthRequiredError } from '../../errors.js';
2
+ import { AuthRequiredError, CliError } from '../../errors.js';
3
3
  cli({
4
4
  site: 'zhihu',
5
5
  name: 'question',
@@ -13,23 +13,25 @@ cli({
13
13
  columns: ['rank', 'author', 'votes', 'content'],
14
14
  func: async (page, kwargs) => {
15
15
  const { id, limit = 5 } = kwargs;
16
+ const answerLimit = Number(limit);
16
17
  const stripHtml = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&').trim();
17
- // Fetch question detail and answers in parallel via evaluate
18
- const result = await page.evaluate(`
19
- async () => {
20
- const [qResp, aResp] = await Promise.all([
21
- fetch('https://www.zhihu.com/api/v4/questions/${id}?include=data[*].detail,excerpt,answer_count,follower_count,visit_count', {credentials: 'include'}),
22
- fetch('https://www.zhihu.com/api/v4/questions/${id}/answers?limit=${limit}&offset=0&sort_by=default&include=data[*].content,voteup_count,comment_count,author', {credentials: 'include'})
23
- ]);
24
- if (!qResp.ok || !aResp.ok) return { error: true };
25
- const q = await qResp.json();
26
- const a = await aResp.json();
27
- return { question: q, answers: a.data || [] };
28
- }
29
- `);
30
- if (!result || result.error)
31
- throw new AuthRequiredError('www.zhihu.com', 'Failed to fetch question data from Zhihu');
32
- const answers = (result.answers ?? []).slice(0, Number(limit)).map((a, i) => ({
18
+ // Only fetch answers here. The question detail endpoint is not used by the
19
+ // current CLI output and can fail independently, which would incorrectly
20
+ // turn a successful answers response into a login error.
21
+ const result = await page.evaluate(async ({ questionId, answerLimit }) => {
22
+ const aResp = await fetch(`https://www.zhihu.com/api/v4/questions/${questionId}/answers?limit=${answerLimit}&offset=0&sort_by=default&include=data[*].content,voteup_count,comment_count,author`, { credentials: 'include' });
23
+ if (!aResp.ok)
24
+ return { ok: false, status: aResp.status };
25
+ const a = await aResp.json();
26
+ return { ok: true, answers: Array.isArray(a?.data) ? a.data : [] };
27
+ }, { questionId: String(id), answerLimit });
28
+ if (!result?.ok) {
29
+ if (result?.status === 401 || result?.status === 403) {
30
+ throw new AuthRequiredError('www.zhihu.com', 'Failed to fetch question data from Zhihu');
31
+ }
32
+ throw new CliError('FETCH_ERROR', `Zhihu question answers request failed with HTTP ${result?.status ?? 'unknown'}`, 'Try again later or rerun with -v for more detail');
33
+ }
34
+ const answers = result.answers.slice(0, answerLimit).map((a, i) => ({
33
35
  rank: i + 1,
34
36
  author: a.author?.name ?? 'anonymous',
35
37
  votes: a.voteup_count ?? 0,
@@ -0,0 +1 @@
1
+ import './question.js';
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '../../registry.js';
3
+ import { AuthRequiredError } from '../../errors.js';
4
+ import './question.js';
5
+ describe('zhihu question', () => {
6
+ it('returns answers even when the unused question detail request fails', async () => {
7
+ const cmd = getRegistry().get('zhihu/question');
8
+ expect(cmd?.func).toBeTypeOf('function');
9
+ const evaluate = vi.fn().mockImplementation(async (_fn, args) => {
10
+ expect(args).toEqual({ questionId: '2021881398772981878', answerLimit: 3 });
11
+ return {
12
+ ok: true,
13
+ answers: [
14
+ {
15
+ author: { name: 'alice' },
16
+ voteup_count: 12,
17
+ content: '<p>Hello <b>Zhihu</b></p>',
18
+ },
19
+ ],
20
+ };
21
+ });
22
+ const page = {
23
+ evaluate,
24
+ };
25
+ await expect(cmd.func(page, { id: '2021881398772981878', limit: 3 })).resolves.toEqual([
26
+ {
27
+ rank: 1,
28
+ author: 'alice',
29
+ votes: 12,
30
+ content: 'Hello Zhihu',
31
+ },
32
+ ]);
33
+ expect(evaluate).toHaveBeenCalledTimes(1);
34
+ });
35
+ it('maps auth-like answer failures to AuthRequiredError', async () => {
36
+ const cmd = getRegistry().get('zhihu/question');
37
+ expect(cmd?.func).toBeTypeOf('function');
38
+ const page = {
39
+ evaluate: vi.fn().mockResolvedValue({ ok: false, status: 403 }),
40
+ };
41
+ await expect(cmd.func(page, { id: '2021881398772981878', limit: 3 })).rejects.toBeInstanceOf(AuthRequiredError);
42
+ });
43
+ it('preserves non-auth fetch failures as CliError instead of login errors', async () => {
44
+ const cmd = getRegistry().get('zhihu/question');
45
+ expect(cmd?.func).toBeTypeOf('function');
46
+ const page = {
47
+ evaluate: vi.fn().mockResolvedValue({ ok: false, status: 500 }),
48
+ };
49
+ await expect(cmd.func(page, { id: '2021881398772981878', limit: 3 })).rejects.toMatchObject({
50
+ code: 'FETCH_ERROR',
51
+ message: 'Zhihu question answers request failed with HTTP 500',
52
+ });
53
+ });
54
+ });
@@ -38,6 +38,8 @@ export function registerCommandToProgram(siteCmd, cmd) {
38
38
  return;
39
39
  const deprecatedSuffix = cmd.deprecated ? ' [deprecated]' : '';
40
40
  const subCmd = siteCmd.command(cmd.name).description(`${cmd.description}${deprecatedSuffix}`);
41
+ if (cmd.aliases?.length)
42
+ subCmd.aliases(cmd.aliases);
41
43
  // Register positional args first, then named options
42
44
  const positionalArgs = [];
43
45
  for (const arg of cmd.args) {
@@ -91,6 +93,9 @@ export function registerCommandToProgram(siteCmd, cmd) {
91
93
  console.error(chalk.yellow(`Deprecated: ${message}${replacement}`));
92
94
  }
93
95
  const result = await executeCommand(cmd, kwargs, verbose);
96
+ if (result === null || result === undefined) {
97
+ return;
98
+ }
94
99
  if (verbose && (!result || (Array.isArray(result) && result.length === 0))) {
95
100
  console.error(chalk.yellow('[Verbose] Warning: Command returned an empty result.'));
96
101
  }
@@ -274,7 +279,11 @@ async function renderError(err, cmdName, verbose) {
274
279
  * Register all commands from the registry onto a Commander program.
275
280
  */
276
281
  export function registerAllCommands(program, siteGroups) {
282
+ const seen = new Set();
277
283
  for (const [, cmd] of getRegistry()) {
284
+ if (seen.has(cmd))
285
+ continue;
286
+ seen.add(cmd);
278
287
  let siteCmd = siteGroups.get(cmd.site);
279
288
  if (!siteCmd) {
280
289
  siteCmd = program.command(cmd.site).description(`${cmd.site} commands`);
@@ -99,3 +99,28 @@ describe('commanderAdapter boolean alias support', () => {
99
99
  expect(kwargs.undo).toBe(false);
100
100
  });
101
101
  });
102
+ describe('commanderAdapter command aliases', () => {
103
+ const cmd = {
104
+ site: 'notebooklm',
105
+ name: 'get',
106
+ aliases: ['metadata'],
107
+ description: 'Get notebook metadata',
108
+ browser: false,
109
+ args: [],
110
+ func: vi.fn(),
111
+ };
112
+ beforeEach(() => {
113
+ mockExecuteCommand.mockReset();
114
+ mockExecuteCommand.mockResolvedValue([]);
115
+ mockRenderOutput.mockReset();
116
+ delete process.env.OPENCLI_VERBOSE;
117
+ process.exitCode = undefined;
118
+ });
119
+ it('registers aliases with Commander so compatibility names execute the same command', async () => {
120
+ const program = new Command();
121
+ const siteCmd = program.command('notebooklm');
122
+ registerCommandToProgram(siteCmd, cmd);
123
+ await program.parseAsync(['node', 'opencli', 'notebooklm', 'metadata']);
124
+ expect(mockExecuteCommand).toHaveBeenCalledWith(cmd, {}, false);
125
+ });
126
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * CLI commands for daemon lifecycle management:
3
+ * opencli daemon status — show daemon state
4
+ * opencli daemon stop — graceful shutdown
5
+ * opencli daemon restart — stop + respawn
6
+ */
7
+ export declare function daemonStatus(): Promise<void>;
8
+ export declare function daemonStop(): Promise<void>;
9
+ export declare function daemonRestart(): Promise<void>;