@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,58 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ const { mockListNotebooklmHistoryViaRpc, mockGetNotebooklmPageState, mockRequireNotebooklmSession, } = vi.hoisted(() => ({
3
+ mockListNotebooklmHistoryViaRpc: vi.fn(),
4
+ mockGetNotebooklmPageState: vi.fn(),
5
+ mockRequireNotebooklmSession: vi.fn(),
6
+ }));
7
+ vi.mock('./utils.js', async () => {
8
+ const actual = await vi.importActual('./utils.js');
9
+ return {
10
+ ...actual,
11
+ getNotebooklmPageState: mockGetNotebooklmPageState,
12
+ listNotebooklmHistoryViaRpc: mockListNotebooklmHistoryViaRpc,
13
+ requireNotebooklmSession: mockRequireNotebooklmSession,
14
+ };
15
+ });
16
+ import { getRegistry } from '../../registry.js';
17
+ import './history.js';
18
+ describe('notebooklm history', () => {
19
+ const history = getRegistry().get('notebooklm/history');
20
+ beforeEach(() => {
21
+ mockListNotebooklmHistoryViaRpc.mockReset();
22
+ mockGetNotebooklmPageState.mockReset();
23
+ mockRequireNotebooklmSession.mockReset();
24
+ mockRequireNotebooklmSession.mockResolvedValue(undefined);
25
+ mockGetNotebooklmPageState.mockResolvedValue({
26
+ url: 'https://notebooklm.google.com/notebook/nb-demo',
27
+ title: 'Browser Automation',
28
+ hostname: 'notebooklm.google.com',
29
+ kind: 'notebook',
30
+ notebookId: 'nb-demo',
31
+ loginRequired: false,
32
+ notebookCount: 1,
33
+ });
34
+ });
35
+ it('lists notebook history threads from the browser rpc', async () => {
36
+ mockListNotebooklmHistoryViaRpc.mockResolvedValue([
37
+ {
38
+ notebook_id: 'nb-demo',
39
+ thread_id: '28e0f2cb-4591-45a3-a661-7653666f7c78',
40
+ item_count: 0,
41
+ preview: 'Summarize this notebook',
42
+ url: 'https://notebooklm.google.com/notebook/nb-demo',
43
+ source: 'rpc',
44
+ },
45
+ ]);
46
+ const result = await history.func({}, {});
47
+ expect(result).toEqual([
48
+ {
49
+ notebook_id: 'nb-demo',
50
+ thread_id: '28e0f2cb-4591-45a3-a661-7653666f7c78',
51
+ item_count: 0,
52
+ preview: 'Summarize this notebook',
53
+ url: 'https://notebooklm.google.com/notebook/nb-demo',
54
+ source: 'rpc',
55
+ },
56
+ ]);
57
+ });
58
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { AuthRequiredError } from '../../errors.js';
3
+ import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
4
+ import { ensureNotebooklmHome, listNotebooklmLinks, listNotebooklmViaRpc, readCurrentNotebooklm, requireNotebooklmSession, } from './utils.js';
5
+ cli({
6
+ site: NOTEBOOKLM_SITE,
7
+ name: 'list',
8
+ description: 'List NotebookLM notebooks via in-page batchexecute RPC in the current logged-in session',
9
+ domain: NOTEBOOKLM_DOMAIN,
10
+ strategy: Strategy.COOKIE,
11
+ browser: true,
12
+ navigateBefore: false,
13
+ args: [],
14
+ columns: ['title', 'id', 'is_owner', 'created_at', 'source', 'url'],
15
+ func: async (page) => {
16
+ const currentFallback = await readCurrentNotebooklm(page).catch(() => null);
17
+ await ensureNotebooklmHome(page);
18
+ await requireNotebooklmSession(page);
19
+ try {
20
+ const rpcRows = await listNotebooklmViaRpc(page);
21
+ if (rpcRows.length > 0)
22
+ return rpcRows;
23
+ }
24
+ catch (error) {
25
+ if (error instanceof AuthRequiredError)
26
+ throw error;
27
+ }
28
+ const domRows = await listNotebooklmLinks(page);
29
+ if (domRows.length > 0)
30
+ return domRows;
31
+ if (currentFallback)
32
+ return [currentFallback];
33
+ return [];
34
+ },
35
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { EmptyResultError } from '../../errors.js';
3
+ import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
4
+ import { ensureNotebooklmNotebookBinding, getNotebooklmPageState, listNotebooklmNotesFromPage, requireNotebooklmSession, } from './utils.js';
5
+ cli({
6
+ site: NOTEBOOKLM_SITE,
7
+ name: 'note-list',
8
+ aliases: ['notes-list'],
9
+ description: 'List saved notes from the Studio panel of the current NotebookLM notebook',
10
+ domain: NOTEBOOKLM_DOMAIN,
11
+ strategy: Strategy.COOKIE,
12
+ browser: true,
13
+ navigateBefore: false,
14
+ args: [],
15
+ columns: ['title', 'created_at', 'source', 'url'],
16
+ func: async (page) => {
17
+ await ensureNotebooklmNotebookBinding(page);
18
+ await requireNotebooklmSession(page);
19
+ const state = await getNotebooklmPageState(page);
20
+ if (state.kind !== 'notebook') {
21
+ throw new EmptyResultError('opencli notebooklm note-list', 'Open a specific NotebookLM notebook tab first, then retry.');
22
+ }
23
+ const rows = await listNotebooklmNotesFromPage(page);
24
+ if (rows.length > 0)
25
+ return rows;
26
+ throw new EmptyResultError('opencli notebooklm note-list', 'No NotebookLM notes are visible in the Studio panel. Reload the notebook page or close the note editor and retry.');
27
+ },
28
+ });
@@ -0,0 +1 @@
1
+ import './note-list.js';
@@ -0,0 +1,56 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ const { mockListNotebooklmNotesFromPage, mockGetNotebooklmPageState, mockRequireNotebooklmSession } = vi.hoisted(() => ({
3
+ mockListNotebooklmNotesFromPage: vi.fn(),
4
+ mockGetNotebooklmPageState: vi.fn(),
5
+ mockRequireNotebooklmSession: vi.fn(),
6
+ }));
7
+ vi.mock('./utils.js', async () => {
8
+ const actual = await vi.importActual('./utils.js');
9
+ return {
10
+ ...actual,
11
+ getNotebooklmPageState: mockGetNotebooklmPageState,
12
+ listNotebooklmNotesFromPage: mockListNotebooklmNotesFromPage,
13
+ requireNotebooklmSession: mockRequireNotebooklmSession,
14
+ };
15
+ });
16
+ import { getRegistry } from '../../registry.js';
17
+ import './note-list.js';
18
+ describe('notebooklm note-list', () => {
19
+ const command = getRegistry().get('notebooklm/note-list');
20
+ beforeEach(() => {
21
+ mockListNotebooklmNotesFromPage.mockReset();
22
+ mockGetNotebooklmPageState.mockReset();
23
+ mockRequireNotebooklmSession.mockReset();
24
+ mockRequireNotebooklmSession.mockResolvedValue(undefined);
25
+ mockGetNotebooklmPageState.mockResolvedValue({
26
+ url: 'https://notebooklm.google.com/notebook/nb-demo',
27
+ title: 'Browser Automation',
28
+ hostname: 'notebooklm.google.com',
29
+ kind: 'notebook',
30
+ notebookId: 'nb-demo',
31
+ loginRequired: false,
32
+ notebookCount: 1,
33
+ });
34
+ });
35
+ it('lists notebook notes from the Studio panel', async () => {
36
+ mockListNotebooklmNotesFromPage.mockResolvedValue([
37
+ {
38
+ notebook_id: 'nb-demo',
39
+ title: '新建笔记',
40
+ created_at: '6 分钟前',
41
+ url: 'https://notebooklm.google.com/notebook/nb-demo',
42
+ source: 'studio-list',
43
+ },
44
+ ]);
45
+ const result = await command.func({}, {});
46
+ expect(result).toEqual([
47
+ {
48
+ notebook_id: 'nb-demo',
49
+ title: '新建笔记',
50
+ created_at: '6 分钟前',
51
+ url: 'https://notebooklm.google.com/notebook/nb-demo',
52
+ source: 'studio-list',
53
+ },
54
+ ]);
55
+ });
56
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { EmptyResultError } from '../../errors.js';
3
+ import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
4
+ import { ensureNotebooklmNotebookBinding, findNotebooklmNoteRow, getNotebooklmPageState, listNotebooklmNotesFromPage, readNotebooklmVisibleNoteFromPage, requireNotebooklmSession, } from './utils.js';
5
+ function matchesNoteTitle(title, query) {
6
+ const needle = query.trim().toLowerCase();
7
+ if (!needle)
8
+ return false;
9
+ const normalized = title.trim().toLowerCase();
10
+ return normalized === needle || normalized.includes(needle);
11
+ }
12
+ cli({
13
+ site: NOTEBOOKLM_SITE,
14
+ name: 'notes-get',
15
+ description: 'Get one note from the current NotebookLM notebook by title from the visible note editor',
16
+ domain: NOTEBOOKLM_DOMAIN,
17
+ strategy: Strategy.COOKIE,
18
+ browser: true,
19
+ navigateBefore: false,
20
+ args: [
21
+ {
22
+ name: 'note',
23
+ positional: true,
24
+ required: true,
25
+ help: 'Note title or id from the current notebook',
26
+ },
27
+ ],
28
+ columns: ['title', 'content', 'source', 'url'],
29
+ func: async (page, kwargs) => {
30
+ await ensureNotebooklmNotebookBinding(page);
31
+ await requireNotebooklmSession(page);
32
+ const state = await getNotebooklmPageState(page);
33
+ if (state.kind !== 'notebook') {
34
+ throw new EmptyResultError('opencli notebooklm notes-get', 'Open a specific NotebookLM notebook tab first, then retry.');
35
+ }
36
+ const query = typeof kwargs.note === 'string' ? kwargs.note : String(kwargs.note ?? '');
37
+ const visible = await readNotebooklmVisibleNoteFromPage(page);
38
+ if (visible && matchesNoteTitle(visible.title, query))
39
+ return [visible];
40
+ const rows = await listNotebooklmNotesFromPage(page);
41
+ const listed = findNotebooklmNoteRow(rows, query);
42
+ if (listed) {
43
+ throw new EmptyResultError('opencli notebooklm notes-get', `Note "${query}" is listed in Studio, but opencli currently reads note content only from the visible note editor. Open that note in NotebookLM, then retry.`);
44
+ }
45
+ throw new EmptyResultError('opencli notebooklm notes-get', `Note "${query}" was not found in the current notebook.`);
46
+ },
47
+ });
@@ -0,0 +1 @@
1
+ import './notes-get.js';
@@ -0,0 +1,72 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ const { mockListNotebooklmNotesFromPage, mockReadNotebooklmVisibleNoteFromPage, mockGetNotebooklmPageState, mockRequireNotebooklmSession, } = vi.hoisted(() => ({
3
+ mockListNotebooklmNotesFromPage: vi.fn(),
4
+ mockReadNotebooklmVisibleNoteFromPage: vi.fn(),
5
+ mockGetNotebooklmPageState: vi.fn(),
6
+ mockRequireNotebooklmSession: vi.fn(),
7
+ }));
8
+ vi.mock('./utils.js', async () => {
9
+ const actual = await vi.importActual('./utils.js');
10
+ return {
11
+ ...actual,
12
+ listNotebooklmNotesFromPage: mockListNotebooklmNotesFromPage,
13
+ readNotebooklmVisibleNoteFromPage: mockReadNotebooklmVisibleNoteFromPage,
14
+ getNotebooklmPageState: mockGetNotebooklmPageState,
15
+ requireNotebooklmSession: mockRequireNotebooklmSession,
16
+ };
17
+ });
18
+ import { getRegistry } from '../../registry.js';
19
+ import './notes-get.js';
20
+ describe('notebooklm notes-get', () => {
21
+ const command = getRegistry().get('notebooklm/notes-get');
22
+ beforeEach(() => {
23
+ mockListNotebooklmNotesFromPage.mockReset();
24
+ mockReadNotebooklmVisibleNoteFromPage.mockReset();
25
+ mockGetNotebooklmPageState.mockReset();
26
+ mockRequireNotebooklmSession.mockReset();
27
+ mockRequireNotebooklmSession.mockResolvedValue(undefined);
28
+ mockGetNotebooklmPageState.mockResolvedValue({
29
+ url: 'https://notebooklm.google.com/notebook/nb-demo',
30
+ title: 'Browser Automation',
31
+ hostname: 'notebooklm.google.com',
32
+ kind: 'notebook',
33
+ notebookId: 'nb-demo',
34
+ loginRequired: false,
35
+ notebookCount: 1,
36
+ });
37
+ });
38
+ it('returns the currently visible note editor content when the title matches', async () => {
39
+ mockReadNotebooklmVisibleNoteFromPage.mockResolvedValue({
40
+ notebook_id: 'nb-demo',
41
+ title: '新建笔记',
42
+ content: '第一段\\n第二段',
43
+ url: 'https://notebooklm.google.com/notebook/nb-demo',
44
+ source: 'studio-editor',
45
+ });
46
+ const result = await command.func({}, { note: '新建笔记' });
47
+ expect(result).toEqual([
48
+ {
49
+ notebook_id: 'nb-demo',
50
+ title: '新建笔记',
51
+ content: '第一段\\n第二段',
52
+ url: 'https://notebooklm.google.com/notebook/nb-demo',
53
+ source: 'studio-editor',
54
+ },
55
+ ]);
56
+ });
57
+ it('explains the current visible-note limitation when the target note is listed but not open', async () => {
58
+ mockReadNotebooklmVisibleNoteFromPage.mockResolvedValue(null);
59
+ mockListNotebooklmNotesFromPage.mockResolvedValue([
60
+ {
61
+ notebook_id: 'nb-demo',
62
+ title: '新建笔记',
63
+ created_at: '6 分钟前',
64
+ url: 'https://notebooklm.google.com/notebook/nb-demo',
65
+ source: 'studio-list',
66
+ },
67
+ ]);
68
+ await expect(command.func({}, { note: '新建笔记' })).rejects.toMatchObject({
69
+ hint: expect.stringMatching(/currently reads note content only from the visible note editor/i),
70
+ });
71
+ });
72
+ });
@@ -0,0 +1,36 @@
1
+ import type { IPage } from '../../types.js';
2
+ export type NotebooklmPageAuth = {
3
+ csrfToken: string;
4
+ sessionId: string;
5
+ sourcePath: string;
6
+ };
7
+ export type NotebooklmFetchResponse = {
8
+ ok: boolean;
9
+ status: number;
10
+ body: string;
11
+ finalUrl: string;
12
+ };
13
+ export type NotebooklmRpcCallResult = {
14
+ auth: NotebooklmPageAuth;
15
+ url: string;
16
+ requestBody: string;
17
+ response: NotebooklmFetchResponse;
18
+ result: unknown;
19
+ };
20
+ export declare function extractNotebooklmPageAuthFromHtml(html: string, sourcePath?: string, preferredTokens?: {
21
+ csrfToken?: string;
22
+ sessionId?: string;
23
+ }): NotebooklmPageAuth;
24
+ export declare function getNotebooklmPageAuth(page: IPage): Promise<NotebooklmPageAuth>;
25
+ export declare function buildNotebooklmRpcBody(rpcId: string, params: unknown[] | Record<string, unknown> | null, csrfToken: string): string;
26
+ export declare function stripNotebooklmAntiXssi(rawBody: string): string;
27
+ export declare function parseNotebooklmChunkedResponse(rawBody: string): unknown[];
28
+ export declare function extractNotebooklmRpcResult(rawBody: string, rpcId: string): unknown;
29
+ export declare function fetchNotebooklmInPage(page: IPage, url: string, options?: {
30
+ method?: 'GET' | 'POST';
31
+ headers?: Record<string, string>;
32
+ body?: string;
33
+ }): Promise<NotebooklmFetchResponse>;
34
+ export declare function callNotebooklmRpc(page: IPage, rpcId: string, params: unknown[] | Record<string, unknown> | null, options?: {
35
+ hl?: string;
36
+ }): Promise<NotebooklmRpcCallResult>;
@@ -0,0 +1,189 @@
1
+ import { AuthRequiredError, CliError } from '../../errors.js';
2
+ import { NOTEBOOKLM_DOMAIN } from './shared.js';
3
+ export function extractNotebooklmPageAuthFromHtml(html, sourcePath = '/', preferredTokens) {
4
+ const csrfMatch = html.match(/"SNlM0e":"([^"]+)"/);
5
+ const sessionMatch = html.match(/"FdrFJe":"([^"]+)"/);
6
+ const csrfToken = preferredTokens?.csrfToken?.trim() || (csrfMatch ? csrfMatch[1] : '');
7
+ const sessionId = preferredTokens?.sessionId?.trim() || (sessionMatch ? sessionMatch[1] : '');
8
+ if (!csrfToken || !sessionId) {
9
+ throw new CliError('NOTEBOOKLM_TOKENS', 'NotebookLM page tokens were not found in the current page HTML', 'Open the NotebookLM notebook page in Chrome, wait for it to finish loading, then retry with --verbose if it still fails.');
10
+ }
11
+ return { csrfToken, sessionId, sourcePath: sourcePath || '/' };
12
+ }
13
+ async function probeNotebooklmPageAuth(page) {
14
+ const raw = await page.evaluate(`(() => {
15
+ const wiz = window.WIZ_global_data || {};
16
+ const html = document.documentElement.innerHTML;
17
+ return {
18
+ html,
19
+ sourcePath: location.pathname || '/',
20
+ readyState: document.readyState || '',
21
+ csrfToken: typeof wiz.SNlM0e === 'string' ? wiz.SNlM0e : '',
22
+ sessionId: typeof wiz.FdrFJe === 'string' ? wiz.FdrFJe : '',
23
+ };
24
+ })()`);
25
+ return {
26
+ html: String(raw?.html ?? ''),
27
+ sourcePath: String(raw?.sourcePath ?? '/'),
28
+ readyState: String(raw?.readyState ?? ''),
29
+ csrfToken: String(raw?.csrfToken ?? ''),
30
+ sessionId: String(raw?.sessionId ?? ''),
31
+ };
32
+ }
33
+ export async function getNotebooklmPageAuth(page) {
34
+ let lastError;
35
+ for (let attempt = 0; attempt < 2; attempt += 1) {
36
+ const probe = await probeNotebooklmPageAuth(page);
37
+ try {
38
+ return extractNotebooklmPageAuthFromHtml(probe.html, probe.sourcePath, { csrfToken: probe.csrfToken, sessionId: probe.sessionId });
39
+ }
40
+ catch (error) {
41
+ lastError = error;
42
+ if (attempt === 0 && typeof page.wait === 'function') {
43
+ await page.wait(0.5).catch(() => undefined);
44
+ continue;
45
+ }
46
+ }
47
+ }
48
+ throw lastError;
49
+ }
50
+ export function buildNotebooklmRpcBody(rpcId, params, csrfToken) {
51
+ const rpcRequest = [[[rpcId, JSON.stringify(params), null, 'generic']]];
52
+ return `f.req=${encodeURIComponent(JSON.stringify(rpcRequest))}&at=${encodeURIComponent(csrfToken)}&`;
53
+ }
54
+ export function stripNotebooklmAntiXssi(rawBody) {
55
+ if (!rawBody.startsWith(")]}'"))
56
+ return rawBody;
57
+ return rawBody.replace(/^\)\]\}'\r?\n/, '');
58
+ }
59
+ export function parseNotebooklmChunkedResponse(rawBody) {
60
+ const cleaned = stripNotebooklmAntiXssi(rawBody).trim();
61
+ if (!cleaned)
62
+ return [];
63
+ const lines = cleaned.split('\n');
64
+ const chunks = [];
65
+ for (let i = 0; i < lines.length; i += 1) {
66
+ const line = lines[i].trim();
67
+ if (!line)
68
+ continue;
69
+ if (/^\d+$/.test(line)) {
70
+ const nextLine = lines[i + 1];
71
+ if (!nextLine)
72
+ continue;
73
+ try {
74
+ chunks.push(JSON.parse(nextLine));
75
+ }
76
+ catch {
77
+ // Ignore malformed chunks and keep scanning.
78
+ }
79
+ i += 1;
80
+ continue;
81
+ }
82
+ if (line.startsWith('[')) {
83
+ try {
84
+ chunks.push(JSON.parse(line));
85
+ }
86
+ catch {
87
+ // Ignore malformed chunks and keep scanning.
88
+ }
89
+ }
90
+ }
91
+ return chunks;
92
+ }
93
+ export function extractNotebooklmRpcResult(rawBody, rpcId) {
94
+ const chunks = parseNotebooklmChunkedResponse(rawBody);
95
+ for (const chunk of chunks) {
96
+ if (!Array.isArray(chunk))
97
+ continue;
98
+ const items = Array.isArray(chunk[0]) ? chunk : [chunk];
99
+ for (const item of items) {
100
+ if (!Array.isArray(item) || item.length < 1)
101
+ continue;
102
+ if (item[0] === 'er') {
103
+ const errorCode = typeof item[2] === 'number'
104
+ ? item[2]
105
+ : typeof item[5] === 'number'
106
+ ? item[5]
107
+ : null;
108
+ if (errorCode === 401 || errorCode === 403) {
109
+ throw new AuthRequiredError(NOTEBOOKLM_DOMAIN, `NotebookLM RPC returned auth error (${errorCode})`);
110
+ }
111
+ throw new CliError('NOTEBOOKLM_RPC', `NotebookLM RPC failed${errorCode ? ` (code=${errorCode})` : ''}`, 'Retry from an already logged-in NotebookLM session, or inspect the raw response with debug logging.');
112
+ }
113
+ if (item[0] === 'wrb.fr' && item[1] === rpcId) {
114
+ const payload = item[2];
115
+ if (typeof payload === 'string') {
116
+ try {
117
+ return JSON.parse(payload);
118
+ }
119
+ catch {
120
+ return payload;
121
+ }
122
+ }
123
+ return payload;
124
+ }
125
+ }
126
+ }
127
+ return null;
128
+ }
129
+ export async function fetchNotebooklmInPage(page, url, options = {}) {
130
+ const method = options.method ?? 'GET';
131
+ const headers = options.headers ?? {};
132
+ const body = options.body ?? '';
133
+ const raw = await page.evaluate(`(async () => {
134
+ const request = {
135
+ url: ${JSON.stringify(url)},
136
+ method: ${JSON.stringify(method)},
137
+ headers: ${JSON.stringify(headers)},
138
+ body: ${JSON.stringify(body)},
139
+ };
140
+
141
+ const response = await fetch(request.url, {
142
+ method: request.method,
143
+ headers: request.headers,
144
+ body: request.method === 'GET' ? undefined : request.body,
145
+ credentials: 'include',
146
+ });
147
+
148
+ return {
149
+ ok: response.ok,
150
+ status: response.status,
151
+ body: await response.text(),
152
+ finalUrl: response.url,
153
+ };
154
+ })()`);
155
+ return {
156
+ ok: Boolean(raw?.ok),
157
+ status: Number(raw?.status ?? 0),
158
+ body: String(raw?.body ?? ''),
159
+ finalUrl: String(raw?.finalUrl ?? url),
160
+ };
161
+ }
162
+ export async function callNotebooklmRpc(page, rpcId, params, options = {}) {
163
+ const auth = await getNotebooklmPageAuth(page);
164
+ const requestBody = buildNotebooklmRpcBody(rpcId, params, auth.csrfToken);
165
+ const url = `https://${NOTEBOOKLM_DOMAIN}/_/LabsTailwindUi/data/batchexecute` +
166
+ `?rpcids=${rpcId}&source-path=${encodeURIComponent(auth.sourcePath)}` +
167
+ `&hl=${encodeURIComponent(options.hl ?? 'en')}` +
168
+ `&f.sid=${encodeURIComponent(auth.sessionId)}&rt=c`;
169
+ const response = await fetchNotebooklmInPage(page, url, {
170
+ method: 'POST',
171
+ headers: {
172
+ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
173
+ },
174
+ body: requestBody,
175
+ });
176
+ if (response.status === 401 || response.status === 403) {
177
+ throw new AuthRequiredError(NOTEBOOKLM_DOMAIN, `NotebookLM RPC returned auth error (${response.status})`);
178
+ }
179
+ if (!response.ok) {
180
+ throw new CliError('NOTEBOOKLM_RPC', `NotebookLM RPC request failed with HTTP ${response.status}`, 'Retry from the NotebookLM home page in an already logged-in Chrome session.');
181
+ }
182
+ return {
183
+ auth,
184
+ url,
185
+ requestBody,
186
+ response,
187
+ result: extractNotebooklmRpcResult(response.body, rpcId),
188
+ };
189
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { AuthRequiredError } from '../../errors.js';
3
+ import { buildNotebooklmRpcBody, extractNotebooklmRpcResult, getNotebooklmPageAuth, parseNotebooklmChunkedResponse, } from './rpc.js';
4
+ describe('notebooklm rpc transport', () => {
5
+ it('extracts auth tokens from the page html via page evaluation', async () => {
6
+ const page = {
7
+ evaluate: vi.fn(async (script) => {
8
+ expect(script).toContain('document.documentElement.innerHTML');
9
+ return {
10
+ html: '<html>"SNlM0e":"csrf-123","FdrFJe":"sess-456"</html>',
11
+ sourcePath: '/',
12
+ };
13
+ }),
14
+ };
15
+ await expect(getNotebooklmPageAuth(page)).resolves.toEqual({
16
+ csrfToken: 'csrf-123',
17
+ sessionId: 'sess-456',
18
+ sourcePath: '/',
19
+ });
20
+ expect(page.evaluate).toHaveBeenCalledTimes(1);
21
+ });
22
+ it('falls back to WIZ_global_data tokens when html regex data is missing', async () => {
23
+ const page = {
24
+ evaluate: vi.fn(async () => ({
25
+ html: '<html><body>NotebookLM</body></html>',
26
+ sourcePath: '/notebook/nb-demo',
27
+ readyState: 'complete',
28
+ csrfToken: 'csrf-wiz',
29
+ sessionId: 'sess-wiz',
30
+ })),
31
+ };
32
+ await expect(getNotebooklmPageAuth(page)).resolves.toEqual({
33
+ csrfToken: 'csrf-wiz',
34
+ sessionId: 'sess-wiz',
35
+ sourcePath: '/notebook/nb-demo',
36
+ });
37
+ });
38
+ it('retries token extraction once when the first probe returns no tokens', async () => {
39
+ const page = {
40
+ evaluate: vi.fn()
41
+ .mockResolvedValueOnce({
42
+ html: '<html><body>Loading…</body></html>',
43
+ sourcePath: '/notebook/nb-demo',
44
+ readyState: 'interactive',
45
+ csrfToken: '',
46
+ sessionId: '',
47
+ })
48
+ .mockResolvedValueOnce({
49
+ html: '<html>"SNlM0e":"csrf-123","FdrFJe":"sess-456"</html>',
50
+ sourcePath: '/notebook/nb-demo',
51
+ readyState: 'complete',
52
+ csrfToken: '',
53
+ sessionId: '',
54
+ }),
55
+ wait: vi.fn(async () => undefined),
56
+ };
57
+ await expect(getNotebooklmPageAuth(page)).resolves.toEqual({
58
+ csrfToken: 'csrf-123',
59
+ sessionId: 'sess-456',
60
+ sourcePath: '/notebook/nb-demo',
61
+ });
62
+ expect(page.evaluate).toHaveBeenCalledTimes(2);
63
+ });
64
+ it('builds the rpc body with the expected notebooklm payload shape', () => {
65
+ const body = buildNotebooklmRpcBody('wXbhsf', [null, 1, null, [2]], 'csrf-123');
66
+ expect(body).toContain('f.req=');
67
+ expect(body).toContain('at=csrf-123');
68
+ expect(body.endsWith('&')).toBe(true);
69
+ expect(decodeURIComponent(body)).toContain('"[null,1,null,[2]]"');
70
+ });
71
+ it('parses chunked batchexecute responses into json chunks', () => {
72
+ const raw = `)]}'\n107\n[["wrb.fr","wXbhsf","[[[\\\"Notebook One\\\",null,\\\"nb1\\\",null,null,[null,false,null,null,null,[1704067200]]]]]"]]`;
73
+ const chunks = parseNotebooklmChunkedResponse(raw);
74
+ expect(chunks).toHaveLength(1);
75
+ expect(Array.isArray(chunks[0])).toBe(true);
76
+ expect(chunks[0]).toEqual([
77
+ [
78
+ 'wrb.fr',
79
+ 'wXbhsf',
80
+ '[[["Notebook One",null,"nb1",null,null,[null,false,null,null,null,[1704067200]]]]]',
81
+ ],
82
+ ]);
83
+ });
84
+ it('extracts the rpc payload from wrb.fr responses', () => {
85
+ const raw = `)]}'\n107\n[["wrb.fr","wXbhsf","[[[\\\"Notebook One\\\",null,\\\"nb1\\\",null,null,[null,false,null,null,null,[1704067200]]]]]"]]`;
86
+ const result = extractNotebooklmRpcResult(raw, 'wXbhsf');
87
+ expect(result).toEqual([
88
+ [
89
+ ['Notebook One', null, 'nb1', null, null, [null, false, null, null, null, [1704067200]]],
90
+ ],
91
+ ]);
92
+ });
93
+ it('classifies auth errors as AuthRequiredError', () => {
94
+ const raw = `)]}'\n25\n[["er",null,null,null,null,401,"generic"]]`;
95
+ expect(() => extractNotebooklmRpcResult(raw, 'wXbhsf')).toThrow(AuthRequiredError);
96
+ try {
97
+ extractNotebooklmRpcResult(raw, 'wXbhsf');
98
+ }
99
+ catch (error) {
100
+ expect(error).toBeInstanceOf(AuthRequiredError);
101
+ expect(error.domain).toBe('notebooklm.google.com');
102
+ expect(error.code).toBe('AUTH_REQUIRED');
103
+ }
104
+ });
105
+ });