@jackwener/opencli 1.7.22 → 1.8.1
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.
- package/README.md +35 -194
- package/README.zh-CN.md +42 -260
- package/cli-manifest.json +8160 -4392
- package/clis/12306/me.js +73 -0
- package/clis/12306/orders.js +96 -0
- package/clis/12306/passengers.js +90 -0
- package/clis/12306/price.js +166 -0
- package/clis/12306/stations.js +66 -0
- package/clis/12306/train.js +91 -0
- package/clis/12306/trains.js +119 -0
- package/clis/12306/utils.js +272 -0
- package/clis/12306/utils.test.js +331 -0
- package/clis/36kr/article.js +6 -3
- package/clis/36kr/article.test.js +46 -0
- package/clis/_atlassian/shared.js +577 -0
- package/clis/_atlassian/shared.test.js +170 -0
- package/clis/apple-podcasts/commands.test.js +20 -0
- package/clis/apple-podcasts/search.js +2 -2
- package/clis/barchart/greeks.js +144 -56
- package/clis/barchart/greeks.test.js +138 -0
- package/clis/bilibili/comment.js +125 -0
- package/clis/bilibili/comment.test.js +153 -0
- package/clis/bilibili/comments.js +116 -21
- package/clis/bilibili/comments.test.js +77 -18
- package/clis/bilibili/subtitle.js +76 -31
- package/clis/bilibili/subtitle.test.js +156 -9
- package/clis/bilibili/summary.js +167 -0
- package/clis/bilibili/summary.test.js +210 -0
- package/clis/bilibili/utils.js +63 -5
- package/clis/bilibili/utils.test.js +45 -1
- package/clis/booking/booking.test.js +356 -0
- package/clis/booking/search.js +351 -0
- package/clis/chatgpt/envelope.test.js +108 -0
- package/clis/chatgpt/image.js +2 -2
- package/clis/chatgpt/image.test.js +6 -0
- package/clis/chatgpt/utils.js +148 -41
- package/clis/chatgpt/utils.test.js +92 -2
- package/clis/chess/analyze.js +35 -0
- package/clis/chess/analyze.test.js +79 -0
- package/clis/chess/game.js +114 -0
- package/clis/chess/game.test.js +178 -0
- package/clis/chess/games.js +67 -0
- package/clis/chess/games.test.js +164 -0
- package/clis/chess/stats.js +32 -0
- package/clis/chess/stats.test.js +79 -0
- package/clis/chess/utils.js +170 -0
- package/clis/chess/utils.test.js +230 -0
- package/clis/confluence/commands.test.js +195 -0
- package/clis/confluence/create.js +39 -0
- package/clis/confluence/page.js +23 -0
- package/clis/confluence/search.js +34 -0
- package/clis/confluence/shared.js +173 -0
- package/clis/confluence/update.js +38 -0
- package/clis/douyin/_shared/browser-fetch.js +44 -20
- package/clis/douyin/_shared/browser-fetch.test.js +22 -1
- package/clis/douyin/_shared/evaluate-result.js +16 -0
- package/clis/douyin/_shared/tos-upload.js +105 -69
- package/clis/douyin/_shared/vod-upload.js +212 -0
- package/clis/douyin/_shared/vod-upload.test.js +38 -0
- package/clis/douyin/delete.js +137 -4
- package/clis/douyin/delete.test.js +90 -1
- package/clis/douyin/hashtag.js +84 -23
- package/clis/douyin/hashtag.test.js +113 -0
- package/clis/douyin/publish-upload-id.test.js +170 -0
- package/clis/douyin/publish.js +88 -42
- package/clis/douyin/user-videos.js +9 -2
- package/clis/douyin/user-videos.test.js +43 -0
- package/clis/flomo/memos.js +228 -0
- package/clis/flomo/memos.test.js +144 -0
- package/clis/geogebra/add-circle.js +46 -0
- package/clis/geogebra/add-line.js +35 -0
- package/clis/geogebra/add-point.js +27 -0
- package/clis/geogebra/add-polygon.js +25 -0
- package/clis/geogebra/eval.js +35 -0
- package/clis/geogebra/geogebra.test.js +175 -0
- package/clis/geogebra/hexagon.js +62 -0
- package/clis/geogebra/info.js +72 -0
- package/clis/geogebra/list.js +35 -0
- package/clis/geogebra/triangle.js +60 -0
- package/clis/geogebra/utils.js +271 -0
- package/clis/gitee/search.js +2 -2
- package/clis/gitee/search.test.js +65 -0
- package/clis/jike/post.js +27 -17
- package/clis/jike/read.test.js +86 -0
- package/clis/jike/topic.js +32 -19
- package/clis/jike/user.js +33 -20
- package/clis/jira/attachments.js +28 -0
- package/clis/jira/commands.test.js +287 -0
- package/clis/jira/comments.js +28 -0
- package/clis/jira/issue.js +28 -0
- package/clis/jira/links.js +28 -0
- package/clis/jira/search.js +47 -0
- package/clis/jira/shared.js +256 -0
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/frontpage.test.js +37 -0
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/linkedin/connect.js +401 -0
- package/clis/linkedin/connect.test.js +213 -0
- package/clis/linkedin/inbox.js +234 -0
- package/clis/linkedin/inbox.test.js +152 -0
- package/clis/linkedin/job-detail.js +167 -0
- package/clis/linkedin/job-detail.test.js +38 -0
- package/clis/linkedin/jobs-preferences.js +113 -0
- package/clis/linkedin/jobs-preferences.test.js +43 -0
- package/clis/linkedin/people-search.js +262 -0
- package/clis/linkedin/people-search.test.js +216 -0
- package/clis/linkedin/post-analytics.js +74 -0
- package/clis/linkedin/post-analytics.test.js +40 -0
- package/clis/linkedin/posts-core.js +241 -0
- package/clis/linkedin/posts.js +22 -0
- package/clis/linkedin/posts.test.js +40 -0
- package/clis/linkedin/profile-analytics.js +104 -0
- package/clis/linkedin/profile-analytics.test.js +67 -0
- package/clis/linkedin/profile-experience.js +671 -0
- package/clis/linkedin/profile-experience.test.js +152 -0
- package/clis/linkedin/profile-projects.js +311 -0
- package/clis/linkedin/profile-projects.test.js +111 -0
- package/clis/linkedin/profile-read.js +148 -0
- package/clis/linkedin/profile-read.test.js +77 -0
- package/clis/linkedin/safe-send.js +357 -0
- package/clis/linkedin/safe-send.test.js +204 -0
- package/clis/linkedin/salesnav-inbox.js +210 -0
- package/clis/linkedin/salesnav-inbox.test.js +113 -0
- package/clis/linkedin/salesnav-message.js +360 -0
- package/clis/linkedin/salesnav-message.test.js +172 -0
- package/clis/linkedin/salesnav-search.js +186 -0
- package/clis/linkedin/salesnav-search.test.js +76 -0
- package/clis/linkedin/salesnav-thread.js +212 -0
- package/clis/linkedin/salesnav-thread.test.js +79 -0
- package/clis/linkedin/sent-invitations.js +92 -0
- package/clis/linkedin/sent-invitations.test.js +62 -0
- package/clis/linkedin/services-read.js +213 -0
- package/clis/linkedin/services-read.test.js +105 -0
- package/clis/linkedin/shared.js +124 -0
- package/clis/linkedin/thread-snapshot.js +214 -0
- package/clis/linkedin/thread-snapshot.test.js +89 -0
- package/clis/linkedin/timeline.js +14 -7
- package/clis/linkedin-learning/course.js +138 -0
- package/clis/linkedin-learning/course.test.js +114 -0
- package/clis/linkedin-learning/search.js +155 -0
- package/clis/linkedin-learning/search.test.js +144 -0
- package/clis/linkedin-learning/trending.js +133 -0
- package/clis/linkedin-learning/trending.test.js +123 -0
- package/clis/notebooklm/add-source.js +269 -0
- package/clis/notebooklm/add-source.test.js +97 -0
- package/clis/notebooklm/create.js +76 -0
- package/clis/notebooklm/create.test.js +58 -0
- package/clis/notebooklm/generate-audio.js +91 -0
- package/clis/notebooklm/generate-audio.test.js +63 -0
- package/clis/notebooklm/generate-slides.js +106 -0
- package/clis/notebooklm/generate-slides.test.js +75 -0
- package/clis/notebooklm/open.test.js +10 -10
- package/clis/notebooklm/rpc.js +20 -6
- package/clis/notebooklm/rpc.test.js +27 -1
- package/clis/notebooklm/utils.js +100 -24
- package/clis/notebooklm/utils.test.js +60 -1
- package/clis/notebooklm/write-note.js +103 -0
- package/clis/notebooklm/write-note.test.js +70 -0
- package/clis/pixiv/detail.js +41 -34
- package/clis/pixiv/detail.test.js +93 -0
- package/clis/pixiv/user.js +36 -31
- package/clis/pixiv/user.test.js +100 -0
- package/clis/pixiv/utils.js +56 -7
- package/clis/powerchina/search.js +3 -3
- package/clis/powerchina/search.test.js +27 -1
- package/clis/reddit/extract-media.test.js +149 -0
- package/clis/reddit/frontpage.js +47 -9
- package/clis/reddit/frontpage.test.js +34 -0
- package/clis/reddit/home.js +31 -1
- package/clis/reddit/home.test.js +46 -3
- package/clis/reddit/hot.js +32 -1
- package/clis/reddit/hot.test.js +15 -1
- package/clis/reddit/popular.js +39 -1
- package/clis/reddit/popular.test.js +26 -0
- package/clis/reddit/saved.js +1 -1
- package/clis/reddit/search.js +38 -1
- package/clis/reddit/search.test.js +26 -0
- package/clis/reddit/subreddit.js +52 -7
- package/clis/reddit/subreddit.test.js +31 -0
- package/clis/reddit/subscribed.js +165 -0
- package/clis/reddit/subscribed.test.js +168 -0
- package/clis/reddit/upvoted.js +1 -1
- package/clis/suno/commands.test.js +188 -0
- package/clis/suno/download.js +140 -0
- package/clis/suno/download.test.js +151 -0
- package/clis/suno/generate.js +231 -0
- package/clis/suno/generate.test.js +252 -0
- package/clis/suno/list.js +79 -0
- package/clis/suno/status.js +63 -0
- package/clis/suno/utils.js +549 -0
- package/clis/suno/utils.test.js +329 -0
- package/clis/twitter/device-follow.js +193 -0
- package/clis/twitter/device-follow.test.js +287 -0
- package/clis/twitter/download.js +443 -73
- package/clis/twitter/download.test.js +457 -0
- package/clis/twitter/followers.js +6 -2
- package/clis/twitter/followers.test.js +19 -1
- package/clis/twitter/following.js +14 -5
- package/clis/twitter/following.test.js +29 -0
- package/clis/twitter/likes.js +12 -4
- package/clis/twitter/likes.test.js +26 -1
- package/clis/twitter/list-add.js +1 -1
- package/clis/twitter/list-create.js +155 -0
- package/clis/twitter/list-create.test.js +169 -0
- package/clis/twitter/list-remove.js +13 -6
- package/clis/twitter/list-remove.test.js +74 -0
- package/clis/twitter/list-tweets.js +6 -2
- package/clis/twitter/list-tweets.test.js +41 -1
- package/clis/twitter/lists.js +31 -4
- package/clis/twitter/lists.test.js +152 -16
- package/clis/twitter/notifications.js +4 -4
- package/clis/twitter/post.js +62 -4
- package/clis/twitter/post.test.js +35 -3
- package/clis/twitter/profile.js +81 -28
- package/clis/twitter/profile.test.js +113 -2
- package/clis/twitter/quote.js +9 -4
- package/clis/twitter/reply.js +13 -10
- package/clis/twitter/reply.test.js +41 -0
- package/clis/twitter/search.js +7 -3
- package/clis/twitter/search.test.js +41 -0
- package/clis/twitter/shared.js +155 -0
- package/clis/twitter/shared.test.js +465 -1
- package/clis/twitter/thread.js +10 -2
- package/clis/twitter/thread.test.js +58 -0
- package/clis/twitter/timeline.js +6 -2
- package/clis/twitter/timeline.test.js +2 -0
- package/clis/twitter/tweets.js +3 -2
- package/clis/twitter/tweets.test.js +1 -1
- package/clis/twitter/utils.js +53 -16
- package/clis/upwork/detail.js +132 -0
- package/clis/upwork/feed.js +109 -0
- package/clis/upwork/search.js +115 -0
- package/clis/upwork/upwork.test.js +566 -0
- package/clis/upwork/utils.js +323 -0
- package/clis/weibo/delete.js +172 -0
- package/clis/weibo/delete.test.js +94 -0
- package/clis/weibo/publish.js +37 -14
- package/clis/weibo/publish.test.js +14 -5
- package/clis/weibo/user-posts.js +234 -0
- package/clis/weibo/user-posts.test.js +92 -0
- package/clis/weread/book-search.js +438 -0
- package/clis/weread/book-search.test.js +242 -0
- package/clis/weread/search-regression.test.js +98 -11
- package/clis/weread/search.js +32 -9
- package/clis/weread-official/book.js +135 -0
- package/clis/weread-official/commands.test.js +385 -0
- package/clis/weread-official/discover.js +107 -0
- package/clis/weread-official/list-apis.js +95 -0
- package/clis/weread-official/notes.js +171 -0
- package/clis/weread-official/readdata.js +158 -0
- package/clis/weread-official/review.js +93 -0
- package/clis/weread-official/search.js +106 -0
- package/clis/weread-official/shelf.js +97 -0
- package/clis/weread-official/utils.js +293 -0
- package/clis/weread-official/utils.test.js +242 -0
- package/clis/wikipedia/trending.js +7 -3
- package/clis/wikipedia/trending.test.js +57 -0
- package/clis/xianyu/chat.js +24 -109
- package/clis/xianyu/chat.test.js +5 -0
- package/clis/xianyu/im.js +322 -0
- package/clis/xianyu/im.test.js +253 -0
- package/clis/xianyu/inbox.js +96 -0
- package/clis/xianyu/messages.js +91 -0
- package/clis/xianyu/reply.js +82 -0
- package/clis/xiaohongshu/creator-note-detail.js +166 -28
- package/clis/xiaohongshu/creator-note-detail.test.js +196 -36
- package/clis/xiaohongshu/creator-notes-summary.js +2 -1
- package/clis/xiaohongshu/creator-notes-summary.test.js +7 -0
- package/clis/xiaohongshu/creator-notes.js +252 -2
- package/clis/xiaohongshu/creator-notes.test.js +90 -1
- package/clis/xiaohongshu/creator-stats.js +2 -1
- package/clis/xiaohongshu/creator-stats.test.js +24 -0
- package/clis/xiaohongshu/delete-note.js +260 -0
- package/clis/xiaohongshu/delete-note.test.js +172 -0
- package/clis/xiaohongshu/download.js +97 -39
- package/clis/xiaohongshu/download.test.js +201 -0
- package/clis/xiaohongshu/publish.js +48 -8
- package/clis/xiaohongshu/publish.test.js +65 -10
- package/clis/xiaohongshu/user-helpers.test.js +41 -0
- package/clis/xiaohongshu/user.js +27 -4
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/youdao/note.js +258 -0
- package/clis/youdao/note.test.js +99 -0
- package/clis/youtube/transcript.js +397 -24
- package/clis/youtube/transcript.test.js +196 -6
- package/clis/zhihu/answer-comments.js +280 -0
- package/clis/zhihu/answer-comments.test.js +287 -0
- package/clis/zhihu/answer-detail.js +2 -19
- package/clis/zhihu/answer-detail.test.js +8 -0
- package/clis/zhihu/collection.js +17 -16
- package/clis/zhihu/collection.test.js +50 -3
- package/clis/zhihu/download.js +1 -1
- package/clis/zhihu/question.js +42 -17
- package/clis/zhihu/question.test.js +113 -11
- package/clis/zhihu/search.js +195 -43
- package/clis/zhihu/search.test.js +198 -0
- package/clis/zhihu/text.js +29 -0
- package/clis/zhihu/text.test.js +24 -0
- package/dist/src/browser/errors.js +4 -2
- package/dist/src/browser/errors.test.js +6 -0
- package/dist/src/browser/network-cache.js +13 -1
- package/dist/src/browser/network-cache.test.js +17 -0
- package/dist/src/browser/page.js +30 -4
- package/dist/src/browser/page.test.js +42 -0
- package/dist/src/browser/utils.d.ts +1 -1
- package/dist/src/cli-argv-preprocess.d.ts +26 -0
- package/dist/src/cli-argv-preprocess.js +138 -0
- package/dist/src/cli-argv-preprocess.test.js +79 -0
- package/dist/src/convention-audit.js +15 -8
- package/dist/src/convention-audit.test.js +21 -0
- package/dist/src/download/index.js +13 -1
- package/dist/src/download/index.test.js +23 -1
- package/dist/src/download/media-download.js +15 -2
- package/dist/src/download/media-download.test.d.ts +1 -0
- package/dist/src/download/media-download.test.js +112 -0
- package/dist/src/download/progress.js +2 -2
- package/dist/src/download/progress.test.js +12 -1
- package/dist/src/electron-apps.js +1 -1
- package/dist/src/electron-apps.test.js +7 -2
- package/dist/src/errors.d.ts +17 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/external-clis.yaml +8 -0
- package/dist/src/main.js +14 -2
- package/dist/src/output.js +11 -1
- package/dist/src/output.test.js +6 -0
- package/dist/src/registry.js +1 -0
- package/dist/src/registry.test.js +11 -0
- package/dist/src/utils.d.ts +43 -0
- package/dist/src/utils.js +97 -0
- package/dist/src/utils.test.d.ts +1 -0
- package/dist/src/utils.test.js +155 -0
- package/package.json +8 -2
- package/scripts/silent-column-drop-baseline.json +0 -52
- package/scripts/typed-error-lint-baseline.json +28 -380
- package/clis/slock/_utils.js +0 -12
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
+
import { callNotebooklmRpc } from './rpc.js';
|
|
5
|
+
import { buildNotebooklmNotebookUrl, listNotebooklmSourcesViaRpc, parseNotebooklmNotebookTarget, requireNotebooklmExecute, requireNotebooklmSession } from './utils.js';
|
|
6
|
+
|
|
7
|
+
const NOTEBOOKLM_CREATE_ARTIFACT_RPC_ID = 'R7cb6c';
|
|
8
|
+
const SLIDE_DECK_CONFIG_BLOCK = [2, null, null, [1, null, null, null, null, null, null, null, null, null, [1]], [[1, 4, 2, 3, 6]]];
|
|
9
|
+
const ARTIFACT_UUID_RE = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
10
|
+
|
|
11
|
+
function toExcludedUuidSet(excludedIds) {
|
|
12
|
+
return new Set(excludedIds.map((id) => String(id ?? '').toLowerCase()).filter(Boolean));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildCreateSlidesArgs(projectId, sourceIds, options = {}) {
|
|
16
|
+
const sourceTuples = sourceIds.map((id) => [[id]]);
|
|
17
|
+
const language = options.language || 'en';
|
|
18
|
+
const length = Number.isInteger(options.length) ? options.length : 3;
|
|
19
|
+
return [
|
|
20
|
+
SLIDE_DECK_CONFIG_BLOCK,
|
|
21
|
+
projectId,
|
|
22
|
+
[
|
|
23
|
+
null,
|
|
24
|
+
null,
|
|
25
|
+
8,
|
|
26
|
+
sourceTuples,
|
|
27
|
+
null,
|
|
28
|
+
null,
|
|
29
|
+
null, null, null, null, null, null, null, null, null, null,
|
|
30
|
+
[[null, language, 1, length]],
|
|
31
|
+
],
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseSlidesIdFromResult(result, excludedIds = []) {
|
|
36
|
+
const excluded = toExcludedUuidSet(excludedIds);
|
|
37
|
+
if (typeof result === 'string' && ARTIFACT_UUID_RE.test(result) && !excluded.has(result.toLowerCase())) return result;
|
|
38
|
+
const stack = [result];
|
|
39
|
+
while (stack.length) {
|
|
40
|
+
const node = stack.shift();
|
|
41
|
+
if (typeof node === 'string' && ARTIFACT_UUID_RE.test(node) && !excluded.has(node.toLowerCase())) return node;
|
|
42
|
+
if (Array.isArray(node)) for (const child of node) stack.push(child);
|
|
43
|
+
else if (node && typeof node === 'object') for (const v of Object.values(node)) stack.push(v);
|
|
44
|
+
}
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseSlideDeckLength(value) {
|
|
49
|
+
if (value === undefined || value === '') return 3;
|
|
50
|
+
const length = Number(value);
|
|
51
|
+
if (!Number.isInteger(length) || length <= 0) {
|
|
52
|
+
throw new ArgumentError('--length must be a positive integer');
|
|
53
|
+
}
|
|
54
|
+
return length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
cli({
|
|
58
|
+
site: NOTEBOOKLM_SITE,
|
|
59
|
+
name: 'generate-slides',
|
|
60
|
+
access: 'write',
|
|
61
|
+
description: 'Trigger a Slide Deck (AI presentation) generation for a NotebookLM notebook, using all of its sources',
|
|
62
|
+
domain: NOTEBOOKLM_DOMAIN,
|
|
63
|
+
strategy: Strategy.COOKIE,
|
|
64
|
+
browser: true,
|
|
65
|
+
navigateBefore: false,
|
|
66
|
+
args: [
|
|
67
|
+
{ name: 'notebook', positional: true, required: true, help: 'Notebook id from `notebooklm list` or full notebook URL' },
|
|
68
|
+
{ name: 'length', help: 'Slide deck length: 1=Short, 3=Default (default 3)' },
|
|
69
|
+
{ name: 'language', help: 'Language code (default en)' },
|
|
70
|
+
{ name: 'execute', type: 'boolean', help: 'Actually trigger remote NotebookLM slide deck generation' },
|
|
71
|
+
],
|
|
72
|
+
columns: ['notebook_id', 'slides_id', 'source_count', 'status', 'notebook_url'],
|
|
73
|
+
func: async (page, kwargs) => {
|
|
74
|
+
const notebookId = parseNotebooklmNotebookTarget(String(kwargs.notebook ?? ''));
|
|
75
|
+
const length = parseSlideDeckLength(kwargs.length);
|
|
76
|
+
const language = String(kwargs.language ?? 'en').trim() || 'en';
|
|
77
|
+
requireNotebooklmExecute(kwargs.execute, 'generate NotebookLM slides');
|
|
78
|
+
try {
|
|
79
|
+
await page.goto(buildNotebooklmNotebookUrl(notebookId));
|
|
80
|
+
await page.wait(2);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
throw new CommandExecutionError(`Failed to open NotebookLM notebook ${notebookId}: ${error?.message || error}`);
|
|
84
|
+
}
|
|
85
|
+
await requireNotebooklmSession(page);
|
|
86
|
+
const sources = await listNotebooklmSourcesViaRpc(page);
|
|
87
|
+
const sourceIds = sources.map((s) => s.id).filter((id) => typeof id === 'string' && id);
|
|
88
|
+
if (sourceIds.length === 0) {
|
|
89
|
+
throw new EmptyResultError('notebooklm generate-slides', 'The notebook has no sources; add a source before generating a slide deck.');
|
|
90
|
+
}
|
|
91
|
+
const rpc = await callNotebooklmRpc(page, NOTEBOOKLM_CREATE_ARTIFACT_RPC_ID, buildCreateSlidesArgs(notebookId, sourceIds, { length, language }));
|
|
92
|
+
const slidesId = parseSlidesIdFromResult(rpc.result, [notebookId, ...sourceIds]);
|
|
93
|
+
if (!slidesId) {
|
|
94
|
+
throw new CommandExecutionError('NotebookLM CreateArtifact (slides) RPC returned no slide-deck id; the server may have rejected the request.');
|
|
95
|
+
}
|
|
96
|
+
return [{
|
|
97
|
+
notebook_id: notebookId,
|
|
98
|
+
slides_id: slidesId,
|
|
99
|
+
source_count: sourceIds.length,
|
|
100
|
+
status: 'pending',
|
|
101
|
+
notebook_url: buildNotebooklmNotebookUrl(notebookId),
|
|
102
|
+
}];
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export const __test__ = { SLIDE_DECK_CONFIG_BLOCK, buildCreateSlidesArgs, parseSlideDeckLength, parseSlidesIdFromResult };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ArgumentError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
|
+
import { __test__ } from './generate-slides.js';
|
|
5
|
+
|
|
6
|
+
const { SLIDE_DECK_CONFIG_BLOCK, buildCreateSlidesArgs, parseSlideDeckLength, parseSlidesIdFromResult } = __test__;
|
|
7
|
+
|
|
8
|
+
describe('notebooklm generate-slides', () => {
|
|
9
|
+
it('SLIDE_DECK_CONFIG_BLOCK reuses the same R7cb6c config envelope as audio', () => {
|
|
10
|
+
expect(SLIDE_DECK_CONFIG_BLOCK).toEqual([
|
|
11
|
+
2, null, null,
|
|
12
|
+
[1, null, null, null, null, null, null, null, null, null, [1]],
|
|
13
|
+
[[1, 4, 2, 3, 6]],
|
|
14
|
+
]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('buildCreateSlidesArgs matches the live-captured slide-deck wire format', () => {
|
|
18
|
+
const projectId = '17e2b882-6a01-4c6c-9262-0738dfa2abee';
|
|
19
|
+
const sourceA = '493b9ddd-453b-4523-b638-bb560b37723c';
|
|
20
|
+
const sourceB = '182508cf-9afd-4db5-8640-2f8cafcc7111';
|
|
21
|
+
expect(buildCreateSlidesArgs(projectId, [sourceA, sourceB])).toEqual([
|
|
22
|
+
[2, null, null, [1, null, null, null, null, null, null, null, null, null, [1]], [[1, 4, 2, 3, 6]]],
|
|
23
|
+
projectId,
|
|
24
|
+
[
|
|
25
|
+
null, null, 8,
|
|
26
|
+
[[[sourceA]], [[sourceB]]],
|
|
27
|
+
null, null,
|
|
28
|
+
null, null, null, null, null, null, null, null, null, null,
|
|
29
|
+
[[null, 'en', 1, 3]],
|
|
30
|
+
],
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('buildCreateSlidesArgs honours --language and --length overrides', () => {
|
|
35
|
+
const args = buildCreateSlidesArgs('pid', ['s1'], { language: 'zh', length: 1 });
|
|
36
|
+
expect(args[2][16]).toEqual([[null, 'zh', 1, 1]]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('parseSlideDeckLength defaults empty values and rejects invalid input', () => {
|
|
40
|
+
expect(parseSlideDeckLength(undefined)).toBe(3);
|
|
41
|
+
expect(parseSlideDeckLength('')).toBe(3);
|
|
42
|
+
expect(parseSlideDeckLength('1')).toBe(1);
|
|
43
|
+
expect(() => parseSlideDeckLength('many')).toThrow(ArgumentError);
|
|
44
|
+
expect(() => parseSlideDeckLength(0)).toThrow(ArgumentError);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('parseSlidesIdFromResult finds a UUID-shaped slides id anywhere in the tree', () => {
|
|
48
|
+
const id = '1f8ada7d-cb33-49a4-8498-c5b81c1a899d';
|
|
49
|
+
expect(parseSlidesIdFromResult([[id, 'opencli-slides-test']])).toBe(id);
|
|
50
|
+
expect(parseSlidesIdFromResult({ artifactId: id })).toBe(id);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('parseSlidesIdFromResult ignores non-UUID strings and empty inputs', () => {
|
|
54
|
+
expect(parseSlidesIdFromResult([[null, 'opencli-slides-test']])).toBe('');
|
|
55
|
+
expect(parseSlidesIdFromResult({})).toBe('');
|
|
56
|
+
expect(parseSlidesIdFromResult([])).toBe('');
|
|
57
|
+
expect(parseSlidesIdFromResult(null)).toBe('');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('parseSlidesIdFromResult skips notebook/source ids before selecting the generated deck id', () => {
|
|
61
|
+
const notebookId = '17e2b882-6a01-4c6c-9262-0738dfa2abee';
|
|
62
|
+
const sourceId = '493b9ddd-453b-4523-b638-bb560b37723c';
|
|
63
|
+
const slidesId = '1f8ada7d-cb33-49a4-8498-c5b81c1a899d';
|
|
64
|
+
expect(parseSlidesIdFromResult([notebookId, [[sourceId]], [slidesId]], [notebookId, sourceId])).toBe(slidesId);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('refuses to trigger remote slide generation without --execute', async () => {
|
|
68
|
+
const command = getRegistry().get('notebooklm/generate-slides');
|
|
69
|
+
const page = { goto: vi.fn() };
|
|
70
|
+
await expect(command.func(page, {
|
|
71
|
+
notebook: '17e2b882-6a01-4c6c-9262-0738dfa2abee',
|
|
72
|
+
})).rejects.toThrow(ArgumentError);
|
|
73
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -23,18 +23,18 @@ describe('notebooklm open', () => {
|
|
|
23
23
|
mockRequireNotebooklmSession.mockReset();
|
|
24
24
|
mockRequireNotebooklmSession.mockResolvedValue(undefined);
|
|
25
25
|
mockGetNotebooklmPageState.mockResolvedValue({
|
|
26
|
-
url: 'https://notebooklm.google.com/notebook/
|
|
26
|
+
url: 'https://notebooklm.google.com/notebook/17e2b882-1234-1234-1234-abcdef012345',
|
|
27
27
|
title: 'Browser Automation',
|
|
28
28
|
hostname: 'notebooklm.google.com',
|
|
29
29
|
kind: 'notebook',
|
|
30
|
-
notebookId: '
|
|
30
|
+
notebookId: '17e2b882-1234-1234-1234-abcdef012345',
|
|
31
31
|
loginRequired: false,
|
|
32
32
|
notebookCount: 1,
|
|
33
33
|
});
|
|
34
34
|
mockReadCurrentNotebooklm.mockResolvedValue({
|
|
35
|
-
id: '
|
|
35
|
+
id: '17e2b882-1234-1234-1234-abcdef012345',
|
|
36
36
|
title: 'Browser Automation',
|
|
37
|
-
url: 'https://notebooklm.google.com/notebook/
|
|
37
|
+
url: 'https://notebooklm.google.com/notebook/17e2b882-1234-1234-1234-abcdef012345',
|
|
38
38
|
source: 'current-page',
|
|
39
39
|
});
|
|
40
40
|
});
|
|
@@ -43,12 +43,12 @@ describe('notebooklm open', () => {
|
|
|
43
43
|
goto: vi.fn(async () => { }),
|
|
44
44
|
wait: vi.fn(async () => { }),
|
|
45
45
|
};
|
|
46
|
-
const result = await command.func(page, { notebook: '
|
|
47
|
-
expect(page.goto).toHaveBeenCalledWith('https://notebooklm.google.com/notebook/
|
|
46
|
+
const result = await command.func(page, { notebook: '17e2b882-1234-1234-1234-abcdef012345' });
|
|
47
|
+
expect(page.goto).toHaveBeenCalledWith('https://notebooklm.google.com/notebook/17e2b882-1234-1234-1234-abcdef012345');
|
|
48
48
|
expect(result).toEqual([{
|
|
49
|
-
id: '
|
|
49
|
+
id: '17e2b882-1234-1234-1234-abcdef012345',
|
|
50
50
|
title: 'Browser Automation',
|
|
51
|
-
url: 'https://notebooklm.google.com/notebook/
|
|
51
|
+
url: 'https://notebooklm.google.com/notebook/17e2b882-1234-1234-1234-abcdef012345',
|
|
52
52
|
source: 'current-page',
|
|
53
53
|
}]);
|
|
54
54
|
});
|
|
@@ -57,7 +57,7 @@ describe('notebooklm open', () => {
|
|
|
57
57
|
goto: vi.fn(async () => { }),
|
|
58
58
|
wait: vi.fn(async () => { }),
|
|
59
59
|
};
|
|
60
|
-
await command.func(page, { notebook: 'https://notebooklm.google.com/notebook/
|
|
61
|
-
expect(page.goto).toHaveBeenCalledWith('https://notebooklm.google.com/notebook/
|
|
60
|
+
await command.func(page, { notebook: 'https://notebooklm.google.com/notebook/17e2b882-1234-1234-1234-abcdef012345?pli=1' });
|
|
61
|
+
expect(page.goto).toHaveBeenCalledWith('https://notebooklm.google.com/notebook/17e2b882-1234-1234-1234-abcdef012345');
|
|
62
62
|
});
|
|
63
63
|
});
|
package/clis/notebooklm/rpc.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { AuthRequiredError, CliError } from '@jackwener/opencli/errors';
|
|
2
2
|
import { NOTEBOOKLM_DOMAIN } from './shared.js';
|
|
3
|
+
|
|
4
|
+
export function unwrapNotebooklmEvaluateResult(payload) {
|
|
5
|
+
if (payload && typeof payload === 'object' && !Array.isArray(payload) && 'session' in payload && 'data' in payload) {
|
|
6
|
+
return payload.data;
|
|
7
|
+
}
|
|
8
|
+
return payload;
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
export function extractNotebooklmPageAuthFromHtml(html, sourcePath = '/', preferredTokens) {
|
|
4
12
|
const csrfMatch = html.match(/"SNlM0e":"([^"]+)"/);
|
|
5
13
|
const sessionMatch = html.match(/"FdrFJe":"([^"]+)"/);
|
|
@@ -8,26 +16,30 @@ export function extractNotebooklmPageAuthFromHtml(html, sourcePath = '/', prefer
|
|
|
8
16
|
if (!csrfToken || !sessionId) {
|
|
9
17
|
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
18
|
}
|
|
11
|
-
return { csrfToken, sessionId, sourcePath: sourcePath || '/' };
|
|
19
|
+
return { csrfToken, sessionId, sourcePath: sourcePath || '/', authuser: preferredTokens?.authuser ?? '' };
|
|
12
20
|
}
|
|
13
21
|
async function probeNotebooklmPageAuth(page) {
|
|
14
|
-
const raw = await page.evaluate(`(() => {
|
|
22
|
+
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
|
|
15
23
|
const wiz = window.WIZ_global_data || {};
|
|
16
24
|
const html = document.documentElement.innerHTML;
|
|
25
|
+
const authMatch = (location.search || '').match(/[?&]authuser=(\\d+)/);
|
|
26
|
+
const pathMatch = (location.pathname || '').match(/^\\/u\\/(\\d+)\\//);
|
|
17
27
|
return {
|
|
18
28
|
html,
|
|
19
29
|
sourcePath: location.pathname || '/',
|
|
20
30
|
readyState: document.readyState || '',
|
|
21
31
|
csrfToken: typeof wiz.SNlM0e === 'string' ? wiz.SNlM0e : '',
|
|
22
32
|
sessionId: typeof wiz.FdrFJe === 'string' ? wiz.FdrFJe : '',
|
|
33
|
+
authuser: authMatch ? authMatch[1] : (pathMatch ? pathMatch[1] : ''),
|
|
23
34
|
};
|
|
24
|
-
})()`);
|
|
35
|
+
})()`));
|
|
25
36
|
return {
|
|
26
37
|
html: String(raw?.html ?? ''),
|
|
27
38
|
sourcePath: String(raw?.sourcePath ?? '/'),
|
|
28
39
|
readyState: String(raw?.readyState ?? ''),
|
|
29
40
|
csrfToken: String(raw?.csrfToken ?? ''),
|
|
30
41
|
sessionId: String(raw?.sessionId ?? ''),
|
|
42
|
+
authuser: String(raw?.authuser ?? ''),
|
|
31
43
|
};
|
|
32
44
|
}
|
|
33
45
|
export async function getNotebooklmPageAuth(page) {
|
|
@@ -35,7 +47,7 @@ export async function getNotebooklmPageAuth(page) {
|
|
|
35
47
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
36
48
|
const probe = await probeNotebooklmPageAuth(page);
|
|
37
49
|
try {
|
|
38
|
-
return extractNotebooklmPageAuthFromHtml(probe.html, probe.sourcePath, { csrfToken: probe.csrfToken, sessionId: probe.sessionId });
|
|
50
|
+
return extractNotebooklmPageAuthFromHtml(probe.html, probe.sourcePath, { csrfToken: probe.csrfToken, sessionId: probe.sessionId, authuser: probe.authuser });
|
|
39
51
|
}
|
|
40
52
|
catch (error) {
|
|
41
53
|
lastError = error;
|
|
@@ -130,7 +142,7 @@ export async function fetchNotebooklmInPage(page, url, options = {}) {
|
|
|
130
142
|
const method = options.method ?? 'GET';
|
|
131
143
|
const headers = options.headers ?? {};
|
|
132
144
|
const body = options.body ?? '';
|
|
133
|
-
const raw = await page.evaluate(`(async () => {
|
|
145
|
+
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(async () => {
|
|
134
146
|
const request = {
|
|
135
147
|
url: ${JSON.stringify(url)},
|
|
136
148
|
method: ${JSON.stringify(method)},
|
|
@@ -151,7 +163,7 @@ export async function fetchNotebooklmInPage(page, url, options = {}) {
|
|
|
151
163
|
body: await response.text(),
|
|
152
164
|
finalUrl: response.url,
|
|
153
165
|
};
|
|
154
|
-
})()`);
|
|
166
|
+
})()`));
|
|
155
167
|
return {
|
|
156
168
|
ok: Boolean(raw?.ok),
|
|
157
169
|
status: Number(raw?.status ?? 0),
|
|
@@ -162,8 +174,10 @@ export async function fetchNotebooklmInPage(page, url, options = {}) {
|
|
|
162
174
|
export async function callNotebooklmRpc(page, rpcId, params, options = {}) {
|
|
163
175
|
const auth = await getNotebooklmPageAuth(page);
|
|
164
176
|
const requestBody = buildNotebooklmRpcBody(rpcId, params, auth.csrfToken);
|
|
177
|
+
const authuser = auth.authuser || '';
|
|
165
178
|
const url = `https://${NOTEBOOKLM_DOMAIN}/_/LabsTailwindUi/data/batchexecute` +
|
|
166
179
|
`?rpcids=${rpcId}&source-path=${encodeURIComponent(auth.sourcePath)}` +
|
|
180
|
+
(authuser ? `&authuser=${encodeURIComponent(authuser)}` : '') +
|
|
167
181
|
`&hl=${encodeURIComponent(options.hl ?? 'en')}` +
|
|
168
182
|
`&f.sid=${encodeURIComponent(auth.sessionId)}&rt=c`;
|
|
169
183
|
const response = await fetchNotebooklmInPage(page, url, {
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { AuthRequiredError } from '@jackwener/opencli/errors';
|
|
3
|
-
import { buildNotebooklmRpcBody, extractNotebooklmRpcResult, getNotebooklmPageAuth, parseNotebooklmChunkedResponse, } from './rpc.js';
|
|
3
|
+
import { buildNotebooklmRpcBody, extractNotebooklmRpcResult, getNotebooklmPageAuth, parseNotebooklmChunkedResponse, unwrapNotebooklmEvaluateResult, } from './rpc.js';
|
|
4
4
|
describe('notebooklm rpc transport', () => {
|
|
5
|
+
it('unwraps Browser Bridge evaluate envelopes', () => {
|
|
6
|
+
const data = { ok: true };
|
|
7
|
+
expect(unwrapNotebooklmEvaluateResult({ session: 'site:notebooklm:abc', data })).toBe(data);
|
|
8
|
+
expect(unwrapNotebooklmEvaluateResult(data)).toBe(data);
|
|
9
|
+
});
|
|
10
|
+
|
|
5
11
|
it('extracts auth tokens from the page html via page evaluation', async () => {
|
|
6
12
|
const page = {
|
|
7
13
|
evaluate: vi.fn(async (script) => {
|
|
@@ -16,9 +22,27 @@ describe('notebooklm rpc transport', () => {
|
|
|
16
22
|
csrfToken: 'csrf-123',
|
|
17
23
|
sessionId: 'sess-456',
|
|
18
24
|
sourcePath: '/',
|
|
25
|
+
authuser: '',
|
|
19
26
|
});
|
|
20
27
|
expect(page.evaluate).toHaveBeenCalledTimes(1);
|
|
21
28
|
});
|
|
29
|
+
it('extracts auth tokens when page evaluation is wrapped in a Browser Bridge envelope', async () => {
|
|
30
|
+
const page = {
|
|
31
|
+
evaluate: vi.fn(async () => ({
|
|
32
|
+
session: 'site:notebooklm:abc',
|
|
33
|
+
data: {
|
|
34
|
+
html: '<html>"SNlM0e":"csrf-123","FdrFJe":"sess-456"</html>',
|
|
35
|
+
sourcePath: '/notebook/nb-demo',
|
|
36
|
+
},
|
|
37
|
+
})),
|
|
38
|
+
};
|
|
39
|
+
await expect(getNotebooklmPageAuth(page)).resolves.toEqual({
|
|
40
|
+
csrfToken: 'csrf-123',
|
|
41
|
+
sessionId: 'sess-456',
|
|
42
|
+
sourcePath: '/notebook/nb-demo',
|
|
43
|
+
authuser: '',
|
|
44
|
+
});
|
|
45
|
+
});
|
|
22
46
|
it('falls back to WIZ_global_data tokens when html regex data is missing', async () => {
|
|
23
47
|
const page = {
|
|
24
48
|
evaluate: vi.fn(async () => ({
|
|
@@ -33,6 +57,7 @@ describe('notebooklm rpc transport', () => {
|
|
|
33
57
|
csrfToken: 'csrf-wiz',
|
|
34
58
|
sessionId: 'sess-wiz',
|
|
35
59
|
sourcePath: '/notebook/nb-demo',
|
|
60
|
+
authuser: '',
|
|
36
61
|
});
|
|
37
62
|
});
|
|
38
63
|
it('retries token extraction once when the first probe returns no tokens', async () => {
|
|
@@ -58,6 +83,7 @@ describe('notebooklm rpc transport', () => {
|
|
|
58
83
|
csrfToken: 'csrf-123',
|
|
59
84
|
sessionId: 'sess-456',
|
|
60
85
|
sourcePath: '/notebook/nb-demo',
|
|
86
|
+
authuser: '',
|
|
61
87
|
});
|
|
62
88
|
expect(page.evaluate).toHaveBeenCalledTimes(2);
|
|
63
89
|
});
|
package/clis/notebooklm/utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AuthRequiredError, CliError } from '@jackwener/opencli/errors';
|
|
1
|
+
import { ArgumentError, AuthRequiredError, CliError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
2
2
|
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_HOME_URL, } from './shared.js';
|
|
3
|
-
import { callNotebooklmRpc, getNotebooklmPageAuth, } from './rpc.js';
|
|
3
|
+
import { callNotebooklmRpc, getNotebooklmPageAuth, unwrapNotebooklmEvaluateResult, } from './rpc.js';
|
|
4
4
|
export { buildNotebooklmRpcBody, extractNotebooklmRpcResult, fetchNotebooklmInPage, getNotebooklmPageAuth, parseNotebooklmChunkedResponse, stripNotebooklmAntiXssi, } from './rpc.js';
|
|
5
5
|
const NOTEBOOKLM_LIST_RPC_ID = 'wXbhsf';
|
|
6
6
|
const NOTEBOOKLM_NOTEBOOK_DETAIL_RPC_ID = 'rLM1Ne';
|
|
@@ -13,28 +13,61 @@ function unwrapNotebooklmSingletonResult(result) {
|
|
|
13
13
|
}
|
|
14
14
|
return current;
|
|
15
15
|
}
|
|
16
|
+
export function isPlainObject(value) {
|
|
17
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
18
|
+
}
|
|
16
19
|
export function parseNotebooklmIdFromUrl(url) {
|
|
17
20
|
const match = url.match(/\/notebook\/([^/?#]+)/);
|
|
18
21
|
return match?.[1] ?? '';
|
|
19
22
|
}
|
|
23
|
+
const NOTEBOOK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24
|
+
function ensureNotebookUuid(candidate) {
|
|
25
|
+
if (!NOTEBOOK_UUID_RE.test(candidate)) {
|
|
26
|
+
throw new CliError('NOTEBOOKLM_INVALID_NOTEBOOK', `NotebookLM notebook id "${candidate}" is not a valid UUID`, 'Pass a notebook id from `opencli notebooklm list` or a full notebook URL like https://notebooklm.google.com/notebook/<uuid>.');
|
|
27
|
+
}
|
|
28
|
+
return candidate;
|
|
29
|
+
}
|
|
20
30
|
export function parseNotebooklmNotebookTarget(value) {
|
|
21
31
|
const normalized = value.trim();
|
|
22
32
|
if (!normalized) {
|
|
23
33
|
throw new CliError('NOTEBOOKLM_INVALID_NOTEBOOK', 'NotebookLM notebook id is required', 'Pass a notebook id from `opencli notebooklm list` or a full notebook URL.');
|
|
24
34
|
}
|
|
25
35
|
if (/^https?:\/\//i.test(normalized)) {
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = new URL(normalized);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new CliError('NOTEBOOKLM_INVALID_NOTEBOOK', 'NotebookLM notebook URL is invalid', 'Pass a full NotebookLM notebook URL like https://notebooklm.google.com/notebook/<uuid>.');
|
|
42
|
+
}
|
|
43
|
+
if (parsed.protocol !== 'https:' || parsed.hostname !== NOTEBOOKLM_DOMAIN || parsed.username || parsed.password || parsed.port) {
|
|
44
|
+
throw new CliError('NOTEBOOKLM_INVALID_NOTEBOOK', 'NotebookLM notebook URL must be a canonical https://notebooklm.google.com URL', 'Pass a notebook id from `opencli notebooklm list` or a full NotebookLM notebook URL.');
|
|
45
|
+
}
|
|
26
46
|
const notebookId = parseNotebooklmIdFromUrl(normalized);
|
|
27
|
-
if (notebookId)
|
|
28
|
-
|
|
29
|
-
|
|
47
|
+
if (!notebookId) {
|
|
48
|
+
throw new CliError('NOTEBOOKLM_INVALID_NOTEBOOK', 'NotebookLM notebook URL is invalid', 'Pass a full NotebookLM notebook URL like https://notebooklm.google.com/notebook/<uuid>.');
|
|
49
|
+
}
|
|
50
|
+
return ensureNotebookUuid(notebookId);
|
|
30
51
|
}
|
|
31
52
|
const pathMatch = normalized.match(/(?:^|\/)notebook\/([^/?#]+)/);
|
|
32
53
|
if (pathMatch?.[1])
|
|
33
|
-
return pathMatch[1];
|
|
34
|
-
return normalized;
|
|
54
|
+
return ensureNotebookUuid(pathMatch[1]);
|
|
55
|
+
return ensureNotebookUuid(normalized);
|
|
56
|
+
}
|
|
57
|
+
export function getNotebooklmAuthuser() {
|
|
58
|
+
const v = process.env.OPENCLI_NOTEBOOKLM_AUTHUSER;
|
|
59
|
+
return typeof v === 'string' && /^\d+$/.test(v) ? v : '';
|
|
60
|
+
}
|
|
61
|
+
export function requireNotebooklmExecute(value, action) {
|
|
62
|
+
if (value !== true) {
|
|
63
|
+
throw new ArgumentError(`Refusing to ${action}: pass --execute to perform this NotebookLM write`);
|
|
64
|
+
}
|
|
35
65
|
}
|
|
36
66
|
export function buildNotebooklmNotebookUrl(notebookId) {
|
|
37
|
-
|
|
67
|
+
const u = new URL(`/notebook/${encodeURIComponent(notebookId)}`, NOTEBOOKLM_HOME_URL);
|
|
68
|
+
const authuser = getNotebooklmAuthuser();
|
|
69
|
+
if (authuser) u.searchParams.set('authuser', authuser);
|
|
70
|
+
return u.toString();
|
|
38
71
|
}
|
|
39
72
|
export function classifyNotebooklmPage(url) {
|
|
40
73
|
try {
|
|
@@ -401,6 +434,42 @@ export async function getNotebooklmDetailViaRpc(page) {
|
|
|
401
434
|
const rpc = await callNotebooklmRpc(page, NOTEBOOKLM_NOTEBOOK_DETAIL_RPC_ID, [state.notebookId, null, [2], null, 0]);
|
|
402
435
|
return parseNotebooklmNotebookDetailResult(rpc.result);
|
|
403
436
|
}
|
|
437
|
+
export async function getNotebooklmNotebookDetailById(page, notebookId) {
|
|
438
|
+
const rpc = await callNotebooklmRpc(page, NOTEBOOKLM_NOTEBOOK_DETAIL_RPC_ID, [notebookId, null, [2], null, 0]);
|
|
439
|
+
return { detail: parseNotebooklmNotebookDetailResult(rpc.result), sources: parseNotebooklmSourceListResult(rpc.result) };
|
|
440
|
+
}
|
|
441
|
+
export async function verifyNotebooklmNotebookExists(page, notebookId, action) {
|
|
442
|
+
try {
|
|
443
|
+
const { detail } = await getNotebooklmNotebookDetailById(page, notebookId);
|
|
444
|
+
if (!detail || detail.id !== notebookId) {
|
|
445
|
+
throw new CommandExecutionError(`NotebookLM ${action} succeeded but the notebook ${notebookId} was not found in the post-write verification`);
|
|
446
|
+
}
|
|
447
|
+
return detail;
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
if (error instanceof AuthRequiredError || error instanceof CommandExecutionError)
|
|
451
|
+
throw error;
|
|
452
|
+
throw new CommandExecutionError(`NotebookLM ${action} post-write verification failed: ${error?.message || error}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
export async function verifyNotebooklmSourceAdded(page, notebookId, sourceId, action) {
|
|
456
|
+
try {
|
|
457
|
+
const { detail, sources } = await getNotebooklmNotebookDetailById(page, notebookId);
|
|
458
|
+
if (!detail || detail.id !== notebookId) {
|
|
459
|
+
throw new CommandExecutionError(`NotebookLM ${action} succeeded but the notebook ${notebookId} was not found in the post-write verification`);
|
|
460
|
+
}
|
|
461
|
+
const matched = sources.find((s) => s.id === sourceId);
|
|
462
|
+
if (!matched) {
|
|
463
|
+
throw new CommandExecutionError(`NotebookLM ${action} succeeded but source ${sourceId} did not appear in the notebook's source list`);
|
|
464
|
+
}
|
|
465
|
+
return matched;
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
if (error instanceof AuthRequiredError || error instanceof CommandExecutionError)
|
|
469
|
+
throw error;
|
|
470
|
+
throw new CommandExecutionError(`NotebookLM ${action} post-write verification failed: ${error?.message || error}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
404
473
|
export async function listNotebooklmSourcesViaRpc(page) {
|
|
405
474
|
const state = await getNotebooklmPageState(page);
|
|
406
475
|
if (state.kind !== 'notebook' || !state.notebookId)
|
|
@@ -434,7 +503,7 @@ export async function listNotebooklmNotesFromPage(page) {
|
|
|
434
503
|
const state = await getNotebooklmPageState(page);
|
|
435
504
|
if (state.kind !== 'notebook' || !state.notebookId)
|
|
436
505
|
return [];
|
|
437
|
-
const raw = await page.evaluate(`(() => {
|
|
506
|
+
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
|
|
438
507
|
return Array.from(document.querySelectorAll('artifact-library-note')).map((node) => {
|
|
439
508
|
const titleNode = node.querySelector('.artifact-title');
|
|
440
509
|
return {
|
|
@@ -442,7 +511,7 @@ export async function listNotebooklmNotesFromPage(page) {
|
|
|
442
511
|
text: (node.innerText || node.textContent || '').replace(/\\s+/g, ' ').trim(),
|
|
443
512
|
};
|
|
444
513
|
});
|
|
445
|
-
})()`);
|
|
514
|
+
})()`));
|
|
446
515
|
if (!Array.isArray(raw) || raw.length === 0)
|
|
447
516
|
return [];
|
|
448
517
|
return parseNotebooklmNoteListRawRows(raw, state.notebookId, state.url || `https://${NOTEBOOKLM_DOMAIN}/notebook/${state.notebookId}`);
|
|
@@ -451,13 +520,13 @@ export async function readNotebooklmSummaryFromPage(page) {
|
|
|
451
520
|
const state = await getNotebooklmPageState(page);
|
|
452
521
|
if (state.kind !== 'notebook' || !state.notebookId)
|
|
453
522
|
return null;
|
|
454
|
-
const raw = await page.evaluate(`(() => {
|
|
523
|
+
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
|
|
455
524
|
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
456
525
|
const title = normalize(document.querySelector('.notebook-title, h1, [data-testid="notebook-title"]')?.textContent || document.title || '');
|
|
457
526
|
const summaryNode = document.querySelector('.notebook-summary, .summary-content, [class*="summary"]');
|
|
458
527
|
const summary = normalize(summaryNode?.textContent || '');
|
|
459
528
|
return { title, summary };
|
|
460
|
-
})()`);
|
|
529
|
+
})()`));
|
|
461
530
|
return parseNotebooklmSummaryRawRow(raw, state.notebookId, state.url || `https://${NOTEBOOKLM_DOMAIN}/notebook/${state.notebookId}`);
|
|
462
531
|
}
|
|
463
532
|
export async function getNotebooklmSummaryViaRpc(page) {
|
|
@@ -499,7 +568,7 @@ export async function readNotebooklmVisibleNoteFromPage(page) {
|
|
|
499
568
|
const state = await getNotebooklmPageState(page);
|
|
500
569
|
if (state.kind !== 'notebook' || !state.notebookId)
|
|
501
570
|
return null;
|
|
502
|
-
const raw = await page.evaluate(`(() => {
|
|
571
|
+
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
|
|
503
572
|
const normalizeText = (value) => (value || '').replace(/\\u00a0/g, ' ').replace(/\\r\\n/g, '\\n').trim();
|
|
504
573
|
const titleNode = document.querySelector('.note-header__editable-title');
|
|
505
574
|
const title = titleNode instanceof HTMLInputElement || titleNode instanceof HTMLTextAreaElement
|
|
@@ -516,7 +585,7 @@ export async function readNotebooklmVisibleNoteFromPage(page) {
|
|
|
516
585
|
title: normalizeText(title),
|
|
517
586
|
content: normalizeText(content),
|
|
518
587
|
};
|
|
519
|
-
})()`);
|
|
588
|
+
})()`));
|
|
520
589
|
return parseNotebooklmVisibleNoteRawRow(raw, state.notebookId, state.url || `https://${NOTEBOOKLM_DOMAIN}/notebook/${state.notebookId}`);
|
|
521
590
|
}
|
|
522
591
|
export async function ensureNotebooklmHome(page) {
|
|
@@ -526,11 +595,18 @@ export async function ensureNotebooklmHome(page) {
|
|
|
526
595
|
const currentKind = currentUrl ? classifyNotebooklmPage(currentUrl) : 'unknown';
|
|
527
596
|
if (currentKind === 'home')
|
|
528
597
|
return;
|
|
529
|
-
|
|
530
|
-
|
|
598
|
+
const authuser = getNotebooklmAuthuser();
|
|
599
|
+
const target = authuser ? `${NOTEBOOKLM_HOME_URL}?authuser=${encodeURIComponent(authuser)}` : NOTEBOOKLM_HOME_URL;
|
|
600
|
+
try {
|
|
601
|
+
await page.goto(target);
|
|
602
|
+
await page.wait(2);
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
throw new CommandExecutionError(`Failed to open NotebookLM home: ${error?.message || error}`);
|
|
606
|
+
}
|
|
531
607
|
}
|
|
532
608
|
export async function getNotebooklmPageState(page) {
|
|
533
|
-
const raw = await page.evaluate(`(() => {
|
|
609
|
+
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
|
|
534
610
|
const url = window.location.href;
|
|
535
611
|
const title = document.title || '';
|
|
536
612
|
const hostname = window.location.hostname || '';
|
|
@@ -557,7 +633,7 @@ export async function getNotebooklmPageState(page) {
|
|
|
557
633
|
.reduce((count, href, index, list) => list.indexOf(href) === index ? count + 1 : count, 0);
|
|
558
634
|
|
|
559
635
|
return { url, title, hostname, kind, notebookId, loginRequired, notebookCount, path };
|
|
560
|
-
})()`);
|
|
636
|
+
})()`));
|
|
561
637
|
const state = {
|
|
562
638
|
url: String(raw?.url ?? ''),
|
|
563
639
|
title: normalizeNotebooklmTitle(raw?.title, 'NotebookLM'),
|
|
@@ -582,7 +658,7 @@ export async function getNotebooklmPageState(page) {
|
|
|
582
658
|
return state;
|
|
583
659
|
}
|
|
584
660
|
export async function readCurrentNotebooklm(page) {
|
|
585
|
-
const raw = await page.evaluate(`(() => {
|
|
661
|
+
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
|
|
586
662
|
const url = window.location.href;
|
|
587
663
|
const match = url.match(/\\/notebook\\/([^/?#]+)/);
|
|
588
664
|
if (!match) return null;
|
|
@@ -595,7 +671,7 @@ export async function readCurrentNotebooklm(page) {
|
|
|
595
671
|
url,
|
|
596
672
|
source: 'current-page',
|
|
597
673
|
};
|
|
598
|
-
})()`);
|
|
674
|
+
})()`));
|
|
599
675
|
if (!raw)
|
|
600
676
|
return null;
|
|
601
677
|
return {
|
|
@@ -608,7 +684,7 @@ export async function readCurrentNotebooklm(page) {
|
|
|
608
684
|
};
|
|
609
685
|
}
|
|
610
686
|
export async function listNotebooklmLinks(page) {
|
|
611
|
-
const raw = await page.evaluate(`(() => {
|
|
687
|
+
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
|
|
612
688
|
const rows = [];
|
|
613
689
|
const seen = new Set();
|
|
614
690
|
|
|
@@ -656,7 +732,7 @@ export async function listNotebooklmLinks(page) {
|
|
|
656
732
|
}
|
|
657
733
|
|
|
658
734
|
return rows;
|
|
659
|
-
})()`);
|
|
735
|
+
})()`));
|
|
660
736
|
if (!Array.isArray(raw))
|
|
661
737
|
return [];
|
|
662
738
|
return raw
|
|
@@ -671,7 +747,7 @@ export async function listNotebooklmLinks(page) {
|
|
|
671
747
|
.filter((row) => row.id && row.url);
|
|
672
748
|
}
|
|
673
749
|
export async function listNotebooklmSourcesFromPage(page) {
|
|
674
|
-
const raw = await page.evaluate(`(() => {
|
|
750
|
+
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
|
|
675
751
|
const notebookMatch = window.location.href.match(/\\/notebook\\/([^/?#]+)/);
|
|
676
752
|
const notebookId = notebookMatch ? notebookMatch[1] : '';
|
|
677
753
|
if (!notebookId) return [];
|
|
@@ -721,7 +797,7 @@ export async function listNotebooklmSourcesFromPage(page) {
|
|
|
721
797
|
});
|
|
722
798
|
}
|
|
723
799
|
return rows;
|
|
724
|
-
})()`);
|
|
800
|
+
})()`));
|
|
725
801
|
if (!Array.isArray(raw))
|
|
726
802
|
return [];
|
|
727
803
|
return raw.filter((row) => row.id && row.title);
|