@jackwener/opencli 1.5.8 โ†’ 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 (194) hide show
  1. package/CHANGELOG.md +21 -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/output.js +28 -0
  82. package/dist/output.test.js +15 -0
  83. package/dist/pipeline/executor.js +2 -7
  84. package/dist/pipeline/steps/browser.js +1 -1
  85. package/dist/pipeline/template.js +25 -3
  86. package/dist/record.d.ts +50 -0
  87. package/dist/record.js +298 -57
  88. package/dist/record.test.d.ts +1 -0
  89. package/dist/record.test.js +293 -0
  90. package/dist/registry.d.ts +2 -0
  91. package/dist/registry.js +1 -0
  92. package/dist/registry.test.js +10 -0
  93. package/dist/runtime.js +3 -3
  94. package/dist/snapshotFormatter.d.ts +1 -1
  95. package/dist/snapshotFormatter.js +4 -4
  96. package/dist/snapshotFormatter.test.d.ts +1 -1
  97. package/dist/snapshotFormatter.test.js +2 -2
  98. package/dist/types.d.ts +3 -1
  99. package/dist/types.js +1 -1
  100. package/docs/.vitepress/config.mts +2 -0
  101. package/docs/adapters/browser/amazon.md +53 -0
  102. package/docs/adapters/browser/gemini.md +72 -0
  103. package/docs/adapters/browser/notebooklm.md +5 -5
  104. package/docs/adapters/index.md +3 -1
  105. package/extension/dist/background.js +5 -143
  106. package/extension/src/background.test.ts +7 -163
  107. package/extension/src/background.ts +7 -157
  108. package/extension/src/protocol.ts +1 -5
  109. package/package.json +1 -1
  110. package/skills/opencli-explorer/SKILL.md +847 -0
  111. package/skills/opencli-oneshot/SKILL.md +216 -0
  112. package/skills/opencli-usage/SKILL.md +71 -0
  113. package/skills/opencli-usage/browser.md +429 -0
  114. package/skills/opencli-usage/desktop.md +118 -0
  115. package/skills/opencli-usage/plugins.md +82 -0
  116. package/skills/opencli-usage/public-api.md +149 -0
  117. package/src/browser/base-page.ts +197 -0
  118. package/src/browser/cdp.ts +7 -131
  119. package/src/browser/daemon-client.ts +3 -14
  120. package/src/browser/discover.ts +1 -4
  121. package/src/browser/errors.ts +22 -0
  122. package/src/browser/index.ts +1 -1
  123. package/src/browser/page.ts +13 -212
  124. package/src/browser/tabs.ts +5 -5
  125. package/src/browser.test.ts +15 -15
  126. package/src/clis/amazon/bestsellers.test.ts +22 -0
  127. package/src/clis/amazon/bestsellers.ts +180 -0
  128. package/src/clis/amazon/discussion.test.ts +38 -0
  129. package/src/clis/amazon/discussion.ts +131 -0
  130. package/src/clis/amazon/offer.test.ts +35 -0
  131. package/src/clis/amazon/offer.ts +185 -0
  132. package/src/clis/amazon/product.test.ts +26 -0
  133. package/src/clis/amazon/product.ts +131 -0
  134. package/src/clis/amazon/search.test.ts +24 -0
  135. package/src/clis/amazon/search.ts +128 -0
  136. package/src/clis/amazon/shared.test.ts +37 -0
  137. package/src/clis/amazon/shared.ts +316 -0
  138. package/src/clis/gemini/ask.ts +46 -0
  139. package/src/clis/gemini/image.ts +115 -0
  140. package/src/clis/gemini/new.ts +22 -0
  141. package/src/clis/gemini/utils.test.ts +36 -0
  142. package/src/clis/gemini/utils.ts +523 -0
  143. package/src/clis/notebooklm/compat.test.ts +3 -3
  144. package/src/clis/notebooklm/current.ts +2 -3
  145. package/src/clis/notebooklm/get.ts +1 -3
  146. package/src/clis/notebooklm/history.ts +1 -3
  147. package/src/clis/notebooklm/note-list.ts +1 -3
  148. package/src/clis/notebooklm/notes-get.ts +1 -3
  149. package/src/clis/notebooklm/open.test.ts +78 -0
  150. package/src/clis/notebooklm/open.ts +61 -0
  151. package/src/clis/notebooklm/source-fulltext.ts +1 -3
  152. package/src/clis/notebooklm/source-get.ts +1 -3
  153. package/src/clis/notebooklm/source-guide.ts +1 -3
  154. package/src/clis/notebooklm/source-list.ts +1 -3
  155. package/src/clis/notebooklm/status.ts +1 -2
  156. package/src/clis/notebooklm/summary.ts +1 -3
  157. package/src/clis/notebooklm/utils.ts +29 -20
  158. package/src/clis/xiaohongshu/creator-note-detail.test.ts +11 -11
  159. package/src/clis/xiaohongshu/creator-notes-summary.test.ts +6 -6
  160. package/src/clis/xiaohongshu/creator-notes.test.ts +22 -22
  161. package/src/commanderAdapter.test.ts +47 -0
  162. package/src/commanderAdapter.ts +7 -3
  163. package/src/commands/daemon.test.ts +1 -1
  164. package/src/commands/daemon.ts +1 -1
  165. package/src/doctor.ts +7 -8
  166. package/src/explore.ts +1 -1
  167. package/src/output.test.ts +17 -0
  168. package/src/output.ts +27 -0
  169. package/src/pipeline/executor.ts +2 -7
  170. package/src/pipeline/steps/browser.ts +1 -1
  171. package/src/pipeline/template.ts +27 -4
  172. package/src/record.test.ts +362 -0
  173. package/src/record.ts +341 -62
  174. package/src/registry.test.ts +12 -0
  175. package/src/registry.ts +3 -0
  176. package/src/runtime.ts +3 -3
  177. package/src/snapshotFormatter.test.ts +2 -2
  178. package/src/snapshotFormatter.ts +4 -4
  179. package/src/types.ts +3 -1
  180. package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
  181. package/.agents/workflows/cross-project-adapter-migration.md +0 -54
  182. package/SKILL.md +0 -879
  183. package/dist/clis/notebooklm/bind-current.js +0 -29
  184. package/dist/clis/notebooklm/bind-current.test.d.ts +0 -1
  185. package/dist/clis/notebooklm/bind-current.test.js +0 -35
  186. package/dist/clis/notebooklm/binding.test.js +0 -44
  187. package/src/clis/notebooklm/bind-current.test.ts +0 -43
  188. package/src/clis/notebooklm/bind-current.ts +0 -36
  189. package/src/clis/notebooklm/binding.test.ts +0 -53
  190. /package/dist/browser/{mcp.d.ts โ†’ bridge.d.ts} +0 -0
  191. /package/dist/browser/{mcp.js โ†’ bridge.js} +0 -0
  192. /package/dist/clis/{notebooklm/bind-current.d.ts โ†’ amazon/bestsellers.test.d.ts} +0 -0
  193. /package/dist/clis/{notebooklm/binding.test.d.ts โ†’ amazon/discussion.test.d.ts} +0 -0
  194. /package/src/browser/{mcp.ts โ†’ bridge.ts} +0 -0
@@ -0,0 +1,293 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { analyzeRecordedRequests, buildWriteRecordedYaml, createRecordedEntry, generateFullCaptureInterceptorJs, generateRecordedCandidates, } from './record.js';
3
+ import { render } from './pipeline/template.js';
4
+ describe('record request-body capture', () => {
5
+ it('captures a JSON fetch request body alongside the JSON response body', () => {
6
+ const entry = createRecordedEntry({
7
+ url: 'https://api.example.com/tasks',
8
+ method: 'POST',
9
+ requestContentType: 'application/json',
10
+ requestBodyText: '{"title":"Ship #601","priority":"high"}',
11
+ responseBody: { id: 'task_123', ok: true },
12
+ });
13
+ expect(entry).toMatchObject({
14
+ method: 'POST',
15
+ requestContentType: 'application/json',
16
+ requestBody: { title: 'Ship #601', priority: 'high' },
17
+ responseBody: { id: 'task_123', ok: true },
18
+ });
19
+ });
20
+ it('captures a JSON request body from fetch(Request)', async () => {
21
+ class MockXMLHttpRequest {
22
+ open() { }
23
+ send() { }
24
+ setRequestHeader() { }
25
+ addEventListener() { }
26
+ getResponseHeader() { return null; }
27
+ responseText = '';
28
+ }
29
+ const mockFetch = vi.fn(async () => new Response(JSON.stringify({ id: 'task_123', ok: true }), { headers: { 'content-type': 'application/json' } }));
30
+ vi.stubGlobal('fetch', mockFetch);
31
+ vi.stubGlobal('XMLHttpRequest', MockXMLHttpRequest);
32
+ vi.stubGlobal('window', globalThis);
33
+ // eslint-disable-next-line no-eval
34
+ eval(generateFullCaptureInterceptorJs());
35
+ const request = new Request('https://api.example.com/tasks', {
36
+ method: 'POST',
37
+ headers: { 'content-type': 'application/json' },
38
+ body: JSON.stringify({ title: 'Ship #601' }),
39
+ });
40
+ await globalThis.fetch(request);
41
+ const recorded = globalThis.__opencli_record;
42
+ expect(recorded).toHaveLength(1);
43
+ expect(recorded?.[0]?.requestBody).toEqual({ title: 'Ship #601' });
44
+ });
45
+ it('captures a JSON request body from XHR send()', async () => {
46
+ class MockXMLHttpRequest {
47
+ __listeners = {};
48
+ __rec_url;
49
+ __rec_method;
50
+ __rec_request_content_type;
51
+ responseText = JSON.stringify({ id: 'task_456', ok: true });
52
+ open(method, url) {
53
+ this.__rec_method = method;
54
+ this.__rec_url = url;
55
+ }
56
+ send() {
57
+ for (const listener of this.__listeners.load ?? [])
58
+ listener.call(this);
59
+ }
60
+ setRequestHeader(name, value) {
61
+ if (name.toLowerCase() === 'content-type')
62
+ this.__rec_request_content_type = value;
63
+ }
64
+ addEventListener(event, listener) {
65
+ this.__listeners[event] ??= [];
66
+ this.__listeners[event].push(listener);
67
+ }
68
+ getResponseHeader(name) {
69
+ return name.toLowerCase() === 'content-type' ? 'application/json' : null;
70
+ }
71
+ }
72
+ const mockFetch = vi.fn(async () => new Response(JSON.stringify({ ok: true }), { headers: { 'content-type': 'application/json' } }));
73
+ vi.stubGlobal('fetch', mockFetch);
74
+ vi.stubGlobal('XMLHttpRequest', MockXMLHttpRequest);
75
+ vi.stubGlobal('window', globalThis);
76
+ // eslint-disable-next-line no-eval
77
+ eval(generateFullCaptureInterceptorJs());
78
+ const xhr = new XMLHttpRequest();
79
+ xhr.open('PATCH', 'https://api.example.com/tasks/submit');
80
+ xhr.setRequestHeader('content-type', 'application/json;charset=utf-8');
81
+ xhr.send('{"done":true}');
82
+ const recorded = globalThis.__opencli_record;
83
+ expect(recorded).toHaveLength(1);
84
+ expect(recorded?.[0]?.requestBody).toEqual({ done: true });
85
+ });
86
+ it('does not interrupt fetch when reading a Request body fails', async () => {
87
+ class MockXMLHttpRequest {
88
+ open() { }
89
+ send() { }
90
+ setRequestHeader() { }
91
+ addEventListener() { }
92
+ getResponseHeader() { return null; }
93
+ responseText = '';
94
+ }
95
+ class BrokenRequest extends Request {
96
+ clone() {
97
+ throw new Error('clone failed');
98
+ }
99
+ }
100
+ const mockFetch = vi.fn(async () => new Response(JSON.stringify({ id: 'task_123', ok: true }), { headers: { 'content-type': 'application/json' } }));
101
+ vi.stubGlobal('fetch', mockFetch);
102
+ vi.stubGlobal('XMLHttpRequest', MockXMLHttpRequest);
103
+ vi.stubGlobal('window', globalThis);
104
+ // eslint-disable-next-line no-eval
105
+ eval(generateFullCaptureInterceptorJs());
106
+ const request = new BrokenRequest('https://api.example.com/tasks', {
107
+ method: 'POST',
108
+ headers: { 'content-type': 'application/json' },
109
+ body: JSON.stringify({ title: 'Ship #601' }),
110
+ });
111
+ await expect(globalThis.fetch(request)).resolves.toBeInstanceOf(Response);
112
+ expect(mockFetch).toHaveBeenCalledTimes(1);
113
+ });
114
+ });
115
+ describe('record write candidates', () => {
116
+ it('keeps a POST request with JSON request body and object response as a write candidate', () => {
117
+ const result = analyzeRecordedRequests([
118
+ createRecordedEntry({
119
+ url: 'https://api.example.com/tasks/create',
120
+ method: 'POST',
121
+ requestContentType: 'application/json',
122
+ requestBodyText: '{"title":"Ship #601"}',
123
+ responseBody: { id: 'task_123', ok: true },
124
+ }),
125
+ ]);
126
+ expect(result.candidates).toHaveLength(1);
127
+ expect(result.candidates[0]).toMatchObject({
128
+ kind: 'write',
129
+ req: { method: 'POST' },
130
+ });
131
+ });
132
+ it('accepts vendor JSON content types for write candidates', () => {
133
+ const result = analyzeRecordedRequests([
134
+ createRecordedEntry({
135
+ url: 'https://api.example.com/tasks',
136
+ method: 'POST',
137
+ requestContentType: 'application/vnd.api+json',
138
+ requestBodyText: '{"title":"Ship #601"}',
139
+ responseBody: { id: 'task_123', ok: true },
140
+ }),
141
+ ]);
142
+ expect(result.candidates).toHaveLength(1);
143
+ expect(result.candidates[0]).toMatchObject({
144
+ kind: 'write',
145
+ req: { method: 'POST' },
146
+ });
147
+ });
148
+ it('rejects a POST request that has no usable JSON request body', () => {
149
+ const result = analyzeRecordedRequests([
150
+ createRecordedEntry({
151
+ url: 'https://api.example.com/tasks/create',
152
+ method: 'POST',
153
+ requestContentType: 'application/json',
154
+ requestBodyText: '',
155
+ responseBody: { id: 'task_123', ok: true },
156
+ }),
157
+ ]);
158
+ expect(result.candidates).toEqual([]);
159
+ });
160
+ it('rejects array request and response bodies for first-version write candidates', () => {
161
+ const result = analyzeRecordedRequests([
162
+ createRecordedEntry({
163
+ url: 'https://api.example.com/tasks/batch',
164
+ method: 'POST',
165
+ requestContentType: 'application/json',
166
+ requestBodyText: '[{"title":"Ship #601"}]',
167
+ responseBody: [{ id: 'task_123' }],
168
+ }),
169
+ ]);
170
+ expect(result.candidates).toEqual([]);
171
+ });
172
+ it('generates a write YAML candidate from a replayable JSON write request', () => {
173
+ const candidates = generateRecordedCandidates('demo', 'https://demo.example.com/app', [
174
+ createRecordedEntry({
175
+ url: 'https://api.example.com/tasks/create',
176
+ method: 'POST',
177
+ requestContentType: 'application/json',
178
+ requestBodyText: '{"title":"Ship #601"}',
179
+ responseBody: { id: 'task_123', ok: true },
180
+ }),
181
+ ]);
182
+ expect(candidates).toHaveLength(1);
183
+ expect(candidates[0]).toMatchObject({
184
+ kind: 'write',
185
+ name: 'create',
186
+ strategy: 'cookie',
187
+ });
188
+ expect(JSON.stringify(candidates[0].yaml)).toContain('Ship #601');
189
+ });
190
+ it('builds a write template that replays the recorded JSON body with application/json', () => {
191
+ const candidate = buildWriteRecordedYaml('demo', 'https://demo.example.com/app', createRecordedEntry({
192
+ url: 'https://api.example.com/tasks/create',
193
+ method: 'POST',
194
+ requestContentType: 'application/json',
195
+ requestBodyText: '{"title":"Ship #601"}',
196
+ responseBody: { id: 'task_123', ok: true },
197
+ }), 'create');
198
+ expect(candidate.name).toBe('create');
199
+ expect(JSON.stringify(candidate.yaml)).toContain('method: \\"POST\\"');
200
+ expect(JSON.stringify(candidate.yaml)).toContain('content-type');
201
+ expect(JSON.stringify(candidate.yaml)).toContain('Ship #601');
202
+ });
203
+ });
204
+ describe('record read candidates', () => {
205
+ it('keeps existing read candidates for array responses', () => {
206
+ const result = analyzeRecordedRequests([
207
+ {
208
+ url: 'https://api.example.com/feed',
209
+ method: 'GET',
210
+ status: null,
211
+ requestContentType: null,
212
+ responseContentType: 'application/json',
213
+ requestBody: null,
214
+ responseBody: { items: [{ title: 'A' }, { title: 'B' }] },
215
+ contentType: 'application/json',
216
+ body: { items: [{ title: 'A' }, { title: 'B' }] },
217
+ capturedAt: 1,
218
+ },
219
+ ]);
220
+ expect(result.candidates).toHaveLength(1);
221
+ expect(result.candidates[0]).toMatchObject({ kind: 'read' });
222
+ });
223
+ it('keeps read YAML generation on the baseline fetch path', () => {
224
+ const candidates = generateRecordedCandidates('demo', 'https://demo.example.com/app', [
225
+ createRecordedEntry({
226
+ url: 'https://api.example.com/search?q=test',
227
+ method: 'GET',
228
+ responseBody: { items: [{ title: 'A' }, { title: 'B' }] },
229
+ }),
230
+ ]);
231
+ const yaml = candidates[0].yaml;
232
+ expect(yaml.pipeline[1]?.evaluate).toContain(`fetch("https://api.example.com/search?q=`);
233
+ expect(yaml.pipeline[1]?.evaluate).toContain(`{ credentials: 'include' }`);
234
+ expect(yaml.pipeline[1]?.evaluate).not.toContain('method: "POST"');
235
+ expect(yaml.pipeline[1]?.evaluate).not.toContain('body: JSON.stringify');
236
+ });
237
+ it('renders search and page args into the read YAML fetch URL', () => {
238
+ const candidates = generateRecordedCandidates('demo', 'https://demo.example.com/app', [
239
+ createRecordedEntry({
240
+ url: 'https://api.example.com/search?q=test&page=2',
241
+ method: 'GET',
242
+ responseBody: { items: [{ title: 'A' }, { title: 'B' }] },
243
+ }),
244
+ ]);
245
+ const yaml = candidates[0].yaml;
246
+ const renderedEvaluate = render(yaml.pipeline[1]?.evaluate, {
247
+ args: { keyword: 'desk', page: 3 },
248
+ });
249
+ expect(renderedEvaluate).toContain('https://api.example.com/search?q=desk&page=3');
250
+ });
251
+ it('keeps GET and POST candidates separate when they share the same URL pattern', () => {
252
+ const candidates = generateRecordedCandidates('demo', 'https://demo.example.com/app', [
253
+ createRecordedEntry({
254
+ url: 'https://api.example.com/tasks',
255
+ method: 'GET',
256
+ responseBody: { items: [{ title: 'A' }, { title: 'B' }] },
257
+ }),
258
+ createRecordedEntry({
259
+ url: 'https://api.example.com/tasks',
260
+ method: 'POST',
261
+ requestContentType: 'application/json',
262
+ requestBodyText: '{"title":"Ship #601"}',
263
+ responseBody: { id: 'task_123', ok: true },
264
+ }),
265
+ ]);
266
+ expect(candidates).toHaveLength(2);
267
+ expect(candidates.some((candidate) => candidate.kind === 'read')).toBe(true);
268
+ expect(candidates.some((candidate) => candidate.kind === 'write')).toBe(true);
269
+ });
270
+ });
271
+ describe('record noise filtering', () => {
272
+ it('filters analytics POST noise from write candidates', () => {
273
+ const result = analyzeRecordedRequests([
274
+ createRecordedEntry({
275
+ url: 'https://api.example.com/analytics/event',
276
+ method: 'POST',
277
+ requestContentType: 'application/json',
278
+ requestBodyText: '{"event":"click"}',
279
+ responseBody: { ok: true, accepted: 1 },
280
+ }),
281
+ ]);
282
+ expect(result.candidates).toEqual([]);
283
+ });
284
+ });
285
+ afterEach(() => {
286
+ vi.unstubAllGlobals();
287
+ Reflect.deleteProperty(globalThis, '__opencli_record');
288
+ Reflect.deleteProperty(globalThis, '__opencli_record_patched');
289
+ Reflect.deleteProperty(globalThis, '__opencli_orig_fetch');
290
+ Reflect.deleteProperty(globalThis, '__opencli_orig_xhr_open');
291
+ Reflect.deleteProperty(globalThis, '__opencli_orig_xhr_send');
292
+ Reflect.deleteProperty(globalThis, '__opencli_orig_xhr_set_request_header');
293
+ });
@@ -55,6 +55,8 @@ export interface CliCommand {
55
55
  * - `string`: navigate to this specific URL instead of the domain root
56
56
  */
57
57
  navigateBefore?: boolean | string;
58
+ /** Override the default CLI output format when the user does not pass -f/--format. */
59
+ defaultFormat?: 'table' | 'plain' | 'json' | 'yaml' | 'yml' | 'md' | 'markdown' | 'csv';
58
60
  }
59
61
  /** Internal extension for lazy-loaded TS modules (not exposed in public API) */
60
62
  export interface InternalCliCommand extends CliCommand {
package/dist/registry.js CHANGED
@@ -32,6 +32,7 @@ export function cli(opts) {
32
32
  deprecated: opts.deprecated,
33
33
  replacedBy: opts.replacedBy,
34
34
  navigateBefore: opts.navigateBefore,
35
+ defaultFormat: opts.defaultFormat,
35
36
  };
36
37
  registerCommand(cmd);
37
38
  return cmd;
@@ -62,6 +62,16 @@ describe('cli() registration', () => {
62
62
  expect(registry.get('test-registry/compat')).toBe(cmd);
63
63
  expect(registry.get('test-registry/legacy-name')).toBe(cmd);
64
64
  });
65
+ it('preserves defaultFormat on the registered command', () => {
66
+ const cmd = cli({
67
+ site: 'test-registry',
68
+ name: 'plain-default',
69
+ description: 'prefers plain output',
70
+ defaultFormat: 'plain',
71
+ });
72
+ expect(cmd.defaultFormat).toBe('plain');
73
+ expect(getRegistry().get('test-registry/plain-default')?.defaultFormat).toBe('plain');
74
+ });
65
75
  });
66
76
  describe('fullName', () => {
67
77
  it('returns site/name', () => {
package/dist/runtime.js CHANGED
@@ -46,9 +46,9 @@ export function withTimeoutMs(promise, timeoutMs, makeError = 'Operation timed o
46
46
  });
47
47
  }
48
48
  export async function browserSession(BrowserFactory, fn, opts = {}) {
49
- const mcp = new BrowserFactory();
49
+ const browser = new BrowserFactory();
50
50
  try {
51
- const page = await mcp.connect({
51
+ const page = await browser.connect({
52
52
  timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT,
53
53
  workspace: opts.workspace,
54
54
  cdpEndpoint: opts.cdpEndpoint,
@@ -56,6 +56,6 @@ export async function browserSession(BrowserFactory, fn, opts = {}) {
56
56
  return await fn(page);
57
57
  }
58
58
  finally {
59
- await mcp.close().catch(() => { });
59
+ await browser.close().catch(() => { });
60
60
  }
61
61
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Aria snapshot formatter: parses Playwright MCP snapshot text into clean format.
2
+ * Aria snapshot formatter: parses snapshot text into clean format.
3
3
  *
4
4
  * 4-pass pipeline:
5
5
  * 1. Parse & filter: strip annotations, metadata, noise, ads, boilerplate subtrees
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Aria snapshot formatter: parses Playwright MCP snapshot text into clean format.
2
+ * Aria snapshot formatter: parses snapshot text into clean format.
3
3
  *
4
4
  * 4-pass pipeline:
5
5
  * 1. Parse & filter: strip annotations, metadata, noise, ads, boilerplate subtrees
@@ -50,10 +50,10 @@ const BOILERPLATE_LABELS = [
50
50
  /**
51
51
  * Parse role and text from a trimmed snapshot line.
52
52
  * Handles quoted labels and trailing text after colon correctly,
53
- * including lines wrapped in single quotes by Playwright.
53
+ * including lines wrapped in single quotes.
54
54
  */
55
55
  function parseLine(trimmed) {
56
- // Unwrap outer single quotes if present (Playwright wraps lines with special chars)
56
+ // Unwrap outer single quotes if present (snapshot wraps lines with special chars)
57
57
  let line = trimmed;
58
58
  if (line.startsWith("'") && line.endsWith("':")) {
59
59
  line = line.slice(1, -2) + ':';
@@ -90,7 +90,7 @@ function parseLine(trimmed) {
90
90
  }
91
91
  /**
92
92
  * Strip ALL bracket annotations from a content line, preserving quoted strings.
93
- * Handles both double-quoted and outer single-quoted lines from Playwright.
93
+ * Handles both double-quoted and outer single-quoted lines.
94
94
  */
95
95
  function stripAnnotations(content) {
96
96
  // Unwrap outer single quotes first
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tests for snapshotFormatter.ts: Playwright MCP snapshot tree filtering.
2
+ * Tests for snapshotFormatter.ts: snapshot tree filtering.
3
3
  *
4
4
  * Uses sanitized excerpts from real websites (GitHub, Bilibili, Twitter)
5
5
  * to validate noise filtering, annotation stripping, and output quality.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tests for snapshotFormatter.ts: Playwright MCP snapshot tree filtering.
2
+ * Tests for snapshotFormatter.ts: snapshot tree filtering.
3
3
  *
4
4
  * Uses sanitized excerpts from real websites (GitHub, Bilibili, Twitter)
5
5
  * to validate noise filtering, annotation stripping, and output quality.
@@ -7,7 +7,7 @@
7
7
  import { describe, it, expect } from 'vitest';
8
8
  import { formatSnapshot } from './snapshotFormatter.js';
9
9
  // ---------------------------------------------------------------------------
10
- // Fixtures: sanitized excerpts from real Playwright MCP snapshots
10
+ // Fixtures: sanitized excerpts from real aria snapshots
11
11
  // ---------------------------------------------------------------------------
12
12
  /** GitHub dashboard navigation bar (generic-heavy, refs, /url: lines) */
13
13
  const GITHUB_NAV = `\
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Page interface: type-safe abstraction over Playwright MCP browser page.
2
+ * Page interface: type-safe abstraction over browser page.
3
3
  *
4
4
  * All pipeline steps and CLI adapters should use this interface
5
5
  * instead of `any` for browser interactions.
@@ -78,4 +78,6 @@ export interface IPage {
78
78
  closeWindow?(): Promise<void>;
79
79
  /** Returns the current page URL, or null if unavailable. */
80
80
  getCurrentUrl?(): Promise<string | null>;
81
+ /** Returns the active tab ID, or undefined if not yet resolved. */
82
+ getActiveTabId?(): number | undefined;
81
83
  }
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Page interface: type-safe abstraction over Playwright MCP browser page.
2
+ * Page interface: type-safe abstraction over browser page.
3
3
  *
4
4
  * All pipeline steps and CLI adapters should use this interface
5
5
  * instead of `any` for browser interactions.
@@ -72,6 +72,8 @@ export default defineConfig({
72
72
  { text: 'Band', link: '/adapters/browser/band' },
73
73
  { text: 'Chaoxing', link: '/adapters/browser/chaoxing' },
74
74
  { text: 'Grok', link: '/adapters/browser/grok' },
75
+ { text: 'Amazon', link: '/adapters/browser/amazon' },
76
+ { text: 'Gemini', link: '/adapters/browser/gemini' },
75
77
  { text: 'NotebookLM', link: '/adapters/browser/notebooklm' },
76
78
  { text: 'WeRead', link: '/adapters/browser/weread' },
77
79
  { text: 'Douban', link: '/adapters/browser/douban' },
@@ -0,0 +1,53 @@
1
+ # Amazon
2
+
3
+ **Mode**: ๐Ÿ” Browser ยท **Domain**: `amazon.com`
4
+
5
+ ## Commands
6
+
7
+ | Command | Description |
8
+ |---------|-------------|
9
+ | `opencli amazon bestsellers [<best-sellers-url>]` | Read Amazon Best Sellers pages for ranked candidate discovery |
10
+ | `opencli amazon search "<query>"` | Read Amazon search results for coarse filtering |
11
+ | `opencli amazon product <asin-or-url>` | Read a product page with title, price, rating, breadcrumbs, and bullets |
12
+ | `opencli amazon offer <asin-or-url>` | Read seller / fulfillment / buy-box facts from the product page |
13
+ | `opencli amazon discussion <asin-or-url>` | Read review summary and sample customer reviews |
14
+
15
+ ## Usage Examples
16
+
17
+ ```bash
18
+ # Root Best Sellers page
19
+ opencli amazon bestsellers https://www.amazon.com/Best-Sellers/zgbs --limit 10 -f json
20
+
21
+ # Category-specific Best Sellers page
22
+ opencli amazon bestsellers "<category-best-sellers-url>" --limit 50 -f json
23
+
24
+ # Search products
25
+ opencli amazon search "desk shelf organizer" --limit 20 -f json
26
+
27
+ # Validate one product
28
+ opencli amazon product B0FJS72893 -f json
29
+
30
+ # Validate seller / offer facts
31
+ opencli amazon offer B0FJS72893 -f json
32
+
33
+ # Read review summary + samples
34
+ opencli amazon discussion B0FJS72893 --limit 5 -f json
35
+ ```
36
+
37
+ ## Prerequisites
38
+
39
+ - Chrome running with an active `amazon.com` session in the shared profile
40
+ - [Browser Bridge extension](/guide/browser-bridge) installed
41
+
42
+ ## Notes
43
+
44
+ - This adapter only returns fields visible on public Amazon pages.
45
+ - `bestsellers` and `search` are for candidate discovery; `product`, `offer`, and `discussion` are the validation surfaces.
46
+ - `offer` is the right surface for `sold_by`, `ships_from`, and Amazon-retail exclusion.
47
+ - `discussion` may return review data even when Q&A is absent. Missing Q&A is a normal outcome, not an error.
48
+
49
+ ## Troubleshooting
50
+
51
+ - If Amazon shows a robot-check page, clear it in Chrome and retry.
52
+ - If CDP is attached to the wrong tab, retry with `OPENCLI_CDP_TARGET=amazon.com`.
53
+ - Avoid running multiple Amazon browser commands in parallel against the same shared Chrome target.
@@ -0,0 +1,72 @@
1
+ # Gemini
2
+
3
+ **Mode**: ๐Ÿ” Browser ยท **Domain**: `gemini.google.com`
4
+
5
+ ## Commands
6
+
7
+ | Command | Description |
8
+ |---------|-------------|
9
+ | `opencli gemini new` | Start a new Gemini web chat |
10
+ | `opencli gemini ask <prompt>` | Send a prompt and return only the assistant reply |
11
+ | `opencli gemini image <prompt>` | Generate images in Gemini and optionally save them locally |
12
+
13
+ ## Usage Examples
14
+
15
+ ```bash
16
+ # Start a fresh chat
17
+ opencli gemini new
18
+
19
+ # Ask Gemini and return minimal plain-text output
20
+ opencli gemini ask "Reply with exactly: HELLO"
21
+
22
+ # Ask in a new chat and wait longer
23
+ opencli gemini ask "Summarize this design in 3 bullets" --new true --timeout 90
24
+
25
+ # Generate an icon image with short flags
26
+ opencli gemini image "Generate a tiny cyan moon icon" --rt 1:1 --st icon
27
+
28
+ # Only generate in Gemini and print the page link without downloading files
29
+ opencli gemini image "A watercolor sunset over a lake" --sd true
30
+
31
+ # Save generated images to a custom directory
32
+ opencli gemini image "A flat illustration of a robot" --op ~/tmp/gemini-images
33
+ ```
34
+
35
+ ## Options
36
+
37
+ ### `ask`
38
+
39
+ | Option | Description |
40
+ |--------|-------------|
41
+ | `prompt` | Prompt to send (required positional argument) |
42
+ | `--timeout` | Max seconds to wait for a reply (default: `60`) |
43
+ | `--new` | Start a new chat before sending (default: `false`) |
44
+
45
+ ### `image`
46
+
47
+ | Option | Description |
48
+ |--------|-------------|
49
+ | `prompt` | Image prompt to send (required positional argument) |
50
+ | `--rt` | Aspect ratio shorthand: `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `3:2`, `2:3` |
51
+ | `--st` | Optional style shorthand, e.g. `icon`, `anime`, `watercolor` |
52
+ | `--op` | Output directory for downloaded images (default: `~/tmp/gemini-images`) |
53
+ | `--sd` | Skip download and only print the Gemini page link |
54
+
55
+ ## Behavior
56
+
57
+ - `ask` uses plain minimal output and returns only the assistant response text prefixed with `๐Ÿ’ฌ`.
58
+ - `image` also uses plain output and prints `status / file / link` instead of a table.
59
+ - `image` always starts from a fresh Gemini chat before sending the prompt.
60
+ - When `--sd` is enabled, `image` keeps the generation in Gemini and only prints the conversation link.
61
+
62
+ ## Prerequisites
63
+
64
+ - Chrome is running
65
+ - You are already logged into `gemini.google.com`
66
+ - [Browser Bridge extension](/guide/browser-bridge) is installed
67
+
68
+ ## Caveats
69
+
70
+ - This adapter drives the Gemini consumer web UI, not a public API.
71
+ - It depends on the current browser session and may fail if Gemini shows login, consent, challenge, quota, or other gating UI.
72
+ - DOM or product changes on Gemini can break composer detection, new-chat handling, or image export behavior.
@@ -8,7 +8,8 @@
8
8
  |---------|-------------|
9
9
  | `opencli notebooklm status` | Check whether NotebookLM is reachable in the current Chrome session |
10
10
  | `opencli notebooklm list` | List notebooks visible from the NotebookLM home page |
11
- | `opencli notebooklm current` | Show metadata for the currently opened notebook tab |
11
+ | `opencli notebooklm open <notebook>` | Open one notebook in the NotebookLM automation workspace by id or URL |
12
+ | `opencli notebooklm current` | Show metadata for the currently opened notebook in the automation workspace |
12
13
  | `opencli notebooklm get` | Get richer metadata for the current notebook |
13
14
  | `opencli notebooklm source-list` | List sources in the current notebook |
14
15
  | `opencli notebooklm source-get <source>` | Resolve one source in the current notebook by id or title |
@@ -17,15 +18,14 @@
17
18
  | `opencli notebooklm history` | List conversation history threads for the current notebook |
18
19
  | `opencli notebooklm note-list` | List Studio notes visible in the current notebook |
19
20
  | `opencli notebooklm notes-get <note>` | Read the currently visible Studio note by title |
20
- | `opencli notebooklm bind-current` | Bind the current active NotebookLM tab into the `site:notebooklm` workspace |
21
21
  | `opencli notebooklm summary` | Read the current notebook summary |
22
22
 
23
23
  ## Compatibility Aliases
24
24
 
25
25
  | Alias | Canonical command |
26
26
  |-------|-------------------|
27
+ | `opencli notebooklm select <notebook>` | `opencli notebooklm open <notebook>` |
27
28
  | `opencli notebooklm metadata` | `opencli notebooklm get` |
28
- | `opencli notebooklm use` | `opencli notebooklm bind-current` |
29
29
  | `opencli notebooklm notes-list` | `opencli notebooklm note-list` |
30
30
 
31
31
  ## Positioning
@@ -43,6 +43,7 @@ The current milestone focuses on a stable NotebookLM read surface in desktop Chr
43
43
  ```bash
44
44
  opencli notebooklm status
45
45
  opencli notebooklm list -f json
46
+ opencli notebooklm open nb-demo -f json
46
47
  opencli notebooklm current -f json
47
48
  opencli notebooklm metadata -f json
48
49
  opencli notebooklm source-list -f json
@@ -53,7 +54,6 @@ opencli notebooklm history -f json
53
54
  opencli notebooklm notes-list -f json
54
55
  opencli notebooklm notes-get "Draft note" -f json
55
56
  opencli notebooklm summary -f json
56
- opencli notebooklm use -f json
57
57
  ```
58
58
 
59
59
  ## Prerequisites
@@ -64,6 +64,6 @@ opencli notebooklm use -f json
64
64
 
65
65
  ## Notes
66
66
 
67
- - Notebook-oriented commands assume you already have the target notebook open in Chrome, or that `opencli notebooklm use` can bind an existing notebook tab into `site:notebooklm`.
67
+ - Notebook-oriented commands run in OpenCLI's owned NotebookLM automation workspace/window. Use `opencli notebooklm open <notebook>` first to choose the current notebook for follow-up commands.
68
68
  - `list`, `get`, `source-list`, `history`, `source-fulltext`, and `source-guide` prefer NotebookLM RPC paths and fall back only when the richer path is unavailable.
69
69
  - `notes-get` currently reads note content only from the visible Studio note editor; if the note is listed but not open, open it in NotebookLM first and then retry.
@@ -29,7 +29,8 @@ Run `opencli list` for the live registry.
29
29
  | **[linux-do](./browser/linux-do)** | `feed` `categories` `tags` `search` `topic` `user-topics` `user-posts` | ๐Ÿ” Browser |
30
30
  | **[chaoxing](./browser/chaoxing)** | `assignments` `exams` | ๐Ÿ” Browser |
31
31
  | **[grok](./browser/grok)** | `ask` | ๐Ÿ” Browser |
32
- | **[notebooklm](./browser/notebooklm)** | `status` `list` `current` `get` `metadata` `bind-current` `use` `source-list` `source-get` `source-fulltext` `source-guide` `history` `note-list` `notes-list` `notes-get` `summary` | ๐Ÿ” Browser |
32
+ | **[gemini](./browser/gemini)** | `new` `ask` `image` | ๐Ÿ” Browser |
33
+ | **[notebooklm](./browser/notebooklm)** | `status` `list` `open` `select` `current` `get` `metadata` `source-list` `source-get` `source-fulltext` `source-guide` `history` `note-list` `notes-list` `notes-get` `summary` | ๐Ÿ” Browser |
33
34
  | **[doubao](./browser/doubao)** | `status` `new` `send` `read` `ask` `history` `detail` `meeting-summary` `meeting-transcript` | ๐Ÿ” Browser |
34
35
  | **[weread](./browser/weread)** | `shelf` `search` `book` `ranking` `notebooks` `highlights` `notes` | ๐Ÿ” Browser |
35
36
  | **[douban](./browser/douban)** | `search` `top250` `subject` `photos` `download` `marks` `reviews` `movie-hot` `book-hot` | ๐Ÿ” Browser |
@@ -43,6 +44,7 @@ Run `opencli list` for the live registry.
43
44
  | **[tiktok](./browser/tiktok)** | `explore` `search` `profile` `user` `following` `follow` `unfollow` `like` `unlike` `comment` `save` `unsave` `live` `notifications` `friends` | ๐Ÿ” Browser |
44
45
  | **[google](./browser/google)** | `news` `search` `suggest` `trends` | ๐ŸŒ / ๐Ÿ” |
45
46
  | **[jd](./browser/jd)** | `item` | ๐Ÿ” Browser |
47
+ | **[amazon](./browser/amazon)** | `bestsellers` `search` `product` `offer` `discussion` | ๐Ÿ” Browser |
46
48
  | **[web](./browser/web)** | `read` | ๐Ÿ” Browser |
47
49
  | **[weixin](./browser/weixin)** | `download` | ๐Ÿ” Browser |
48
50
  | **[36kr](./browser/36kr)** | `news` `hot` `search` `article` | ๐ŸŒ / ๐Ÿ” |