@jackwener/opencli 1.5.7 → 1.5.9

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 (199) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +17 -1
  3. package/README.zh-CN.md +17 -1
  4. package/dist/browser/base-page.d.ts +48 -0
  5. package/dist/browser/base-page.js +160 -0
  6. package/dist/browser/cdp.js +4 -106
  7. package/dist/browser/daemon-client.d.ts +1 -7
  8. package/dist/browser/daemon-client.js +2 -9
  9. package/dist/browser/discover.d.ts +1 -4
  10. package/dist/browser/discover.js +1 -4
  11. package/dist/browser/errors.d.ts +4 -0
  12. package/dist/browser/errors.js +20 -0
  13. package/dist/browser/index.d.ts +1 -1
  14. package/dist/browser/index.js +1 -1
  15. package/dist/browser/page.d.ts +6 -35
  16. package/dist/browser/page.js +10 -189
  17. package/dist/browser/tabs.js +5 -5
  18. package/dist/browser.test.js +15 -15
  19. package/dist/cli-manifest.json +294 -22
  20. package/dist/clis/amazon/bestsellers.d.ts +21 -0
  21. package/dist/clis/amazon/bestsellers.js +130 -0
  22. package/dist/clis/amazon/bestsellers.test.js +20 -0
  23. package/dist/clis/amazon/discussion.d.ts +20 -0
  24. package/dist/clis/amazon/discussion.js +91 -0
  25. package/dist/clis/amazon/discussion.test.js +36 -0
  26. package/dist/clis/amazon/offer.d.ts +23 -0
  27. package/dist/clis/amazon/offer.js +140 -0
  28. package/dist/clis/amazon/offer.test.d.ts +1 -0
  29. package/dist/clis/amazon/offer.test.js +29 -0
  30. package/dist/clis/amazon/product.d.ts +18 -0
  31. package/dist/clis/amazon/product.js +92 -0
  32. package/dist/clis/amazon/product.test.d.ts +1 -0
  33. package/dist/clis/amazon/product.test.js +24 -0
  34. package/dist/clis/amazon/search.d.ts +18 -0
  35. package/dist/clis/amazon/search.js +87 -0
  36. package/dist/clis/amazon/search.test.d.ts +1 -0
  37. package/dist/clis/amazon/search.test.js +22 -0
  38. package/dist/clis/amazon/shared.d.ts +64 -0
  39. package/dist/clis/amazon/shared.js +255 -0
  40. package/dist/clis/amazon/shared.test.d.ts +1 -0
  41. package/dist/clis/amazon/shared.test.js +33 -0
  42. package/dist/clis/gemini/ask.d.ts +1 -0
  43. package/dist/clis/gemini/ask.js +40 -0
  44. package/dist/clis/gemini/image.d.ts +1 -0
  45. package/dist/clis/gemini/image.js +105 -0
  46. package/dist/clis/gemini/new.d.ts +1 -0
  47. package/dist/clis/gemini/new.js +20 -0
  48. package/dist/clis/gemini/utils.d.ts +34 -0
  49. package/dist/clis/gemini/utils.js +463 -0
  50. package/dist/clis/gemini/utils.test.d.ts +1 -0
  51. package/dist/clis/gemini/utils.test.js +31 -0
  52. package/dist/clis/notebooklm/compat.test.d.ts +1 -1
  53. package/dist/clis/notebooklm/compat.test.js +3 -3
  54. package/dist/clis/notebooklm/current.js +2 -3
  55. package/dist/clis/notebooklm/get.js +2 -3
  56. package/dist/clis/notebooklm/history.js +2 -3
  57. package/dist/clis/notebooklm/note-list.js +2 -3
  58. package/dist/clis/notebooklm/notes-get.js +2 -3
  59. package/dist/clis/notebooklm/open.d.ts +1 -0
  60. package/dist/clis/notebooklm/open.js +41 -0
  61. package/dist/clis/notebooklm/open.test.d.ts +1 -0
  62. package/dist/clis/notebooklm/open.test.js +63 -0
  63. package/dist/clis/notebooklm/source-fulltext.js +2 -3
  64. package/dist/clis/notebooklm/source-get.js +2 -3
  65. package/dist/clis/notebooklm/source-guide.js +2 -3
  66. package/dist/clis/notebooklm/source-list.js +2 -3
  67. package/dist/clis/notebooklm/status.js +1 -2
  68. package/dist/clis/notebooklm/summary.js +2 -3
  69. package/dist/clis/notebooklm/utils.d.ts +2 -1
  70. package/dist/clis/notebooklm/utils.js +20 -21
  71. package/dist/clis/xiaohongshu/creator-note-detail.test.js +11 -11
  72. package/dist/clis/xiaohongshu/creator-notes-summary.test.js +6 -6
  73. package/dist/clis/xiaohongshu/creator-notes.test.js +22 -22
  74. package/dist/commanderAdapter.js +6 -3
  75. package/dist/commanderAdapter.test.js +33 -0
  76. package/dist/commands/daemon.js +1 -1
  77. package/dist/commands/daemon.test.js +1 -1
  78. package/dist/doctor.d.ts +1 -2
  79. package/dist/doctor.js +7 -8
  80. package/dist/explore.js +1 -1
  81. package/dist/extension-manifest-regression.test.js +1 -0
  82. package/dist/output.js +28 -0
  83. package/dist/output.test.js +15 -0
  84. package/dist/pipeline/executor.js +2 -7
  85. package/dist/pipeline/steps/browser.js +1 -1
  86. package/dist/pipeline/template.js +25 -3
  87. package/dist/record.d.ts +50 -0
  88. package/dist/record.js +298 -57
  89. package/dist/record.test.d.ts +1 -0
  90. package/dist/record.test.js +293 -0
  91. package/dist/registry.d.ts +2 -0
  92. package/dist/registry.js +1 -0
  93. package/dist/registry.test.js +10 -0
  94. package/dist/runtime.js +3 -3
  95. package/dist/snapshotFormatter.d.ts +1 -1
  96. package/dist/snapshotFormatter.js +4 -4
  97. package/dist/snapshotFormatter.test.d.ts +1 -1
  98. package/dist/snapshotFormatter.test.js +2 -2
  99. package/dist/types.d.ts +3 -1
  100. package/dist/types.js +1 -1
  101. package/docs/.vitepress/config.mts +2 -0
  102. package/docs/adapters/browser/amazon.md +53 -0
  103. package/docs/adapters/browser/gemini.md +72 -0
  104. package/docs/adapters/browser/notebooklm.md +5 -5
  105. package/docs/adapters/index.md +3 -1
  106. package/extension/dist/background.js +614 -794
  107. package/extension/manifest.json +2 -1
  108. package/extension/src/background.test.ts +7 -163
  109. package/extension/src/background.ts +7 -156
  110. package/extension/src/cdp.test.ts +75 -0
  111. package/extension/src/cdp.ts +77 -3
  112. package/extension/src/protocol.ts +1 -5
  113. package/package.json +1 -1
  114. package/skills/opencli-explorer/SKILL.md +847 -0
  115. package/skills/opencli-oneshot/SKILL.md +216 -0
  116. package/skills/opencli-usage/SKILL.md +71 -0
  117. package/skills/opencli-usage/browser.md +429 -0
  118. package/skills/opencli-usage/desktop.md +118 -0
  119. package/skills/opencli-usage/plugins.md +82 -0
  120. package/skills/opencli-usage/public-api.md +149 -0
  121. package/src/browser/base-page.ts +197 -0
  122. package/src/browser/cdp.ts +7 -131
  123. package/src/browser/daemon-client.ts +3 -14
  124. package/src/browser/discover.ts +1 -4
  125. package/src/browser/errors.ts +22 -0
  126. package/src/browser/index.ts +1 -1
  127. package/src/browser/page.ts +13 -212
  128. package/src/browser/tabs.ts +5 -5
  129. package/src/browser.test.ts +15 -15
  130. package/src/clis/amazon/bestsellers.test.ts +22 -0
  131. package/src/clis/amazon/bestsellers.ts +180 -0
  132. package/src/clis/amazon/discussion.test.ts +38 -0
  133. package/src/clis/amazon/discussion.ts +131 -0
  134. package/src/clis/amazon/offer.test.ts +35 -0
  135. package/src/clis/amazon/offer.ts +185 -0
  136. package/src/clis/amazon/product.test.ts +26 -0
  137. package/src/clis/amazon/product.ts +131 -0
  138. package/src/clis/amazon/search.test.ts +24 -0
  139. package/src/clis/amazon/search.ts +128 -0
  140. package/src/clis/amazon/shared.test.ts +37 -0
  141. package/src/clis/amazon/shared.ts +316 -0
  142. package/src/clis/gemini/ask.ts +46 -0
  143. package/src/clis/gemini/image.ts +115 -0
  144. package/src/clis/gemini/new.ts +22 -0
  145. package/src/clis/gemini/utils.test.ts +36 -0
  146. package/src/clis/gemini/utils.ts +523 -0
  147. package/src/clis/notebooklm/compat.test.ts +3 -3
  148. package/src/clis/notebooklm/current.ts +2 -3
  149. package/src/clis/notebooklm/get.ts +1 -3
  150. package/src/clis/notebooklm/history.ts +1 -3
  151. package/src/clis/notebooklm/note-list.ts +1 -3
  152. package/src/clis/notebooklm/notes-get.ts +1 -3
  153. package/src/clis/notebooklm/open.test.ts +78 -0
  154. package/src/clis/notebooklm/open.ts +61 -0
  155. package/src/clis/notebooklm/source-fulltext.ts +1 -3
  156. package/src/clis/notebooklm/source-get.ts +1 -3
  157. package/src/clis/notebooklm/source-guide.ts +1 -3
  158. package/src/clis/notebooklm/source-list.ts +1 -3
  159. package/src/clis/notebooklm/status.ts +1 -2
  160. package/src/clis/notebooklm/summary.ts +1 -3
  161. package/src/clis/notebooklm/utils.ts +29 -20
  162. package/src/clis/xiaohongshu/creator-note-detail.test.ts +11 -11
  163. package/src/clis/xiaohongshu/creator-notes-summary.test.ts +6 -6
  164. package/src/clis/xiaohongshu/creator-notes.test.ts +22 -22
  165. package/src/commanderAdapter.test.ts +47 -0
  166. package/src/commanderAdapter.ts +7 -3
  167. package/src/commands/daemon.test.ts +1 -1
  168. package/src/commands/daemon.ts +1 -1
  169. package/src/doctor.ts +7 -8
  170. package/src/explore.ts +1 -1
  171. package/src/extension-manifest-regression.test.ts +1 -0
  172. package/src/output.test.ts +17 -0
  173. package/src/output.ts +27 -0
  174. package/src/pipeline/executor.ts +2 -7
  175. package/src/pipeline/steps/browser.ts +1 -1
  176. package/src/pipeline/template.ts +27 -4
  177. package/src/record.test.ts +362 -0
  178. package/src/record.ts +341 -62
  179. package/src/registry.test.ts +12 -0
  180. package/src/registry.ts +3 -0
  181. package/src/runtime.ts +3 -3
  182. package/src/snapshotFormatter.test.ts +2 -2
  183. package/src/snapshotFormatter.ts +4 -4
  184. package/src/types.ts +3 -1
  185. package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
  186. package/.agents/workflows/cross-project-adapter-migration.md +0 -54
  187. package/SKILL.md +0 -879
  188. package/dist/clis/notebooklm/bind-current.js +0 -29
  189. package/dist/clis/notebooklm/bind-current.test.d.ts +0 -1
  190. package/dist/clis/notebooklm/bind-current.test.js +0 -35
  191. package/dist/clis/notebooklm/binding.test.js +0 -44
  192. package/src/clis/notebooklm/bind-current.test.ts +0 -43
  193. package/src/clis/notebooklm/bind-current.ts +0 -36
  194. package/src/clis/notebooklm/binding.test.ts +0 -53
  195. /package/dist/browser/{mcp.d.ts → bridge.d.ts} +0 -0
  196. /package/dist/browser/{mcp.js → bridge.js} +0 -0
  197. /package/dist/clis/{notebooklm/bind-current.d.ts → amazon/bestsellers.test.d.ts} +0 -0
  198. /package/dist/clis/{notebooklm/binding.test.d.ts → amazon/discussion.test.d.ts} +0 -0
  199. /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;
@@ -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
  });
@@ -153,3 +153,50 @@ describe('commanderAdapter command aliases', () => {
153
153
  expect(mockExecuteCommand).toHaveBeenCalledWith(cmd, {}, false);
154
154
  });
155
155
  });
156
+
157
+ describe('commanderAdapter default formats', () => {
158
+ const cmd: CliCommand = {
159
+ site: 'gemini',
160
+ name: 'ask',
161
+ description: 'Ask Gemini',
162
+ browser: false,
163
+ args: [],
164
+ columns: ['response'],
165
+ defaultFormat: 'plain',
166
+ func: vi.fn(),
167
+ };
168
+
169
+ beforeEach(() => {
170
+ mockExecuteCommand.mockReset();
171
+ mockExecuteCommand.mockResolvedValue([{ response: 'hello' }]);
172
+ mockRenderOutput.mockReset();
173
+ delete process.env.OPENCLI_VERBOSE;
174
+ process.exitCode = undefined;
175
+ });
176
+
177
+ it('uses the command defaultFormat when the user keeps the default table format', async () => {
178
+ const program = new Command();
179
+ const siteCmd = program.command('gemini');
180
+ registerCommandToProgram(siteCmd, cmd);
181
+
182
+ await program.parseAsync(['node', 'opencli', 'gemini', 'ask']);
183
+
184
+ expect(mockRenderOutput).toHaveBeenCalledWith(
185
+ [{ response: 'hello' }],
186
+ expect.objectContaining({ fmt: 'plain' }),
187
+ );
188
+ });
189
+
190
+ it('respects an explicit user format over the command defaultFormat', async () => {
191
+ const program = new Command();
192
+ const siteCmd = program.command('gemini');
193
+ registerCommandToProgram(siteCmd, cmd);
194
+
195
+ await program.parseAsync(['node', 'opencli', 'gemini', 'ask', '--format', 'json']);
196
+
197
+ expect(mockRenderOutput).toHaveBeenCalledWith(
198
+ [{ response: 'hello' }],
199
+ expect.objectContaining({ fmt: 'json' }),
200
+ );
201
+ });
202
+ });
@@ -69,7 +69,7 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
69
69
  }
70
70
  }
71
71
  subCmd
72
- .option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
72
+ .option('-f, --format <fmt>', 'Output format: table, plain, json, yaml, md, csv', 'table')
73
73
  .option('-v, --verbose', 'Debug output', false);
74
74
 
75
75
  subCmd.addHelpText('after', formatRegistryHelpText(cmd));
@@ -95,7 +95,7 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
95
95
  }
96
96
 
97
97
  const verbose = optionsRecord.verbose === true;
98
- const format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
98
+ let format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
99
99
  if (verbose) process.env.OPENCLI_VERBOSE = '1';
100
100
  if (cmd.deprecated) {
101
101
  const message = typeof cmd.deprecated === 'string' ? cmd.deprecated : `${fullName(cmd)} is deprecated.`;
@@ -108,10 +108,14 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
108
108
  return;
109
109
  }
110
110
 
111
+ const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
112
+ if (format === 'table' && resolved.defaultFormat) {
113
+ format = resolved.defaultFormat;
114
+ }
115
+
111
116
  if (verbose && (!result || (Array.isArray(result) && result.length === 0))) {
112
117
  console.error(chalk.yellow('[Verbose] Warning: Command returned an empty result.'));
113
118
  }
114
- const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
115
119
  renderOutput(result, {
116
120
  fmt: format,
117
121
  columns: resolved.columns,
@@ -10,7 +10,7 @@ vi.mock('chalk', () => ({
10
10
  }));
11
11
 
12
12
  const mockConnect = vi.fn();
13
- vi.mock('../browser/mcp.js', () => ({
13
+ vi.mock('../browser/bridge.js', () => ({
14
14
  BrowserBridge: class {
15
15
  connect = mockConnect;
16
16
  },