@jackwener/opencli 1.5.6 → 1.5.7

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 (334) hide show
  1. package/CHANGELOG.md +26 -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/idle-manager.d.ts +19 -0
  176. package/dist/idle-manager.js +54 -0
  177. package/dist/launcher.d.ts +36 -0
  178. package/dist/launcher.js +152 -0
  179. package/dist/launcher.test.d.ts +1 -0
  180. package/dist/launcher.test.js +57 -0
  181. package/dist/main.js +3 -3
  182. package/dist/registry.d.ts +1 -0
  183. package/dist/registry.js +31 -3
  184. package/dist/registry.test.js +13 -0
  185. package/dist/runtime.d.ts +5 -3
  186. package/dist/runtime.js +12 -5
  187. package/dist/serialization.d.ts +1 -0
  188. package/dist/serialization.js +3 -0
  189. package/dist/serialization.test.js +17 -1
  190. package/dist/tui.d.ts +7 -0
  191. package/dist/tui.js +52 -0
  192. package/dist/tui.test.d.ts +1 -0
  193. package/dist/tui.test.js +19 -0
  194. package/dist/weixin-download.test.js +14 -0
  195. package/docs/.vitepress/config.mts +1 -0
  196. package/docs/adapters/browser/notebooklm.md +69 -0
  197. package/docs/adapters/browser/xiaohongshu.md +19 -10
  198. package/docs/adapters/index.md +67 -66
  199. package/docs/guide/browser-bridge.md +12 -0
  200. package/docs/guide/troubleshooting.md +9 -4
  201. package/docs/superpowers/plans/2026-03-31-daemon-lifecycle-redesign.md +857 -0
  202. package/docs/superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md +208 -0
  203. package/docs/zh/guide/browser-bridge.md +12 -0
  204. package/extension/dist/background.js +794 -513
  205. package/extension/src/background.test.ts +202 -2
  206. package/extension/src/background.ts +174 -10
  207. package/extension/src/cdp.ts +12 -0
  208. package/extension/src/protocol.ts +7 -5
  209. package/package.json +1 -1
  210. package/src/browser/cdp.ts +24 -17
  211. package/src/browser/daemon-client.ts +7 -1
  212. package/src/browser/dom-helpers.test.ts +15 -1
  213. package/src/browser/dom-helpers.ts +1 -0
  214. package/src/browser/mcp.ts +18 -13
  215. package/src/browser/page.test.ts +58 -0
  216. package/src/browser/page.ts +18 -2
  217. package/src/browser/stealth.test.ts +153 -0
  218. package/src/browser/stealth.ts +198 -0
  219. package/src/browser.test.ts +1 -1
  220. package/src/build-manifest.test.ts +2 -0
  221. package/src/build-manifest.ts +6 -1
  222. package/src/cli.ts +21 -3
  223. package/src/clis/antigravity/SKILL.md +3 -12
  224. package/src/clis/antigravity/serve.ts +5 -10
  225. package/src/clis/bilibili/subtitle.test.ts +60 -0
  226. package/src/clis/bilibili/subtitle.ts +4 -0
  227. package/src/clis/chatwise/ask.ts +0 -2
  228. package/src/clis/chatwise/export.ts +0 -2
  229. package/src/clis/chatwise/history.ts +0 -2
  230. package/src/clis/chatwise/model.ts +0 -2
  231. package/src/clis/chatwise/new.ts +1 -2
  232. package/src/clis/chatwise/read.ts +0 -2
  233. package/src/clis/chatwise/screenshot.ts +1 -2
  234. package/src/clis/chatwise/send.ts +0 -2
  235. package/src/clis/chatwise/status.ts +1 -2
  236. package/src/clis/ctrip/search.test.ts +73 -0
  237. package/src/clis/ctrip/search.ts +97 -47
  238. package/src/clis/douyin/_shared/sts2.test.ts +31 -0
  239. package/src/clis/douyin/_shared/sts2.ts +11 -3
  240. package/src/clis/douyin/activities.test.ts +41 -1
  241. package/src/clis/douyin/activities.ts +12 -3
  242. package/src/clis/douyin/collections.test.ts +35 -2
  243. package/src/clis/douyin/collections.ts +1 -1
  244. package/src/clis/douyin/draft.test.ts +444 -2
  245. package/src/clis/douyin/draft.ts +382 -218
  246. package/src/clis/douyin/hashtag.test.ts +42 -2
  247. package/src/clis/douyin/hashtag.ts +11 -3
  248. package/src/clis/douyin/profile.test.ts +43 -1
  249. package/src/clis/douyin/profile.ts +9 -2
  250. package/src/clis/douyin/videos.test.ts +52 -2
  251. package/src/clis/douyin/videos.ts +49 -15
  252. package/src/clis/facebook/search.test.ts +70 -0
  253. package/src/clis/facebook/search.yaml +4 -3
  254. package/src/clis/instagram/download.test.ts +159 -0
  255. package/src/clis/instagram/download.ts +286 -0
  256. package/src/clis/notebooklm/bind-current.test.ts +43 -0
  257. package/src/clis/notebooklm/bind-current.ts +36 -0
  258. package/src/clis/notebooklm/binding.test.ts +53 -0
  259. package/src/clis/notebooklm/compat.test.ts +19 -0
  260. package/src/clis/notebooklm/current.ts +38 -0
  261. package/src/clis/notebooklm/get.ts +53 -0
  262. package/src/clis/notebooklm/history.test.ts +70 -0
  263. package/src/clis/notebooklm/history.ts +36 -0
  264. package/src/clis/notebooklm/list.ts +40 -0
  265. package/src/clis/notebooklm/note-list.test.ts +64 -0
  266. package/src/clis/notebooklm/note-list.ts +42 -0
  267. package/src/clis/notebooklm/notes-get.test.ts +88 -0
  268. package/src/clis/notebooklm/notes-get.ts +67 -0
  269. package/src/clis/notebooklm/rpc.test.ts +126 -0
  270. package/src/clis/notebooklm/rpc.ts +286 -0
  271. package/src/clis/notebooklm/shared.ts +98 -0
  272. package/src/clis/notebooklm/source-fulltext.test.ts +123 -0
  273. package/src/clis/notebooklm/source-fulltext.ts +69 -0
  274. package/src/clis/notebooklm/source-get.test.ts +100 -0
  275. package/src/clis/notebooklm/source-get.ts +60 -0
  276. package/src/clis/notebooklm/source-guide.test.ts +121 -0
  277. package/src/clis/notebooklm/source-guide.ts +69 -0
  278. package/src/clis/notebooklm/source-list.ts +45 -0
  279. package/src/clis/notebooklm/status.ts +34 -0
  280. package/src/clis/notebooklm/summary.test.ts +94 -0
  281. package/src/clis/notebooklm/summary.ts +45 -0
  282. package/src/clis/notebooklm/utils.test.ts +446 -0
  283. package/src/clis/notebooklm/utils.ts +893 -0
  284. package/src/clis/substack/utils.test.ts +54 -0
  285. package/src/clis/substack/utils.ts +10 -2
  286. package/src/clis/v2ex/hot.yaml +4 -1
  287. package/src/clis/v2ex/latest.yaml +4 -1
  288. package/src/clis/v2ex/topic.yaml +6 -1
  289. package/src/clis/weixin/download.ts +95 -6
  290. package/src/clis/weread/book.ts +142 -2
  291. package/src/clis/weread/commands.test.ts +314 -154
  292. package/src/clis/weread/utils.ts +33 -4
  293. package/src/clis/xiaohongshu/comments.test.ts +85 -9
  294. package/src/clis/xiaohongshu/comments.ts +76 -17
  295. package/src/clis/xiaohongshu/download.test.ts +96 -0
  296. package/src/clis/xiaohongshu/download.ts +83 -22
  297. package/src/clis/xiaohongshu/note-helpers.ts +25 -0
  298. package/src/clis/xiaohongshu/note.test.ts +164 -0
  299. package/src/clis/xiaohongshu/note.ts +86 -0
  300. package/src/clis/xiaohongshu/search.test.ts +11 -4
  301. package/src/clis/xiaohongshu/search.ts +13 -0
  302. package/src/clis/youtube/search.ts +57 -17
  303. package/src/clis/zhihu/question.test.ts +71 -0
  304. package/src/clis/zhihu/question.ts +27 -15
  305. package/src/commanderAdapter.test.ts +30 -0
  306. package/src/commanderAdapter.ts +7 -0
  307. package/src/commands/daemon.test.ts +238 -0
  308. package/src/commands/daemon.ts +135 -0
  309. package/src/completion.ts +2 -1
  310. package/src/constants.ts +3 -0
  311. package/src/daemon.test.ts +88 -0
  312. package/src/daemon.ts +26 -14
  313. package/src/discovery.ts +52 -2
  314. package/src/electron-apps.test.ts +50 -0
  315. package/src/electron-apps.ts +89 -0
  316. package/src/engine.test.ts +45 -9
  317. package/src/execution.ts +24 -19
  318. package/src/idle-manager.ts +60 -0
  319. package/src/launcher.test.ts +67 -0
  320. package/src/launcher.ts +185 -0
  321. package/src/main.ts +3 -2
  322. package/src/registry.test.ts +15 -0
  323. package/src/registry.ts +32 -3
  324. package/src/runtime.ts +13 -7
  325. package/src/serialization.test.ts +19 -1
  326. package/src/serialization.ts +2 -0
  327. package/src/tui.test.ts +23 -0
  328. package/src/tui.ts +65 -0
  329. package/src/weixin-download.test.ts +27 -0
  330. package/tests/e2e/browser-public-extended.test.ts +6 -2
  331. package/chatwise-opencli.ps1 +0 -82
  332. package/dist/clis/chatwise/shared.d.ts +0 -2
  333. package/dist/clis/chatwise/shared.js +0 -6
  334. package/src/clis/chatwise/shared.ts +0 -8
@@ -0,0 +1,286 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { cli, Strategy } from '../../registry.js';
5
+ import { ArgumentError, AuthRequiredError, CliError, CommandExecutionError, EXIT_CODES } from '../../errors.js';
6
+ import { httpDownload } from '../../download/index.js';
7
+ import type { IPage } from '../../types.js';
8
+
9
+ const INSTAGRAM_GRAPHQL_DOC_ID = '8845758582119845';
10
+ const INSTAGRAM_GRAPHQL_APP_ID = '936619743392459';
11
+ const INSTAGRAM_HOST_SUFFIX = 'instagram.com';
12
+ const SUPPORTED_KINDS = new Set(['p', 'reel', 'tv']);
13
+
14
+ export interface InstagramMediaTarget {
15
+ kind: 'p' | 'reel' | 'tv';
16
+ shortcode: string;
17
+ canonicalUrl: string;
18
+ }
19
+
20
+ interface InstagramPageMediaItem {
21
+ type: 'image' | 'video';
22
+ url: string;
23
+ }
24
+
25
+ interface InstagramFetchResult {
26
+ ok: boolean;
27
+ shortcode?: string;
28
+ owner?: string;
29
+ items?: InstagramPageMediaItem[];
30
+ errorCode?: string;
31
+ error?: string;
32
+ }
33
+
34
+ interface DownloadedMediaItem extends InstagramPageMediaItem {
35
+ filename: string;
36
+ }
37
+
38
+ function displayPath(filePath: string): string {
39
+ const home = os.homedir();
40
+ return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath;
41
+ }
42
+
43
+
44
+ export function parseInstagramMediaTarget(input: string): InstagramMediaTarget {
45
+ const raw = String(input || '').trim();
46
+ if (!raw) {
47
+ throw new ArgumentError(
48
+ 'Instagram URL is required',
49
+ 'Expected https://www.instagram.com/p/... or https://www.instagram.com/reel/...',
50
+ );
51
+ }
52
+
53
+ let url: URL;
54
+ try {
55
+ url = new URL(raw);
56
+ } catch {
57
+ throw new ArgumentError(
58
+ `Invalid Instagram URL: ${raw}`,
59
+ 'Expected https://www.instagram.com/p/<shortcode>/ or /reel/<shortcode>/',
60
+ );
61
+ }
62
+
63
+ if (!['http:', 'https:'].includes(url.protocol)) {
64
+ throw new ArgumentError(`Unsupported URL protocol: ${url.protocol}`);
65
+ }
66
+
67
+ const host = url.hostname.toLowerCase();
68
+ if (host !== INSTAGRAM_HOST_SUFFIX && !host.endsWith(`.${INSTAGRAM_HOST_SUFFIX}`)) {
69
+ throw new ArgumentError(`Unsupported host: ${host}`, 'Only instagram.com URLs are supported');
70
+ }
71
+
72
+ const segments = url.pathname.split('/').filter(Boolean);
73
+ let kind: string | undefined;
74
+ let shortcode: string | undefined;
75
+
76
+ if (segments.length >= 2 && SUPPORTED_KINDS.has(segments[0]!)) {
77
+ kind = segments[0];
78
+ shortcode = segments[1];
79
+ } else if (segments.length >= 3 && SUPPORTED_KINDS.has(segments[1]!)) {
80
+ kind = segments[1];
81
+ shortcode = segments[2];
82
+ }
83
+
84
+ if (!kind || !shortcode) {
85
+ throw new ArgumentError(
86
+ `Unsupported Instagram media URL: ${raw}`,
87
+ 'Only /p/<shortcode>/, /reel/<shortcode>/, and /tv/<shortcode>/ links are supported',
88
+ );
89
+ }
90
+
91
+ return {
92
+ kind: kind as 'p' | 'reel' | 'tv',
93
+ shortcode,
94
+ canonicalUrl: `https://www.instagram.com/${kind}/${shortcode}/`,
95
+ };
96
+ }
97
+
98
+ export function buildInstagramDownloadItems(shortcode: string, items: InstagramPageMediaItem[]): DownloadedMediaItem[] {
99
+ return items
100
+ .filter((item) => item?.url)
101
+ .map((item, index) => {
102
+ const fallbackExt = item.type === 'video' ? '.mp4' : '.jpg';
103
+ let ext = fallbackExt;
104
+
105
+ try {
106
+ const pathname = new URL(item.url).pathname;
107
+ const candidateExt = path.extname(pathname).toLowerCase();
108
+ if (candidateExt && candidateExt.length <= 8) ext = candidateExt;
109
+ } catch {
110
+ ext = fallbackExt;
111
+ }
112
+
113
+ return {
114
+ type: item.type,
115
+ url: item.url,
116
+ filename: `${shortcode}_${String(index + 1).padStart(2, '0')}${ext}`,
117
+ };
118
+ });
119
+ }
120
+
121
+ export function buildInstagramFetchScript(shortcode: string): string {
122
+ return `
123
+ (async () => {
124
+ const shortcode = ${JSON.stringify(shortcode)};
125
+ const docId = ${JSON.stringify(INSTAGRAM_GRAPHQL_DOC_ID)};
126
+ const variables = {
127
+ shortcode,
128
+ fetch_tagged_user_count: null,
129
+ hoisted_comment_id: null,
130
+ hoisted_reply_id: null,
131
+ };
132
+ const url = 'https://www.instagram.com/graphql/query/?doc_id=' + docId + '&variables=' + encodeURIComponent(JSON.stringify(variables));
133
+ const res = await fetch(url, {
134
+ credentials: 'include',
135
+ headers: {
136
+ 'Accept': 'application/json,text/plain,*/*',
137
+ 'X-IG-App-ID': ${JSON.stringify(INSTAGRAM_GRAPHQL_APP_ID)},
138
+ },
139
+ });
140
+ const rawText = await res.text();
141
+
142
+ let data = null;
143
+ try {
144
+ data = rawText ? JSON.parse(rawText) : null;
145
+ } catch {
146
+ return {
147
+ ok: false,
148
+ errorCode: 'COMMAND_EXEC',
149
+ error: 'Instagram returned non-JSON content while fetching media metadata',
150
+ };
151
+ }
152
+
153
+ const message = typeof data?.message === 'string' ? data.message : '';
154
+ const lowered = (message || '').toLowerCase();
155
+
156
+ if (!res.ok) {
157
+ if (res.status === 401 || res.status === 403 || data?.require_login) {
158
+ return { ok: false, errorCode: 'AUTH_REQUIRED', error: message || ('HTTP ' + res.status) };
159
+ }
160
+ if (res.status === 429) {
161
+ return { ok: false, errorCode: 'RATE_LIMITED', error: message || 'HTTP 429' };
162
+ }
163
+ if (res.status === 404 || res.status === 410) {
164
+ return { ok: false, errorCode: 'PRIVATE_OR_UNAVAILABLE', error: message || ('HTTP ' + res.status) };
165
+ }
166
+ return { ok: false, errorCode: 'COMMAND_EXEC', error: message || ('HTTP ' + res.status) };
167
+ }
168
+
169
+ if (data?.require_login) {
170
+ return { ok: false, errorCode: 'AUTH_REQUIRED', error: message || 'Instagram login required' };
171
+ }
172
+ if (lowered.includes('wait a few minutes') || lowered.includes('rate')) {
173
+ return { ok: false, errorCode: 'RATE_LIMITED', error: message || 'Instagram rate limit triggered' };
174
+ }
175
+
176
+ const media = data?.data?.xdt_shortcode_media;
177
+ if (!media) {
178
+ return {
179
+ ok: false,
180
+ errorCode: 'PRIVATE_OR_UNAVAILABLE',
181
+ error: message || 'Post may be private, unavailable, or inaccessible to the current browser session',
182
+ };
183
+ }
184
+
185
+ const nodes = Array.isArray(media?.edge_sidecar_to_children?.edges) && media.edge_sidecar_to_children.edges.length > 0
186
+ ? media.edge_sidecar_to_children.edges.map((edge) => edge?.node).filter(Boolean)
187
+ : [media];
188
+
189
+ const items = nodes
190
+ .map((node) => ({
191
+ type: node?.is_video ? 'video' : 'image',
192
+ url: String(node?.is_video ? (node?.video_url || '') : (node?.display_url || '')),
193
+ }))
194
+ .filter((item) => item.url);
195
+
196
+ return {
197
+ ok: true,
198
+ shortcode: media.shortcode || shortcode,
199
+ owner: media?.owner?.username || '',
200
+ items,
201
+ };
202
+ })()
203
+ `;
204
+ }
205
+
206
+ function ensurePage(page: IPage | null): IPage {
207
+ if (!page) throw new CommandExecutionError('Browser session required');
208
+ return page;
209
+ }
210
+
211
+ function normalizeFetchResult(result: unknown): InstagramFetchResult {
212
+ if (!result || typeof result !== 'object') {
213
+ throw new CommandExecutionError('Failed to fetch Instagram media metadata');
214
+ }
215
+ return result as InstagramFetchResult;
216
+ }
217
+
218
+ function handleFetchFailure(result: InstagramFetchResult): never {
219
+ const message = result.error || 'Instagram media fetch failed';
220
+
221
+ if (result.errorCode === 'AUTH_REQUIRED') {
222
+ throw new AuthRequiredError('instagram.com', message);
223
+ }
224
+ if (result.errorCode === 'RATE_LIMITED') {
225
+ throw new CliError(
226
+ 'RATE_LIMITED',
227
+ message,
228
+ 'Wait a few minutes and retry, or switch to a browser session with a warmer Instagram login state.',
229
+ EXIT_CODES.TEMPFAIL,
230
+ );
231
+ }
232
+ if (result.errorCode === 'PRIVATE_OR_UNAVAILABLE') {
233
+ throw new CommandExecutionError(message, 'Open the post in a logged-in browser session and retry');
234
+ }
235
+
236
+ throw new CommandExecutionError(message);
237
+ }
238
+
239
+ async function downloadInstagramMedia(items: DownloadedMediaItem[], outputDir: string): Promise<void> {
240
+ fs.mkdirSync(outputDir, { recursive: true });
241
+
242
+ for (const item of items) {
243
+ const destPath = path.join(outputDir, item.filename);
244
+ const result = await httpDownload(item.url, destPath, {
245
+ timeout: item.type === 'video' ? 120000 : 60000,
246
+ });
247
+
248
+ if (!result.success) {
249
+ throw new CommandExecutionError(`Failed to download ${item.filename}: ${result.error || 'unknown error'}`);
250
+ }
251
+ }
252
+ }
253
+
254
+ cli({
255
+ site: 'instagram',
256
+ name: 'download',
257
+ description: 'Download images and videos from Instagram posts and reels',
258
+ domain: 'www.instagram.com',
259
+ strategy: Strategy.COOKIE,
260
+ navigateBefore: false,
261
+ args: [
262
+ { name: 'url', positional: true, required: true, help: 'Instagram post / reel / tv URL' },
263
+ { name: 'path', default: path.join(os.homedir(), 'Downloads', 'Instagram'), help: 'Download directory' },
264
+ ],
265
+ func: async (page, kwargs) => {
266
+ const browserPage = ensurePage(page);
267
+ const target = parseInstagramMediaTarget(String(kwargs.url ?? ''));
268
+ const outputRoot = String(kwargs.path ?? path.join(os.homedir(), 'Downloads', 'Instagram'));
269
+
270
+ await browserPage.goto(target.canonicalUrl);
271
+
272
+ const fetchResult = normalizeFetchResult(await browserPage.evaluate(buildInstagramFetchScript(target.shortcode)));
273
+ if (!fetchResult.ok) handleFetchFailure(fetchResult);
274
+
275
+ const shortcode = fetchResult.shortcode || target.shortcode;
276
+ const mediaItems = buildInstagramDownloadItems(shortcode, fetchResult.items || []);
277
+ if (mediaItems.length === 0) {
278
+ throw new CommandExecutionError('No downloadable media found');
279
+ }
280
+
281
+ const savedDir = path.join(outputRoot, shortcode);
282
+ await downloadInstagramMedia(mediaItems, savedDir);
283
+ console.log(`📁 saved: ${displayPath(savedDir)}`);
284
+ return null;
285
+ },
286
+ });
@@ -0,0 +1,43 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const { mockBindCurrentTab } = vi.hoisted(() => ({
4
+ mockBindCurrentTab: vi.fn(),
5
+ }));
6
+
7
+ vi.mock('../../browser/daemon-client.js', () => ({
8
+ bindCurrentTab: mockBindCurrentTab,
9
+ }));
10
+
11
+ import { getRegistry } from '../../registry.js';
12
+ import './bind-current.js';
13
+
14
+ describe('notebooklm bind-current', () => {
15
+ const command = getRegistry().get('notebooklm/bind-current');
16
+
17
+ beforeEach(() => {
18
+ mockBindCurrentTab.mockReset();
19
+ });
20
+
21
+ it('binds the current notebook tab into site:notebooklm', async () => {
22
+ mockBindCurrentTab.mockResolvedValue({
23
+ workspace: 'site:notebooklm',
24
+ tabId: 123,
25
+ title: 'Bound Notebook',
26
+ url: 'https://notebooklm.google.com/notebook/nb-live',
27
+ });
28
+
29
+ const result = await command!.func!({} as any, {});
30
+
31
+ expect(mockBindCurrentTab).toHaveBeenCalledWith('site:notebooklm', {
32
+ matchDomain: 'notebooklm.google.com',
33
+ matchPathPrefix: '/notebook/',
34
+ });
35
+ expect(result).toEqual([{
36
+ workspace: 'site:notebooklm',
37
+ tab_id: 123,
38
+ notebook_id: 'nb-live',
39
+ title: 'Bound Notebook',
40
+ url: 'https://notebooklm.google.com/notebook/nb-live',
41
+ }]);
42
+ });
43
+ });
@@ -0,0 +1,36 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { bindCurrentTab } from '../../browser/daemon-client.js';
3
+ import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
4
+ import { parseNotebooklmIdFromUrl } from './utils.js';
5
+
6
+ cli({
7
+ site: NOTEBOOKLM_SITE,
8
+ name: 'bind-current',
9
+ aliases: ['use'],
10
+ description: 'Bind the current active NotebookLM notebook tab into the site:notebooklm workspace',
11
+ domain: NOTEBOOKLM_DOMAIN,
12
+ strategy: Strategy.COOKIE,
13
+ browser: true,
14
+ navigateBefore: false,
15
+ args: [],
16
+ columns: ['workspace', 'tab_id', 'notebook_id', 'title', 'url'],
17
+ func: async () => {
18
+ const result = await bindCurrentTab(`site:${NOTEBOOKLM_SITE}`, {
19
+ matchDomain: NOTEBOOKLM_DOMAIN,
20
+ matchPathPrefix: '/notebook/',
21
+ }) as {
22
+ tabId?: number;
23
+ workspace?: string;
24
+ title?: string;
25
+ url?: string;
26
+ };
27
+
28
+ return [{
29
+ workspace: result.workspace ?? `site:${NOTEBOOKLM_SITE}`,
30
+ tab_id: result.tabId ?? null,
31
+ notebook_id: result.url ? parseNotebooklmIdFromUrl(result.url) : '',
32
+ title: result.title ?? '',
33
+ url: result.url ?? '',
34
+ }];
35
+ },
36
+ });
@@ -0,0 +1,53 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const { mockBindCurrentTab } = vi.hoisted(() => ({
4
+ mockBindCurrentTab: vi.fn(),
5
+ }));
6
+
7
+ vi.mock('../../browser/daemon-client.js', () => ({
8
+ bindCurrentTab: mockBindCurrentTab,
9
+ }));
10
+
11
+ import { ensureNotebooklmNotebookBinding } from './utils.js';
12
+
13
+ describe('notebooklm automatic binding', () => {
14
+ const originalEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
15
+
16
+ beforeEach(() => {
17
+ mockBindCurrentTab.mockReset();
18
+ if (originalEndpoint === undefined) delete process.env.OPENCLI_CDP_ENDPOINT;
19
+ else process.env.OPENCLI_CDP_ENDPOINT = originalEndpoint;
20
+ });
21
+
22
+ it('does nothing when the current page is already a notebook page', async () => {
23
+ const page = {
24
+ getCurrentUrl: async () => 'https://notebooklm.google.com/notebook/nb-demo',
25
+ };
26
+
27
+ await expect(ensureNotebooklmNotebookBinding(page as any)).resolves.toBe(false);
28
+ expect(mockBindCurrentTab).not.toHaveBeenCalled();
29
+ });
30
+
31
+ it('best-effort binds a notebook page through the browser bridge when currently on home', async () => {
32
+ const page = {
33
+ getCurrentUrl: async () => 'https://notebooklm.google.com/',
34
+ };
35
+
36
+ mockBindCurrentTab.mockResolvedValue({});
37
+ await expect(ensureNotebooklmNotebookBinding(page as any)).resolves.toBe(true);
38
+ expect(mockBindCurrentTab).toHaveBeenCalledWith('site:notebooklm', {
39
+ matchDomain: 'notebooklm.google.com',
40
+ matchPathPrefix: '/notebook/',
41
+ });
42
+ });
43
+
44
+ it('skips daemon binding in direct CDP mode', async () => {
45
+ process.env.OPENCLI_CDP_ENDPOINT = 'ws://127.0.0.1:9222/devtools/page/1';
46
+ const page = {
47
+ getCurrentUrl: async () => 'https://notebooklm.google.com/',
48
+ };
49
+
50
+ await expect(ensureNotebooklmNotebookBinding(page as any)).resolves.toBe(false);
51
+ expect(mockBindCurrentTab).not.toHaveBeenCalled();
52
+ });
53
+ });
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getRegistry } from '../../registry.js';
3
+ import './bind-current.js';
4
+ import './get.js';
5
+ import './note-list.js';
6
+
7
+ describe('notebooklm compatibility aliases', () => {
8
+ it('registers use as a compatibility alias for bind-current', () => {
9
+ expect(getRegistry().get('notebooklm/use')).toBe(getRegistry().get('notebooklm/bind-current'));
10
+ });
11
+
12
+ it('registers metadata as a compatibility alias for get', () => {
13
+ expect(getRegistry().get('notebooklm/metadata')).toBe(getRegistry().get('notebooklm/get'));
14
+ });
15
+
16
+ it('registers notes-list as a compatibility alias for note-list', () => {
17
+ expect(getRegistry().get('notebooklm/notes-list')).toBe(getRegistry().get('notebooklm/note-list'));
18
+ });
19
+ });
@@ -0,0 +1,38 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { EmptyResultError } from '../../errors.js';
3
+ import type { IPage } from '../../types.js';
4
+ import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
5
+ import { ensureNotebooklmNotebookBinding, getNotebooklmPageState, readCurrentNotebooklm, requireNotebooklmSession } from './utils.js';
6
+
7
+ cli({
8
+ site: NOTEBOOKLM_SITE,
9
+ name: 'current',
10
+ description: 'Show metadata for the currently opened NotebookLM notebook tab',
11
+ domain: NOTEBOOKLM_DOMAIN,
12
+ strategy: Strategy.COOKIE,
13
+ browser: true,
14
+ navigateBefore: false,
15
+ args: [],
16
+ columns: ['id', 'title', 'url', 'source'],
17
+ func: async (page: IPage) => {
18
+ await ensureNotebooklmNotebookBinding(page);
19
+ await requireNotebooklmSession(page);
20
+ const state = await getNotebooklmPageState(page);
21
+ if (state.kind !== 'notebook') {
22
+ throw new EmptyResultError(
23
+ 'opencli notebooklm current',
24
+ 'Open a specific NotebookLM notebook tab first, then retry.',
25
+ );
26
+ }
27
+
28
+ const current = await readCurrentNotebooklm(page);
29
+ if (!current) {
30
+ throw new EmptyResultError(
31
+ 'opencli notebooklm current',
32
+ 'NotebookLM notebook metadata was not found on the current page.',
33
+ );
34
+ }
35
+
36
+ return [current];
37
+ },
38
+ });
@@ -0,0 +1,53 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+ import { EmptyResultError } from '../../errors.js';
4
+ import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
5
+ import {
6
+ ensureNotebooklmNotebookBinding,
7
+ getNotebooklmDetailViaRpc,
8
+ getNotebooklmPageState,
9
+ readCurrentNotebooklm,
10
+ requireNotebooklmSession,
11
+ } from './utils.js';
12
+
13
+ cli({
14
+ site: NOTEBOOKLM_SITE,
15
+ name: 'get',
16
+ aliases: ['metadata'],
17
+ description: 'Get rich metadata for the currently opened NotebookLM notebook',
18
+ domain: NOTEBOOKLM_DOMAIN,
19
+ strategy: Strategy.COOKIE,
20
+ browser: true,
21
+ navigateBefore: false,
22
+ args: [],
23
+ columns: ['id', 'title', 'emoji', 'source_count', 'created_at', 'updated_at', 'url', 'source'],
24
+ func: async (page: IPage) => {
25
+ await ensureNotebooklmNotebookBinding(page);
26
+ await requireNotebooklmSession(page);
27
+ const state = await getNotebooklmPageState(page);
28
+ if (state.kind !== 'notebook') {
29
+ throw new EmptyResultError(
30
+ 'opencli notebooklm get',
31
+ 'Open a specific NotebookLM notebook tab first, then retry.',
32
+ );
33
+ }
34
+
35
+ const rpcRow = await getNotebooklmDetailViaRpc(page).catch(() => null);
36
+ if (rpcRow) return [rpcRow];
37
+
38
+ const current = await readCurrentNotebooklm(page);
39
+ if (!current) {
40
+ throw new EmptyResultError(
41
+ 'opencli notebooklm get',
42
+ 'NotebookLM notebook metadata was not found on the current page.',
43
+ );
44
+ }
45
+
46
+ return [{
47
+ ...current,
48
+ emoji: null,
49
+ source_count: null,
50
+ updated_at: null,
51
+ }];
52
+ },
53
+ });
@@ -0,0 +1,70 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const {
4
+ mockListNotebooklmHistoryViaRpc,
5
+ mockGetNotebooklmPageState,
6
+ mockRequireNotebooklmSession,
7
+ } = vi.hoisted(() => ({
8
+ mockListNotebooklmHistoryViaRpc: vi.fn(),
9
+ mockGetNotebooklmPageState: vi.fn(),
10
+ mockRequireNotebooklmSession: vi.fn(),
11
+ }));
12
+
13
+ vi.mock('./utils.js', async () => {
14
+ const actual = await vi.importActual<typeof import('./utils.js')>('./utils.js');
15
+ return {
16
+ ...actual,
17
+ getNotebooklmPageState: mockGetNotebooklmPageState,
18
+ listNotebooklmHistoryViaRpc: mockListNotebooklmHistoryViaRpc,
19
+ requireNotebooklmSession: mockRequireNotebooklmSession,
20
+ };
21
+ });
22
+
23
+ import { getRegistry } from '../../registry.js';
24
+ import './history.js';
25
+
26
+ describe('notebooklm history', () => {
27
+ const history = getRegistry().get('notebooklm/history');
28
+
29
+ beforeEach(() => {
30
+ mockListNotebooklmHistoryViaRpc.mockReset();
31
+ mockGetNotebooklmPageState.mockReset();
32
+ mockRequireNotebooklmSession.mockReset();
33
+ mockRequireNotebooklmSession.mockResolvedValue(undefined);
34
+ mockGetNotebooklmPageState.mockResolvedValue({
35
+ url: 'https://notebooklm.google.com/notebook/nb-demo',
36
+ title: 'Browser Automation',
37
+ hostname: 'notebooklm.google.com',
38
+ kind: 'notebook',
39
+ notebookId: 'nb-demo',
40
+ loginRequired: false,
41
+ notebookCount: 1,
42
+ });
43
+ });
44
+
45
+ it('lists notebook history threads from the browser rpc', async () => {
46
+ mockListNotebooklmHistoryViaRpc.mockResolvedValue([
47
+ {
48
+ notebook_id: 'nb-demo',
49
+ thread_id: '28e0f2cb-4591-45a3-a661-7653666f7c78',
50
+ item_count: 0,
51
+ preview: 'Summarize this notebook',
52
+ url: 'https://notebooklm.google.com/notebook/nb-demo',
53
+ source: 'rpc',
54
+ },
55
+ ]);
56
+
57
+ const result = await history!.func!({} as any, {});
58
+
59
+ expect(result).toEqual([
60
+ {
61
+ notebook_id: 'nb-demo',
62
+ thread_id: '28e0f2cb-4591-45a3-a661-7653666f7c78',
63
+ item_count: 0,
64
+ preview: 'Summarize this notebook',
65
+ url: 'https://notebooklm.google.com/notebook/nb-demo',
66
+ source: 'rpc',
67
+ },
68
+ ]);
69
+ });
70
+ });
@@ -0,0 +1,36 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+ import { EmptyResultError } from '../../errors.js';
4
+ import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
5
+ import {
6
+ ensureNotebooklmNotebookBinding,
7
+ getNotebooklmPageState,
8
+ listNotebooklmHistoryViaRpc,
9
+ requireNotebooklmSession,
10
+ } from './utils.js';
11
+
12
+ cli({
13
+ site: NOTEBOOKLM_SITE,
14
+ name: 'history',
15
+ description: 'List NotebookLM conversation history threads in the current notebook',
16
+ domain: NOTEBOOKLM_DOMAIN,
17
+ strategy: Strategy.COOKIE,
18
+ browser: true,
19
+ navigateBefore: false,
20
+ args: [],
21
+ columns: ['thread_id', 'item_count', 'preview', 'source', 'notebook_id', 'url'],
22
+ func: async (page: IPage) => {
23
+ await ensureNotebooklmNotebookBinding(page);
24
+ await requireNotebooklmSession(page);
25
+ const state = await getNotebooklmPageState(page);
26
+ if (state.kind !== 'notebook') {
27
+ throw new EmptyResultError(
28
+ 'opencli notebooklm history',
29
+ 'Open a specific NotebookLM notebook tab first, then retry.',
30
+ );
31
+ }
32
+
33
+ const rows = await listNotebooklmHistoryViaRpc(page);
34
+ return rows;
35
+ },
36
+ });
@@ -0,0 +1,40 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { AuthRequiredError } from '../../errors.js';
3
+ import type { IPage } from '../../types.js';
4
+ import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
5
+ import {
6
+ ensureNotebooklmHome,
7
+ listNotebooklmLinks,
8
+ listNotebooklmViaRpc,
9
+ readCurrentNotebooklm,
10
+ requireNotebooklmSession,
11
+ } from './utils.js';
12
+
13
+ cli({
14
+ site: NOTEBOOKLM_SITE,
15
+ name: 'list',
16
+ description: 'List NotebookLM notebooks via in-page batchexecute RPC in the current logged-in session',
17
+ domain: NOTEBOOKLM_DOMAIN,
18
+ strategy: Strategy.COOKIE,
19
+ browser: true,
20
+ navigateBefore: false,
21
+ args: [],
22
+ columns: ['title', 'id', 'is_owner', 'created_at', 'source', 'url'],
23
+ func: async (page: IPage) => {
24
+ const currentFallback = await readCurrentNotebooklm(page).catch(() => null);
25
+ await ensureNotebooklmHome(page);
26
+ await requireNotebooklmSession(page);
27
+
28
+ try {
29
+ const rpcRows = await listNotebooklmViaRpc(page);
30
+ if (rpcRows.length > 0) return rpcRows;
31
+ } catch (error) {
32
+ if (error instanceof AuthRequiredError) throw error;
33
+ }
34
+
35
+ const domRows = await listNotebooklmLinks(page);
36
+ if (domRows.length > 0) return domRows;
37
+ if (currentFallback) return [currentFallback];
38
+ return [];
39
+ },
40
+ });