@jackwener/opencli 1.8.0 → 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.
Files changed (153) hide show
  1. package/README.md +8 -49
  2. package/README.zh-CN.md +8 -52
  3. package/cli-manifest.json +1796 -191
  4. package/clis/_atlassian/shared.js +577 -0
  5. package/clis/_atlassian/shared.test.js +170 -0
  6. package/clis/bilibili/comment.js +125 -0
  7. package/clis/bilibili/comment.test.js +153 -0
  8. package/clis/bilibili/comments.js +116 -21
  9. package/clis/bilibili/comments.test.js +77 -18
  10. package/clis/bilibili/subtitle.js +76 -31
  11. package/clis/bilibili/subtitle.test.js +156 -9
  12. package/clis/bilibili/utils.js +63 -5
  13. package/clis/bilibili/utils.test.js +45 -1
  14. package/clis/chess/analyze.js +35 -0
  15. package/clis/chess/analyze.test.js +79 -0
  16. package/clis/chess/game.js +114 -0
  17. package/clis/chess/game.test.js +178 -0
  18. package/clis/chess/games.js +67 -0
  19. package/clis/chess/games.test.js +164 -0
  20. package/clis/chess/stats.js +32 -0
  21. package/clis/chess/stats.test.js +79 -0
  22. package/clis/chess/utils.js +170 -0
  23. package/clis/chess/utils.test.js +230 -0
  24. package/clis/confluence/commands.test.js +195 -0
  25. package/clis/confluence/create.js +39 -0
  26. package/clis/confluence/page.js +23 -0
  27. package/clis/confluence/search.js +34 -0
  28. package/clis/confluence/shared.js +173 -0
  29. package/clis/confluence/update.js +38 -0
  30. package/clis/douyin/hashtag.js +84 -23
  31. package/clis/douyin/hashtag.test.js +113 -0
  32. package/clis/geogebra/add-circle.js +46 -0
  33. package/clis/geogebra/add-line.js +35 -0
  34. package/clis/geogebra/add-point.js +27 -0
  35. package/clis/geogebra/add-polygon.js +25 -0
  36. package/clis/geogebra/eval.js +35 -0
  37. package/clis/geogebra/geogebra.test.js +175 -0
  38. package/clis/geogebra/hexagon.js +62 -0
  39. package/clis/geogebra/info.js +72 -0
  40. package/clis/geogebra/list.js +35 -0
  41. package/clis/geogebra/triangle.js +60 -0
  42. package/clis/geogebra/utils.js +271 -0
  43. package/clis/jira/attachments.js +28 -0
  44. package/clis/jira/commands.test.js +287 -0
  45. package/clis/jira/comments.js +28 -0
  46. package/clis/jira/issue.js +28 -0
  47. package/clis/jira/links.js +28 -0
  48. package/clis/jira/search.js +47 -0
  49. package/clis/jira/shared.js +256 -0
  50. package/clis/linkedin/job-detail.js +167 -0
  51. package/clis/linkedin/job-detail.test.js +38 -0
  52. package/clis/linkedin/jobs-preferences.js +113 -0
  53. package/clis/linkedin/jobs-preferences.test.js +43 -0
  54. package/clis/linkedin/post-analytics.js +74 -0
  55. package/clis/linkedin/post-analytics.test.js +40 -0
  56. package/clis/linkedin/posts-core.js +241 -0
  57. package/clis/linkedin/posts.js +22 -0
  58. package/clis/linkedin/posts.test.js +40 -0
  59. package/clis/linkedin/profile-analytics.js +104 -0
  60. package/clis/linkedin/profile-analytics.test.js +67 -0
  61. package/clis/linkedin/profile-experience.js +671 -0
  62. package/clis/linkedin/profile-experience.test.js +152 -0
  63. package/clis/linkedin/profile-projects.js +311 -0
  64. package/clis/linkedin/profile-projects.test.js +111 -0
  65. package/clis/linkedin/profile-read.js +148 -0
  66. package/clis/linkedin/profile-read.test.js +77 -0
  67. package/clis/linkedin/services-read.js +213 -0
  68. package/clis/linkedin/services-read.test.js +105 -0
  69. package/clis/linkedin/shared.js +124 -0
  70. package/clis/linkedin/timeline.js +14 -7
  71. package/clis/notebooklm/add-source.js +269 -0
  72. package/clis/notebooklm/add-source.test.js +97 -0
  73. package/clis/notebooklm/create.js +76 -0
  74. package/clis/notebooklm/create.test.js +58 -0
  75. package/clis/notebooklm/generate-audio.js +91 -0
  76. package/clis/notebooklm/generate-audio.test.js +63 -0
  77. package/clis/notebooklm/generate-slides.js +106 -0
  78. package/clis/notebooklm/generate-slides.test.js +75 -0
  79. package/clis/notebooklm/open.test.js +10 -10
  80. package/clis/notebooklm/rpc.js +20 -6
  81. package/clis/notebooklm/rpc.test.js +27 -1
  82. package/clis/notebooklm/utils.js +100 -24
  83. package/clis/notebooklm/utils.test.js +60 -1
  84. package/clis/notebooklm/write-note.js +103 -0
  85. package/clis/notebooklm/write-note.test.js +70 -0
  86. package/clis/pixiv/detail.js +41 -34
  87. package/clis/pixiv/detail.test.js +93 -0
  88. package/clis/pixiv/user.js +36 -31
  89. package/clis/pixiv/user.test.js +100 -0
  90. package/clis/pixiv/utils.js +56 -7
  91. package/clis/suno/generate.js +5 -0
  92. package/clis/suno/generate.test.js +9 -0
  93. package/clis/suno/status.js +3 -2
  94. package/clis/suno/utils.js +33 -24
  95. package/clis/suno/utils.test.js +106 -0
  96. package/clis/twitter/followers.js +6 -2
  97. package/clis/twitter/followers.test.js +19 -1
  98. package/clis/twitter/following.js +14 -5
  99. package/clis/twitter/following.test.js +29 -0
  100. package/clis/twitter/likes.js +12 -4
  101. package/clis/twitter/likes.test.js +26 -1
  102. package/clis/twitter/list-add.js +1 -1
  103. package/clis/twitter/list-remove.js +1 -1
  104. package/clis/twitter/notifications.js +4 -4
  105. package/clis/twitter/post.js +62 -4
  106. package/clis/twitter/post.test.js +35 -3
  107. package/clis/twitter/profile.js +81 -28
  108. package/clis/twitter/profile.test.js +113 -2
  109. package/clis/twitter/quote.js +9 -4
  110. package/clis/twitter/reply.js +13 -10
  111. package/clis/twitter/reply.test.js +41 -0
  112. package/clis/twitter/search.js +1 -1
  113. package/clis/twitter/search.test.js +35 -0
  114. package/clis/twitter/shared.js +11 -0
  115. package/clis/twitter/shared.test.js +37 -1
  116. package/clis/twitter/utils.js +53 -16
  117. package/clis/upwork/detail.js +132 -0
  118. package/clis/upwork/feed.js +109 -0
  119. package/clis/upwork/search.js +115 -0
  120. package/clis/upwork/upwork.test.js +566 -0
  121. package/clis/upwork/utils.js +323 -0
  122. package/clis/weread/book-search.js +438 -0
  123. package/clis/weread/book-search.test.js +242 -0
  124. package/clis/weread/search-regression.test.js +80 -0
  125. package/clis/weread/search.js +17 -2
  126. package/clis/xiaohongshu/creator-note-detail.js +165 -28
  127. package/clis/xiaohongshu/creator-note-detail.test.js +186 -37
  128. package/clis/xiaohongshu/creator-notes.js +251 -2
  129. package/clis/xiaohongshu/creator-notes.test.js +79 -2
  130. package/clis/xiaohongshu/download.js +97 -39
  131. package/clis/xiaohongshu/download.test.js +201 -0
  132. package/clis/zhihu/answer-comments.js +2 -21
  133. package/clis/zhihu/answer-detail.js +2 -31
  134. package/clis/zhihu/collection.js +2 -14
  135. package/clis/zhihu/collection.test.js +4 -3
  136. package/clis/zhihu/question.js +1 -9
  137. package/clis/zhihu/question.test.js +2 -2
  138. package/clis/zhihu/search.js +1 -12
  139. package/clis/zhihu/search.test.js +2 -2
  140. package/clis/zhihu/text.js +29 -0
  141. package/clis/zhihu/text.test.js +24 -0
  142. package/dist/src/browser/network-cache.js +13 -1
  143. package/dist/src/browser/network-cache.test.js +17 -0
  144. package/dist/src/download/index.js +13 -1
  145. package/dist/src/download/index.test.js +23 -1
  146. package/dist/src/download/media-download.test.js +3 -1
  147. package/dist/src/download/progress.js +2 -2
  148. package/dist/src/download/progress.test.js +12 -1
  149. package/dist/src/output.js +11 -1
  150. package/dist/src/output.test.js +6 -0
  151. package/dist/src/registry.js +1 -0
  152. package/dist/src/registry.test.js +11 -0
  153. package/package.json +1 -1
@@ -0,0 +1,63 @@
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-audio.js';
5
+
6
+ const { AUDIO_OVERVIEW_CONFIG_BLOCK, buildCreateAudioArgs, parseAudioIdFromResult } = __test__;
7
+
8
+ describe('notebooklm generate-audio', () => {
9
+ it('AUDIO_OVERVIEW_CONFIG_BLOCK matches the live-captured wire shape', () => {
10
+ expect(AUDIO_OVERVIEW_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('buildCreateAudioArgs matches the byte-perfect wire format captured from the UI', () => {
18
+ const projectId = '42ad744e-477d-4198-97b6-a9ae6a663165';
19
+ const sourceId = '7c7666bd-59e1-42ab-879d-bacfe33325eb';
20
+ expect(buildCreateAudioArgs(projectId, [sourceId])).toEqual([
21
+ [2, null, null, [1, null, null, null, null, null, null, null, null, null, [1]], [[1, 4, 2, 3, 6]]],
22
+ projectId,
23
+ [null, null, 1, [[[sourceId]]], null, null, [null, [null, null, null, [[sourceId]]]]],
24
+ ]);
25
+ });
26
+
27
+ it('buildCreateAudioArgs threads multiple sources into both the nested and tail blocks', () => {
28
+ const a = '11111111-1111-4111-8111-111111111111';
29
+ const b = '22222222-2222-4222-8222-222222222222';
30
+ const args = buildCreateAudioArgs('pid', [a, b]);
31
+ expect(args[2][3]).toEqual([[[a]], [[b]]]);
32
+ expect(args[2][6][1][3]).toEqual([[a], [b]]);
33
+ });
34
+
35
+ it('parseAudioIdFromResult walks the tree for a UUID-shaped audio id', () => {
36
+ const id = '38da0e55-2360-4d3e-8573-61b5a6c0c219';
37
+ expect(parseAudioIdFromResult([[id, 'opencli-audio-benjaminliu', 1]])).toBe(id);
38
+ expect(parseAudioIdFromResult({ result: { audioId: id } })).toBe(id);
39
+ });
40
+
41
+ it('parseAudioIdFromResult ignores non-UUID strings', () => {
42
+ expect(parseAudioIdFromResult([[null, 'opencli-audio-benjaminliu', 1]])).toBe('');
43
+ expect(parseAudioIdFromResult({})).toBe('');
44
+ expect(parseAudioIdFromResult([])).toBe('');
45
+ expect(parseAudioIdFromResult(null)).toBe('');
46
+ });
47
+
48
+ it('parseAudioIdFromResult skips notebook/source ids before selecting the generated audio id', () => {
49
+ const notebookId = '17e2b882-6a01-4c6c-9262-0738dfa2abee';
50
+ const sourceId = '7c7666bd-59e1-42ab-879d-bacfe33325eb';
51
+ const audioId = '38da0e55-2360-4d3e-8573-61b5a6c0c219';
52
+ expect(parseAudioIdFromResult([notebookId, [[sourceId]], [audioId]], [notebookId, sourceId])).toBe(audioId);
53
+ });
54
+
55
+ it('refuses to trigger remote audio generation without --execute', async () => {
56
+ const command = getRegistry().get('notebooklm/generate-audio');
57
+ const page = { goto: vi.fn() };
58
+ await expect(command.func(page, {
59
+ notebook: '17e2b882-6a01-4c6c-9262-0738dfa2abee',
60
+ })).rejects.toThrow(ArgumentError);
61
+ expect(page.goto).not.toHaveBeenCalled();
62
+ });
63
+ });
@@ -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/nb-demo',
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: 'nb-demo',
30
+ notebookId: '17e2b882-1234-1234-1234-abcdef012345',
31
31
  loginRequired: false,
32
32
  notebookCount: 1,
33
33
  });
34
34
  mockReadCurrentNotebooklm.mockResolvedValue({
35
- id: 'nb-demo',
35
+ id: '17e2b882-1234-1234-1234-abcdef012345',
36
36
  title: 'Browser Automation',
37
- url: 'https://notebooklm.google.com/notebook/nb-demo',
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: 'nb-demo' });
47
- expect(page.goto).toHaveBeenCalledWith('https://notebooklm.google.com/notebook/nb-demo');
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: 'nb-demo',
49
+ id: '17e2b882-1234-1234-1234-abcdef012345',
50
50
  title: 'Browser Automation',
51
- url: 'https://notebooklm.google.com/notebook/nb-demo',
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/nb-demo?pli=1' });
61
- expect(page.goto).toHaveBeenCalledWith('https://notebooklm.google.com/notebook/nb-demo');
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
  });
@@ -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
  });