@jackwener/opencli 1.5.8 → 1.6.0

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 (220) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +35 -1
  3. package/README.zh-CN.md +17 -1
  4. package/SKILL.md +31 -851
  5. package/autoresearch/baseline-browse.txt +1 -0
  6. package/autoresearch/baseline-skill.txt +1 -0
  7. package/autoresearch/browse-tasks.json +688 -0
  8. package/autoresearch/eval-browse.ts +185 -0
  9. package/autoresearch/eval-skill.ts +248 -0
  10. package/autoresearch/run-browse.sh +9 -0
  11. package/autoresearch/run-skill.sh +9 -0
  12. package/dist/browser/base-page.d.ts +48 -0
  13. package/dist/browser/base-page.js +160 -0
  14. package/dist/browser/cdp.js +4 -106
  15. package/dist/browser/daemon-client.d.ts +20 -7
  16. package/dist/browser/daemon-client.js +39 -39
  17. package/dist/browser/daemon-client.test.js +77 -0
  18. package/dist/browser/discover.d.ts +1 -4
  19. package/dist/browser/discover.js +9 -23
  20. package/dist/browser/errors.d.ts +4 -0
  21. package/dist/browser/errors.js +20 -0
  22. package/dist/browser/index.d.ts +1 -1
  23. package/dist/browser/index.js +1 -1
  24. package/dist/browser/page.d.ts +10 -35
  25. package/dist/browser/page.js +55 -187
  26. package/dist/browser/tabs.js +5 -5
  27. package/dist/browser.test.js +15 -15
  28. package/dist/cli-manifest.json +294 -22
  29. package/dist/cli.js +392 -0
  30. package/dist/clis/amazon/bestsellers.d.ts +21 -0
  31. package/dist/clis/amazon/bestsellers.js +130 -0
  32. package/dist/clis/amazon/bestsellers.test.js +20 -0
  33. package/dist/clis/amazon/discussion.d.ts +20 -0
  34. package/dist/clis/amazon/discussion.js +91 -0
  35. package/dist/clis/amazon/discussion.test.d.ts +1 -0
  36. package/dist/clis/amazon/discussion.test.js +36 -0
  37. package/dist/clis/amazon/offer.d.ts +23 -0
  38. package/dist/clis/amazon/offer.js +140 -0
  39. package/dist/clis/amazon/offer.test.d.ts +1 -0
  40. package/dist/clis/amazon/offer.test.js +29 -0
  41. package/dist/clis/amazon/product.d.ts +18 -0
  42. package/dist/clis/amazon/product.js +92 -0
  43. package/dist/clis/amazon/product.test.d.ts +1 -0
  44. package/dist/clis/amazon/product.test.js +24 -0
  45. package/dist/clis/amazon/search.d.ts +18 -0
  46. package/dist/clis/amazon/search.js +87 -0
  47. package/dist/clis/amazon/search.test.d.ts +1 -0
  48. package/dist/clis/amazon/search.test.js +22 -0
  49. package/dist/clis/amazon/shared.d.ts +64 -0
  50. package/dist/clis/amazon/shared.js +255 -0
  51. package/dist/clis/amazon/shared.test.d.ts +1 -0
  52. package/dist/clis/amazon/shared.test.js +33 -0
  53. package/dist/clis/gemini/ask.d.ts +1 -0
  54. package/dist/clis/gemini/ask.js +40 -0
  55. package/dist/clis/gemini/image.d.ts +1 -0
  56. package/dist/clis/gemini/image.js +105 -0
  57. package/dist/clis/gemini/new.d.ts +1 -0
  58. package/dist/clis/gemini/new.js +20 -0
  59. package/dist/clis/gemini/utils.d.ts +34 -0
  60. package/dist/clis/gemini/utils.js +463 -0
  61. package/dist/clis/gemini/utils.test.d.ts +1 -0
  62. package/dist/clis/gemini/utils.test.js +31 -0
  63. package/dist/clis/notebooklm/compat.test.d.ts +1 -1
  64. package/dist/clis/notebooklm/compat.test.js +3 -3
  65. package/dist/clis/notebooklm/current.js +2 -3
  66. package/dist/clis/notebooklm/get.js +2 -3
  67. package/dist/clis/notebooklm/history.js +2 -3
  68. package/dist/clis/notebooklm/note-list.js +2 -3
  69. package/dist/clis/notebooklm/notes-get.js +2 -3
  70. package/dist/clis/notebooklm/open.d.ts +1 -0
  71. package/dist/clis/notebooklm/open.js +41 -0
  72. package/dist/clis/notebooklm/open.test.d.ts +1 -0
  73. package/dist/clis/notebooklm/open.test.js +63 -0
  74. package/dist/clis/notebooklm/source-fulltext.js +2 -3
  75. package/dist/clis/notebooklm/source-get.js +2 -3
  76. package/dist/clis/notebooklm/source-guide.js +2 -3
  77. package/dist/clis/notebooklm/source-list.js +2 -3
  78. package/dist/clis/notebooklm/status.js +1 -2
  79. package/dist/clis/notebooklm/summary.js +2 -3
  80. package/dist/clis/notebooklm/utils.d.ts +2 -1
  81. package/dist/clis/notebooklm/utils.js +20 -21
  82. package/dist/clis/twitter/article.js +28 -1
  83. package/dist/clis/xiaohongshu/creator-note-detail.test.js +11 -11
  84. package/dist/clis/xiaohongshu/creator-notes-summary.test.js +6 -6
  85. package/dist/clis/xiaohongshu/creator-notes.test.js +22 -22
  86. package/dist/clis/xiaohongshu/note.js +11 -0
  87. package/dist/clis/xiaohongshu/note.test.js +49 -0
  88. package/dist/commanderAdapter.js +7 -4
  89. package/dist/commanderAdapter.test.js +76 -0
  90. package/dist/commands/daemon.js +8 -47
  91. package/dist/commands/daemon.test.js +45 -70
  92. package/dist/discovery.js +27 -0
  93. package/dist/doctor.d.ts +1 -2
  94. package/dist/doctor.js +7 -8
  95. package/dist/explore.js +1 -1
  96. package/dist/output.js +28 -0
  97. package/dist/output.test.js +15 -0
  98. package/dist/pipeline/executor.js +2 -7
  99. package/dist/pipeline/steps/browser.js +1 -1
  100. package/dist/pipeline/template.js +25 -3
  101. package/dist/record.d.ts +50 -0
  102. package/dist/record.js +298 -57
  103. package/dist/record.test.d.ts +1 -0
  104. package/dist/record.test.js +293 -0
  105. package/dist/registry.d.ts +2 -0
  106. package/dist/registry.js +1 -0
  107. package/dist/registry.test.js +10 -0
  108. package/dist/runtime.js +3 -3
  109. package/dist/snapshotFormatter.d.ts +1 -1
  110. package/dist/snapshotFormatter.js +4 -4
  111. package/dist/snapshotFormatter.test.d.ts +1 -1
  112. package/dist/snapshotFormatter.test.js +2 -2
  113. package/dist/types.d.ts +11 -1
  114. package/dist/types.js +1 -1
  115. package/docs/.vitepress/config.mts +2 -0
  116. package/docs/adapters/browser/amazon.md +53 -0
  117. package/docs/adapters/browser/gemini.md +72 -0
  118. package/docs/adapters/browser/notebooklm.md +5 -5
  119. package/docs/adapters/index.md +3 -1
  120. package/docs/guide/getting-started.md +21 -0
  121. package/docs/superpowers/specs/2026-04-02-browse-skill-testing-design.md +144 -0
  122. package/docs/zh/guide/getting-started.md +21 -0
  123. package/extension/package-lock.json +2 -2
  124. package/extension/src/background.test.ts +7 -163
  125. package/extension/src/background.ts +58 -161
  126. package/extension/src/cdp.ts +77 -124
  127. package/extension/src/protocol.ts +5 -5
  128. package/package.json +1 -1
  129. package/skills/opencli-explorer/SKILL.md +853 -0
  130. package/skills/opencli-oneshot/SKILL.md +222 -0
  131. package/skills/opencli-operate/SKILL.md +213 -0
  132. package/skills/opencli-usage/SKILL.md +152 -0
  133. package/skills/opencli-usage/browser.md +429 -0
  134. package/skills/opencli-usage/desktop.md +118 -0
  135. package/skills/opencli-usage/plugins.md +82 -0
  136. package/skills/opencli-usage/public-api.md +149 -0
  137. package/src/browser/base-page.ts +197 -0
  138. package/src/browser/cdp.ts +7 -131
  139. package/src/browser/daemon-client.test.ts +103 -0
  140. package/src/browser/daemon-client.ts +55 -43
  141. package/src/browser/discover.ts +9 -21
  142. package/src/browser/errors.ts +22 -0
  143. package/src/browser/index.ts +1 -1
  144. package/src/browser/page.ts +57 -209
  145. package/src/browser/tabs.ts +5 -5
  146. package/src/browser.test.ts +15 -15
  147. package/src/cli.ts +392 -0
  148. package/src/clis/amazon/bestsellers.test.ts +22 -0
  149. package/src/clis/amazon/bestsellers.ts +180 -0
  150. package/src/clis/amazon/discussion.test.ts +38 -0
  151. package/src/clis/amazon/discussion.ts +131 -0
  152. package/src/clis/amazon/offer.test.ts +35 -0
  153. package/src/clis/amazon/offer.ts +185 -0
  154. package/src/clis/amazon/product.test.ts +26 -0
  155. package/src/clis/amazon/product.ts +131 -0
  156. package/src/clis/amazon/search.test.ts +24 -0
  157. package/src/clis/amazon/search.ts +128 -0
  158. package/src/clis/amazon/shared.test.ts +37 -0
  159. package/src/clis/amazon/shared.ts +316 -0
  160. package/src/clis/gemini/ask.ts +46 -0
  161. package/src/clis/gemini/image.ts +115 -0
  162. package/src/clis/gemini/new.ts +22 -0
  163. package/src/clis/gemini/utils.test.ts +36 -0
  164. package/src/clis/gemini/utils.ts +523 -0
  165. package/src/clis/notebooklm/compat.test.ts +3 -3
  166. package/src/clis/notebooklm/current.ts +2 -3
  167. package/src/clis/notebooklm/get.ts +1 -3
  168. package/src/clis/notebooklm/history.ts +1 -3
  169. package/src/clis/notebooklm/note-list.ts +1 -3
  170. package/src/clis/notebooklm/notes-get.ts +1 -3
  171. package/src/clis/notebooklm/open.test.ts +78 -0
  172. package/src/clis/notebooklm/open.ts +61 -0
  173. package/src/clis/notebooklm/source-fulltext.ts +1 -3
  174. package/src/clis/notebooklm/source-get.ts +1 -3
  175. package/src/clis/notebooklm/source-guide.ts +1 -3
  176. package/src/clis/notebooklm/source-list.ts +1 -3
  177. package/src/clis/notebooklm/status.ts +1 -2
  178. package/src/clis/notebooklm/summary.ts +1 -3
  179. package/src/clis/notebooklm/utils.ts +29 -20
  180. package/src/clis/twitter/article.ts +31 -1
  181. package/src/clis/xiaohongshu/creator-note-detail.test.ts +11 -11
  182. package/src/clis/xiaohongshu/creator-notes-summary.test.ts +6 -6
  183. package/src/clis/xiaohongshu/creator-notes.test.ts +22 -22
  184. package/src/clis/xiaohongshu/note.test.ts +51 -0
  185. package/src/clis/xiaohongshu/note.ts +18 -0
  186. package/src/commanderAdapter.test.ts +109 -0
  187. package/src/commanderAdapter.ts +8 -4
  188. package/src/commands/daemon.test.ts +50 -84
  189. package/src/commands/daemon.ts +8 -56
  190. package/src/discovery.ts +22 -0
  191. package/src/doctor.ts +8 -9
  192. package/src/explore.ts +1 -1
  193. package/src/output.test.ts +17 -0
  194. package/src/output.ts +27 -0
  195. package/src/pipeline/executor.ts +2 -7
  196. package/src/pipeline/steps/browser.ts +1 -1
  197. package/src/pipeline/template.ts +27 -4
  198. package/src/record.test.ts +362 -0
  199. package/src/record.ts +341 -62
  200. package/src/registry.test.ts +12 -0
  201. package/src/registry.ts +3 -0
  202. package/src/runtime.ts +3 -3
  203. package/src/snapshotFormatter.test.ts +2 -2
  204. package/src/snapshotFormatter.ts +4 -4
  205. package/src/types.ts +11 -1
  206. package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
  207. package/.agents/workflows/cross-project-adapter-migration.md +0 -54
  208. package/dist/clis/notebooklm/bind-current.js +0 -29
  209. package/dist/clis/notebooklm/bind-current.test.d.ts +0 -1
  210. package/dist/clis/notebooklm/bind-current.test.js +0 -35
  211. package/dist/clis/notebooklm/binding.test.js +0 -44
  212. package/extension/dist/background.js +0 -819
  213. package/src/clis/notebooklm/bind-current.test.ts +0 -43
  214. package/src/clis/notebooklm/bind-current.ts +0 -36
  215. package/src/clis/notebooklm/binding.test.ts +0 -53
  216. /package/dist/browser/{mcp.d.ts → bridge.d.ts} +0 -0
  217. /package/dist/browser/{mcp.js → bridge.js} +0 -0
  218. /package/dist/{clis/notebooklm/bind-current.d.ts → browser/daemon-client.test.d.ts} +0 -0
  219. /package/dist/clis/{notebooklm/binding.test.d.ts → amazon/bestsellers.test.d.ts} +0 -0
  220. /package/src/browser/{mcp.ts → bridge.ts} +0 -0
@@ -0,0 +1,78 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const {
4
+ mockGetNotebooklmPageState,
5
+ mockReadCurrentNotebooklm,
6
+ mockRequireNotebooklmSession,
7
+ } = vi.hoisted(() => ({
8
+ mockGetNotebooklmPageState: vi.fn(),
9
+ mockReadCurrentNotebooklm: 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
+ readCurrentNotebooklm: mockReadCurrentNotebooklm,
19
+ requireNotebooklmSession: mockRequireNotebooklmSession,
20
+ };
21
+ });
22
+
23
+ import { getRegistry } from '../../registry.js';
24
+ import './open.js';
25
+
26
+ describe('notebooklm open', () => {
27
+ const command = getRegistry().get('notebooklm/open');
28
+
29
+ beforeEach(() => {
30
+ mockGetNotebooklmPageState.mockReset();
31
+ mockReadCurrentNotebooklm.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
+ mockReadCurrentNotebooklm.mockResolvedValue({
44
+ id: 'nb-demo',
45
+ title: 'Browser Automation',
46
+ url: 'https://notebooklm.google.com/notebook/nb-demo',
47
+ source: 'current-page',
48
+ });
49
+ });
50
+
51
+ it('opens a notebook by id in the automation workspace', async () => {
52
+ const page = {
53
+ goto: vi.fn(async () => {}),
54
+ wait: vi.fn(async () => {}),
55
+ };
56
+
57
+ const result = await command!.func!(page as any, { notebook: 'nb-demo' });
58
+
59
+ expect(page.goto).toHaveBeenCalledWith('https://notebooklm.google.com/notebook/nb-demo');
60
+ expect(result).toEqual([{
61
+ id: 'nb-demo',
62
+ title: 'Browser Automation',
63
+ url: 'https://notebooklm.google.com/notebook/nb-demo',
64
+ source: 'current-page',
65
+ }]);
66
+ });
67
+
68
+ it('accepts a full notebook url', async () => {
69
+ const page = {
70
+ goto: vi.fn(async () => {}),
71
+ wait: vi.fn(async () => {}),
72
+ };
73
+
74
+ await command!.func!(page as any, { notebook: 'https://notebooklm.google.com/notebook/nb-demo?pli=1' });
75
+
76
+ expect(page.goto).toHaveBeenCalledWith('https://notebooklm.google.com/notebook/nb-demo');
77
+ });
78
+ });
@@ -0,0 +1,61 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+ import { CliError, EmptyResultError } from '../../errors.js';
4
+ import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
5
+ import {
6
+ buildNotebooklmNotebookUrl,
7
+ getNotebooklmPageState,
8
+ parseNotebooklmNotebookTarget,
9
+ readCurrentNotebooklm,
10
+ requireNotebooklmSession,
11
+ } from './utils.js';
12
+
13
+ cli({
14
+ site: NOTEBOOKLM_SITE,
15
+ name: 'open',
16
+ aliases: ['select'],
17
+ description: 'Open one NotebookLM notebook in the automation workspace by id or URL',
18
+ domain: NOTEBOOKLM_DOMAIN,
19
+ strategy: Strategy.COOKIE,
20
+ browser: true,
21
+ navigateBefore: false,
22
+ args: [
23
+ {
24
+ name: 'notebook',
25
+ positional: true,
26
+ required: true,
27
+ help: 'Notebook id from list output, or a full NotebookLM notebook URL',
28
+ },
29
+ ],
30
+ columns: ['id', 'title', 'url', 'source'],
31
+ func: async (page: IPage, kwargs) => {
32
+ const notebookId = parseNotebooklmNotebookTarget(String(kwargs.notebook ?? ''));
33
+ await page.goto(buildNotebooklmNotebookUrl(notebookId));
34
+ await page.wait(2);
35
+ await requireNotebooklmSession(page);
36
+
37
+ const state = await getNotebooklmPageState(page);
38
+ if (state.kind !== 'notebook') {
39
+ throw new CliError(
40
+ 'NOTEBOOKLM_OPEN_FAILED',
41
+ `NotebookLM notebook "${notebookId}" did not open in the automation workspace`,
42
+ 'Run `opencli notebooklm list -f json` first and pass a valid notebook id.',
43
+ );
44
+ }
45
+ if (state.notebookId !== notebookId) {
46
+ console.warn(
47
+ `[notebooklm open] expected notebook "${notebookId}" but page reports "${state.notebookId}"; continuing`,
48
+ );
49
+ }
50
+
51
+ const current = await readCurrentNotebooklm(page);
52
+ if (!current) {
53
+ throw new EmptyResultError(
54
+ 'opencli notebooklm open',
55
+ 'NotebookLM notebook metadata was not found after navigation.',
56
+ );
57
+ }
58
+
59
+ return [current];
60
+ },
61
+ });
@@ -3,7 +3,6 @@ import type { IPage } from '../../types.js';
3
3
  import { EmptyResultError } from '../../errors.js';
4
4
  import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
5
5
  import {
6
- ensureNotebooklmNotebookBinding,
7
6
  findNotebooklmSourceRow,
8
7
  getNotebooklmPageState,
9
8
  getNotebooklmSourceFulltextViaRpc,
@@ -30,13 +29,12 @@ cli({
30
29
  ],
31
30
  columns: ['title', 'kind', 'char_count', 'url', 'source'],
32
31
  func: async (page: IPage, kwargs) => {
33
- await ensureNotebooklmNotebookBinding(page);
34
32
  await requireNotebooklmSession(page);
35
33
  const state = await getNotebooklmPageState(page);
36
34
  if (state.kind !== 'notebook') {
37
35
  throw new EmptyResultError(
38
36
  'opencli notebooklm source-fulltext',
39
- 'Open a specific NotebookLM notebook tab first, then retry.',
37
+ 'No NotebookLM notebook is open in the automation workspace. Run `opencli notebooklm open <notebook>` first.',
40
38
  );
41
39
  }
42
40
 
@@ -3,7 +3,6 @@ import type { IPage } from '../../types.js';
3
3
  import { EmptyResultError } from '../../errors.js';
4
4
  import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
5
5
  import {
6
- ensureNotebooklmNotebookBinding,
7
6
  findNotebooklmSourceRow,
8
7
  getNotebooklmPageState,
9
8
  listNotebooklmSourcesFromPage,
@@ -29,13 +28,12 @@ cli({
29
28
  ],
30
29
  columns: ['title', 'id', 'type', 'size', 'created_at', 'updated_at', 'url', 'source'],
31
30
  func: async (page: IPage, kwargs) => {
32
- await ensureNotebooklmNotebookBinding(page);
33
31
  await requireNotebooklmSession(page);
34
32
  const state = await getNotebooklmPageState(page);
35
33
  if (state.kind !== 'notebook') {
36
34
  throw new EmptyResultError(
37
35
  'opencli notebooklm source-get',
38
- 'Open a specific NotebookLM notebook tab first, then retry.',
36
+ 'No NotebookLM notebook is open in the automation workspace. Run `opencli notebooklm open <notebook>` first.',
39
37
  );
40
38
  }
41
39
 
@@ -3,7 +3,6 @@ import type { IPage } from '../../types.js';
3
3
  import { EmptyResultError } from '../../errors.js';
4
4
  import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
5
5
  import {
6
- ensureNotebooklmNotebookBinding,
7
6
  findNotebooklmSourceRow,
8
7
  getNotebooklmPageState,
9
8
  getNotebooklmSourceGuideViaRpc,
@@ -30,13 +29,12 @@ cli({
30
29
  ],
31
30
  columns: ['source_id', 'notebook_id', 'title', 'type', 'summary', 'keywords', 'source'],
32
31
  func: async (page: IPage, kwargs) => {
33
- await ensureNotebooklmNotebookBinding(page);
34
32
  await requireNotebooklmSession(page);
35
33
  const state = await getNotebooklmPageState(page);
36
34
  if (state.kind !== 'notebook') {
37
35
  throw new EmptyResultError(
38
36
  'opencli notebooklm source-guide',
39
- 'Open a specific NotebookLM notebook tab first, then retry.',
37
+ 'No NotebookLM notebook is open in the automation workspace. Run `opencli notebooklm open <notebook>` first.',
40
38
  );
41
39
  }
42
40
 
@@ -3,7 +3,6 @@ import type { IPage } from '../../types.js';
3
3
  import { EmptyResultError } from '../../errors.js';
4
4
  import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
5
5
  import {
6
- ensureNotebooklmNotebookBinding,
7
6
  getNotebooklmPageState,
8
7
  listNotebooklmSourcesFromPage,
9
8
  listNotebooklmSourcesViaRpc,
@@ -21,13 +20,12 @@ cli({
21
20
  args: [],
22
21
  columns: ['title', 'id', 'type', 'size', 'created_at', 'updated_at', 'url', 'source'],
23
22
  func: async (page: IPage) => {
24
- await ensureNotebooklmNotebookBinding(page);
25
23
  await requireNotebooklmSession(page);
26
24
  const state = await getNotebooklmPageState(page);
27
25
  if (state.kind !== 'notebook') {
28
26
  throw new EmptyResultError(
29
27
  'opencli notebooklm source-list',
30
- 'Open a specific NotebookLM notebook tab first, then retry.',
28
+ 'No NotebookLM notebook is open in the automation workspace. Run `opencli notebooklm open <notebook>` first.',
31
29
  );
32
30
  }
33
31
 
@@ -1,7 +1,7 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
2
  import type { IPage } from '../../types.js';
3
3
  import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_HOME_URL, NOTEBOOKLM_SITE } from './shared.js';
4
- import { ensureNotebooklmNotebookBinding, getNotebooklmPageState } from './utils.js';
4
+ import { getNotebooklmPageState } from './utils.js';
5
5
 
6
6
  cli({
7
7
  site: NOTEBOOKLM_SITE,
@@ -14,7 +14,6 @@ cli({
14
14
  args: [],
15
15
  columns: ['status', 'login', 'page', 'url', 'title', 'notebooks'],
16
16
  func: async (page: IPage) => {
17
- await ensureNotebooklmNotebookBinding(page);
18
17
  const currentUrl = await page.getCurrentUrl?.().catch(() => null);
19
18
  if (!currentUrl || !currentUrl.includes(NOTEBOOKLM_DOMAIN)) {
20
19
  await page.goto(NOTEBOOKLM_HOME_URL);
@@ -3,7 +3,6 @@ import type { IPage } from '../../types.js';
3
3
  import { EmptyResultError } from '../../errors.js';
4
4
  import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
5
5
  import {
6
- ensureNotebooklmNotebookBinding,
7
6
  getNotebooklmPageState,
8
7
  getNotebooklmSummaryViaRpc,
9
8
  readNotebooklmSummaryFromPage,
@@ -21,13 +20,12 @@ cli({
21
20
  args: [],
22
21
  columns: ['title', 'summary', 'source', 'url'],
23
22
  func: async (page: IPage) => {
24
- await ensureNotebooklmNotebookBinding(page);
25
23
  await requireNotebooklmSession(page);
26
24
  const state = await getNotebooklmPageState(page);
27
25
  if (state.kind !== 'notebook') {
28
26
  throw new EmptyResultError(
29
27
  'opencli notebooklm summary',
30
- 'Open a specific NotebookLM notebook tab first, then retry.',
28
+ 'No NotebookLM notebook is open in the automation workspace. Run `opencli notebooklm open <notebook>` first.',
31
29
  );
32
30
  }
33
31
 
@@ -1,10 +1,8 @@
1
1
  import { AuthRequiredError, CliError } from '../../errors.js';
2
2
  import type { IPage } from '../../types.js';
3
- import { bindCurrentTab } from '../../browser/daemon-client.js';
4
3
  import {
5
4
  NOTEBOOKLM_DOMAIN,
6
5
  NOTEBOOKLM_HOME_URL,
7
- NOTEBOOKLM_SITE,
8
6
  type NotebooklmHistoryRow,
9
7
  type NotebooklmNotebookDetailRow,
10
8
  type NotebooklmNoteDetailRow,
@@ -54,6 +52,35 @@ export function parseNotebooklmIdFromUrl(url: string): string {
54
52
  return match?.[1] ?? '';
55
53
  }
56
54
 
55
+ export function parseNotebooklmNotebookTarget(value: string): string {
56
+ const normalized = value.trim();
57
+ if (!normalized) {
58
+ throw new CliError(
59
+ 'NOTEBOOKLM_INVALID_NOTEBOOK',
60
+ 'NotebookLM notebook id is required',
61
+ 'Pass a notebook id from `opencli notebooklm list` or a full notebook URL.',
62
+ );
63
+ }
64
+
65
+ if (/^https?:\/\//i.test(normalized)) {
66
+ const notebookId = parseNotebooklmIdFromUrl(normalized);
67
+ if (notebookId) return notebookId;
68
+ throw new CliError(
69
+ 'NOTEBOOKLM_INVALID_NOTEBOOK',
70
+ 'NotebookLM notebook URL is invalid',
71
+ 'Pass a full NotebookLM notebook URL like https://notebooklm.google.com/notebook/<id>.',
72
+ );
73
+ }
74
+
75
+ const pathMatch = normalized.match(/(?:^|\/)notebook\/([^/?#]+)/);
76
+ if (pathMatch?.[1]) return pathMatch[1];
77
+ return normalized;
78
+ }
79
+
80
+ export function buildNotebooklmNotebookUrl(notebookId: string): string {
81
+ return new URL(`/notebook/${encodeURIComponent(notebookId)}`, NOTEBOOKLM_HOME_URL).toString();
82
+ }
83
+
57
84
  export function classifyNotebooklmPage(url: string): NotebooklmPageKind {
58
85
  try {
59
86
  const parsed = new URL(url);
@@ -656,24 +683,6 @@ export async function ensureNotebooklmHome(page: IPage): Promise<void> {
656
683
  await page.wait(2);
657
684
  }
658
685
 
659
- export async function ensureNotebooklmNotebookBinding(page: IPage): Promise<boolean> {
660
- if (!page.getCurrentUrl) return false;
661
- if (process.env.OPENCLI_CDP_ENDPOINT) return false;
662
-
663
- const currentUrl = await page.getCurrentUrl().catch(() => null);
664
- if (currentUrl && classifyNotebooklmPage(currentUrl) === 'notebook') return false;
665
-
666
- try {
667
- await bindCurrentTab(`site:${NOTEBOOKLM_SITE}`, {
668
- matchDomain: NOTEBOOKLM_DOMAIN,
669
- matchPathPrefix: '/notebook/',
670
- });
671
- return true;
672
- } catch {
673
- return false;
674
- }
675
- }
676
-
677
686
  export async function getNotebooklmPageState(page: IPage): Promise<NotebooklmPageState> {
678
687
  const raw = await page.evaluate(`(() => {
679
688
  const url = window.location.href;
@@ -16,11 +16,41 @@ cli({
16
16
  ],
17
17
  columns: ['title', 'author', 'content', 'url'],
18
18
  func: async (page, kwargs) => {
19
- // Extract tweet ID from URL if needed
19
+ // Extract tweet ID from URL if needed.
20
+ // Article URLs (x.com/i/article/{articleId}) use a different ID than
21
+ // tweet status URLs — the GraphQL endpoint needs the parent tweet ID.
20
22
  let tweetId = kwargs['tweet-id'];
23
+ const isArticleUrl = /\/article\/\d+/.test(tweetId);
21
24
  const urlMatch = tweetId.match(/\/(?:status|article)\/(\d+)/);
22
25
  if (urlMatch) tweetId = urlMatch[1];
23
26
 
27
+ if (isArticleUrl) {
28
+ // Navigate to the article page and resolve the parent tweet ID from DOM
29
+ await page.goto(`https://x.com/i/article/${tweetId}`);
30
+ await page.wait(3);
31
+ const resolvedId = await page.evaluate(`
32
+ (function() {
33
+ var links = document.querySelectorAll('a[href*="/status/"]');
34
+ for (var i = 0; i < links.length; i++) {
35
+ var m = links[i].href.match(/\\/status\\/(\\d+)/);
36
+ if (m) return m[1];
37
+ }
38
+ var og = document.querySelector('meta[property="og:url"]');
39
+ if (og && og.content) {
40
+ var m2 = og.content.match(/\\/status\\/(\\d+)/);
41
+ if (m2) return m2[1];
42
+ }
43
+ return null;
44
+ })()
45
+ `);
46
+ if (!resolvedId || typeof resolvedId !== 'string') {
47
+ throw new CommandExecutionError(
48
+ `Could not resolve article ${tweetId} to a tweet ID. The article page may not contain a linked tweet.`,
49
+ );
50
+ }
51
+ tweetId = resolvedId;
52
+ }
53
+
24
54
  // Navigate to the tweet page for cookie context
25
55
  await page.goto(`https://x.com/i/status/${tweetId}`);
26
56
  await page.wait(3);
@@ -40,9 +40,9 @@ function createPageMock(evaluateResult: any): IPage {
40
40
  describe('xiaohongshu creator-note-detail', () => {
41
41
  it('parses note detail page text into info and metric rows', () => {
42
42
  const bodyText = `笔记数据详情
43
- 一张图讲清 诡秘之主·耕种者途径
44
- #诡秘之主
45
- #耕种者序列
43
+ 示例内容复盘
44
+ #测试标签
45
+ #内容分析
46
46
  2026-03-18 20:01
47
47
  切换笔记
48
48
  笔记诊断
@@ -86,9 +86,9 @@ describe('xiaohongshu creator-note-detail', () => {
86
86
  6
87
87
  粉丝占比 0%`;
88
88
 
89
- expect(parseCreatorNoteDetailText(bodyText, '69ba940500000000200384db')).toEqual([
90
- { section: '笔记信息', metric: 'note_id', value: '69ba940500000000200384db', extra: '' },
91
- { section: '笔记信息', metric: 'title', value: '一张图讲清 诡秘之主·耕种者途径', extra: '' },
89
+ expect(parseCreatorNoteDetailText(bodyText, 'cccccccccccccccccccccccc')).toEqual([
90
+ { section: '笔记信息', metric: 'note_id', value: 'cccccccccccccccccccccccc', extra: '' },
91
+ { section: '笔记信息', metric: 'title', value: '示例内容复盘', extra: '' },
92
92
  { section: '笔记信息', metric: 'published_at', value: '2026-03-18 20:01', extra: '' },
93
93
  { section: '基础数据', metric: '曝光数', value: '1733', extra: '粉丝占比 6.6%' },
94
94
  { section: '基础数据', metric: '观看数', value: '544', extra: '粉丝占比 7.2%' },
@@ -104,8 +104,8 @@ describe('xiaohongshu creator-note-detail', () => {
104
104
 
105
105
  it('parses structured note detail dom data into rows', () => {
106
106
  expect(parseCreatorNoteDetailDomData({
107
- title: '神雕侠侣战力金字塔',
108
- infoText: '神雕侠侣战力金字塔\n#武侠\n2025-12-04 19:45\n切换笔记',
107
+ title: '测试笔记一',
108
+ infoText: '测试笔记一\n#测试标签\n2025-12-04 19:45\n切换笔记',
109
109
  sections: [
110
110
  {
111
111
  title: '基础数据',
@@ -127,9 +127,9 @@ describe('xiaohongshu creator-note-detail', () => {
127
127
  ],
128
128
  },
129
129
  ],
130
- }, '693155fc000000000d03b42c')).toEqual([
131
- { section: '笔记信息', metric: 'note_id', value: '693155fc000000000d03b42c', extra: '' },
132
- { section: '笔记信息', metric: 'title', value: '神雕侠侣战力金字塔', extra: '' },
130
+ }, 'bbbbbbbbbbbbbbbbbbbbbbbb')).toEqual([
131
+ { section: '笔记信息', metric: 'note_id', value: 'bbbbbbbbbbbbbbbbbbbbbbbb', extra: '' },
132
+ { section: '笔记信息', metric: 'title', value: '测试笔记一', extra: '' },
133
133
  { section: '笔记信息', metric: 'published_at', value: '2025-12-04 19:45', extra: '' },
134
134
  { section: '基础数据', metric: '曝光数', value: '898204', extra: '粉丝占比 0.5%' },
135
135
  { section: '基础数据', metric: '观看数', value: '148284', extra: '粉丝占比 0.6%' },
@@ -7,14 +7,14 @@ import './creator-notes-summary.js';
7
7
  describe('xiaohongshu creator-notes-summary', () => {
8
8
  it('summarizes note list row and detail rows into one compact row', () => {
9
9
  const note: CreatorNoteRow = {
10
- id: '69ba940500000000200384db',
11
- title: '一张图讲清 诡秘之主·耕种者途径',
10
+ id: 'cccccccccccccccccccccccc',
11
+ title: '示例内容复盘',
12
12
  date: '2026年03月18日 20:01',
13
13
  views: 549,
14
14
  likes: 19,
15
15
  collects: 10,
16
16
  comments: 7,
17
- url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=69ba940500000000200384db',
17
+ url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=cccccccccccccccccccccccc',
18
18
  };
19
19
 
20
20
  const rows: CreatorNoteDetailRow[] = [
@@ -34,8 +34,8 @@ describe('xiaohongshu creator-notes-summary', () => {
34
34
 
35
35
  expect(summarizeCreatorNote(note, rows, 1)).toEqual({
36
36
  rank: 1,
37
- id: '69ba940500000000200384db',
38
- title: '一张图讲清 诡秘之主·耕种者途径',
37
+ id: 'cccccccccccccccccccccccc',
38
+ title: '示例内容复盘',
39
39
  published_at: '2026-03-18 20:01',
40
40
  views: '549',
41
41
  likes: '19',
@@ -48,7 +48,7 @@ describe('xiaohongshu creator-notes-summary', () => {
48
48
  top_source_pct: '89.9%',
49
49
  top_interest: '二次元',
50
50
  top_interest_pct: '13%',
51
- url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=69ba940500000000200384db',
51
+ url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=cccccccccccccccccccccccc',
52
52
  });
53
53
  });
54
54
  });
@@ -46,7 +46,7 @@ describe('xiaohongshu creator-notes', () => {
46
46
  const bodyText = `笔记管理
47
47
  全部笔记(366)
48
48
  已发布
49
- 神雕侠侣战力金字塔
49
+ 测试笔记一
50
50
  发布于 2025年12月04日 19:45
51
51
  148208
52
52
  324
@@ -58,7 +58,7 @@ describe('xiaohongshu creator-notes', () => {
58
58
  编辑
59
59
  删除
60
60
  仅自己可见
61
- 终于等到了!!!
61
+ 测试笔记二
62
62
  发布于 2026年03月18日 12:39
63
63
  10
64
64
  0
@@ -70,7 +70,7 @@ describe('xiaohongshu creator-notes', () => {
70
70
  expect(parseCreatorNotesText(bodyText)).toEqual([
71
71
  {
72
72
  id: '',
73
- title: '神雕侠侣战力金字塔',
73
+ title: '测试笔记一',
74
74
  date: '2025年12月04日 19:45',
75
75
  views: 148208,
76
76
  likes: 2279,
@@ -80,7 +80,7 @@ describe('xiaohongshu creator-notes', () => {
80
80
  },
81
81
  {
82
82
  id: '',
83
- title: '终于等到了!!!',
83
+ title: '测试笔记二',
84
84
  date: '2026年03月18日 12:39',
85
85
  views: 10,
86
86
  likes: 0,
@@ -106,7 +106,7 @@ describe('xiaohongshu creator-notes', () => {
106
106
  4
107
107
  5
108
108
  权限设置`,
109
- html: '&quot;noteId&quot;:&quot;69ba940500000000200384db&quot;',
109
+ html: '&quot;noteId&quot;:&quot;aaaaaaaaaaaaaaaaaaaaaaaa&quot;',
110
110
  },
111
111
  ]);
112
112
 
@@ -116,14 +116,14 @@ describe('xiaohongshu creator-notes', () => {
116
116
  expect(result).toEqual([
117
117
  {
118
118
  rank: 1,
119
- id: '69ba940500000000200384db',
119
+ id: 'aaaaaaaaaaaaaaaaaaaaaaaa',
120
120
  title: '示例笔记',
121
121
  date: '2026年03月19日 12:00',
122
122
  views: 10,
123
123
  likes: 3,
124
124
  collects: 4,
125
125
  comments: 2,
126
- url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=69ba940500000000200384db',
126
+ url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=aaaaaaaaaaaaaaaaaaaaaaaa',
127
127
  },
128
128
  ]);
129
129
  });
@@ -136,8 +136,8 @@ describe('xiaohongshu creator-notes', () => {
136
136
  undefined,
137
137
  [
138
138
  {
139
- id: '693155fc000000000d03b42c',
140
- title: '神雕侠侣战力金字塔',
139
+ id: 'bbbbbbbbbbbbbbbbbbbbbbbb',
140
+ title: '测试笔记一',
141
141
  date: '2025年12月04日 19:45',
142
142
  metrics: [148284, 319, 2280, 466, 33],
143
143
  },
@@ -149,14 +149,14 @@ describe('xiaohongshu creator-notes', () => {
149
149
  expect(result).toEqual([
150
150
  {
151
151
  rank: 1,
152
- id: '693155fc000000000d03b42c',
153
- title: '神雕侠侣战力金字塔',
152
+ id: 'bbbbbbbbbbbbbbbbbbbbbbbb',
153
+ title: '测试笔记一',
154
154
  date: '2025年12月04日 19:45',
155
155
  views: 148284,
156
156
  likes: 2280,
157
157
  collects: 466,
158
158
  comments: 319,
159
- url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=693155fc000000000d03b42c',
159
+ url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=bbbbbbbbbbbbbbbbbbbbbbbb',
160
160
  },
161
161
  ]);
162
162
  });
@@ -169,8 +169,8 @@ describe('xiaohongshu creator-notes', () => {
169
169
  data: {
170
170
  note_infos: [
171
171
  {
172
- id: '69ba940500000000200384db',
173
- title: '一张图讲清 诡秘之主·耕种者途径',
172
+ id: 'cccccccccccccccccccccccc',
173
+ title: '示例内容复盘',
174
174
  post_time: new Date('2026-03-18T20:01:00+08:00').getTime(),
175
175
  read_count: 521,
176
176
  like_count: 18,
@@ -187,28 +187,28 @@ describe('xiaohongshu creator-notes', () => {
187
187
  expect(result).toEqual([
188
188
  {
189
189
  rank: 1,
190
- id: '69ba940500000000200384db',
191
- title: '一张图讲清 诡秘之主·耕种者途径',
190
+ id: 'cccccccccccccccccccccccc',
191
+ title: '示例内容复盘',
192
192
  date: '2026年03月18日 20:01',
193
193
  views: 521,
194
194
  likes: 18,
195
195
  collects: 10,
196
196
  comments: 7,
197
- url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=69ba940500000000200384db',
197
+ url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=cccccccccccccccccccccccc',
198
198
  },
199
199
  ]);
200
200
  });
201
201
 
202
202
  it('extracts note ids from creator note-manager html', () => {
203
203
  const html = `
204
- <div>&quot;noteId&quot;:&quot;69ba940500000000200384db&quot;</div>
205
- <div>&quot;noteId&quot;:&quot;69ba2c98000000001a026e0f&quot;</div>
206
- <div>&quot;noteId&quot;:&quot;69ba940500000000200384db&quot;</div>
204
+ <div>&quot;noteId&quot;:&quot;aaaaaaaaaaaaaaaaaaaaaaaa&quot;</div>
205
+ <div>&quot;noteId&quot;:&quot;dddddddddddddddddddddddd&quot;</div>
206
+ <div>&quot;noteId&quot;:&quot;aaaaaaaaaaaaaaaaaaaaaaaa&quot;</div>
207
207
  `;
208
208
 
209
209
  expect(parseCreatorNoteIdsFromHtml(html)).toEqual([
210
- '69ba940500000000200384db',
211
- '69ba2c98000000001a026e0f',
210
+ 'aaaaaaaaaaaaaaaaaaaaaaaa',
211
+ 'dddddddddddddddddddddddd',
212
212
  ]);
213
213
  });
214
214
  });