@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.
- package/CHANGELOG.md +21 -0
- package/README.md +17 -1
- package/README.zh-CN.md +17 -1
- package/dist/browser/base-page.d.ts +48 -0
- package/dist/browser/base-page.js +160 -0
- package/dist/browser/cdp.js +4 -106
- package/dist/browser/daemon-client.d.ts +1 -7
- package/dist/browser/daemon-client.js +2 -9
- package/dist/browser/discover.d.ts +1 -4
- package/dist/browser/discover.js +1 -4
- package/dist/browser/errors.d.ts +4 -0
- package/dist/browser/errors.js +20 -0
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/page.d.ts +6 -35
- package/dist/browser/page.js +10 -189
- package/dist/browser/tabs.js +5 -5
- package/dist/browser.test.js +15 -15
- package/dist/cli-manifest.json +294 -22
- package/dist/clis/amazon/bestsellers.d.ts +21 -0
- package/dist/clis/amazon/bestsellers.js +130 -0
- package/dist/clis/amazon/bestsellers.test.js +20 -0
- package/dist/clis/amazon/discussion.d.ts +20 -0
- package/dist/clis/amazon/discussion.js +91 -0
- package/dist/clis/amazon/discussion.test.js +36 -0
- package/dist/clis/amazon/offer.d.ts +23 -0
- package/dist/clis/amazon/offer.js +140 -0
- package/dist/clis/amazon/offer.test.d.ts +1 -0
- package/dist/clis/amazon/offer.test.js +29 -0
- package/dist/clis/amazon/product.d.ts +18 -0
- package/dist/clis/amazon/product.js +92 -0
- package/dist/clis/amazon/product.test.d.ts +1 -0
- package/dist/clis/amazon/product.test.js +24 -0
- package/dist/clis/amazon/search.d.ts +18 -0
- package/dist/clis/amazon/search.js +87 -0
- package/dist/clis/amazon/search.test.d.ts +1 -0
- package/dist/clis/amazon/search.test.js +22 -0
- package/dist/clis/amazon/shared.d.ts +64 -0
- package/dist/clis/amazon/shared.js +255 -0
- package/dist/clis/amazon/shared.test.d.ts +1 -0
- package/dist/clis/amazon/shared.test.js +33 -0
- package/dist/clis/gemini/ask.d.ts +1 -0
- package/dist/clis/gemini/ask.js +40 -0
- package/dist/clis/gemini/image.d.ts +1 -0
- package/dist/clis/gemini/image.js +105 -0
- package/dist/clis/gemini/new.d.ts +1 -0
- package/dist/clis/gemini/new.js +20 -0
- package/dist/clis/gemini/utils.d.ts +34 -0
- package/dist/clis/gemini/utils.js +463 -0
- package/dist/clis/gemini/utils.test.d.ts +1 -0
- package/dist/clis/gemini/utils.test.js +31 -0
- package/dist/clis/notebooklm/compat.test.d.ts +1 -1
- package/dist/clis/notebooklm/compat.test.js +3 -3
- package/dist/clis/notebooklm/current.js +2 -3
- package/dist/clis/notebooklm/get.js +2 -3
- package/dist/clis/notebooklm/history.js +2 -3
- package/dist/clis/notebooklm/note-list.js +2 -3
- package/dist/clis/notebooklm/notes-get.js +2 -3
- package/dist/clis/notebooklm/open.d.ts +1 -0
- package/dist/clis/notebooklm/open.js +41 -0
- package/dist/clis/notebooklm/open.test.d.ts +1 -0
- package/dist/clis/notebooklm/open.test.js +63 -0
- package/dist/clis/notebooklm/source-fulltext.js +2 -3
- package/dist/clis/notebooklm/source-get.js +2 -3
- package/dist/clis/notebooklm/source-guide.js +2 -3
- package/dist/clis/notebooklm/source-list.js +2 -3
- package/dist/clis/notebooklm/status.js +1 -2
- package/dist/clis/notebooklm/summary.js +2 -3
- package/dist/clis/notebooklm/utils.d.ts +2 -1
- package/dist/clis/notebooklm/utils.js +20 -21
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +11 -11
- package/dist/clis/xiaohongshu/creator-notes-summary.test.js +6 -6
- package/dist/clis/xiaohongshu/creator-notes.test.js +22 -22
- package/dist/commanderAdapter.js +6 -3
- package/dist/commanderAdapter.test.js +33 -0
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/daemon.test.js +1 -1
- package/dist/doctor.d.ts +1 -2
- package/dist/doctor.js +7 -8
- package/dist/explore.js +1 -1
- package/dist/output.js +28 -0
- package/dist/output.test.js +15 -0
- package/dist/pipeline/executor.js +2 -7
- package/dist/pipeline/steps/browser.js +1 -1
- package/dist/pipeline/template.js +25 -3
- package/dist/record.d.ts +50 -0
- package/dist/record.js +298 -57
- package/dist/record.test.d.ts +1 -0
- package/dist/record.test.js +293 -0
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +1 -0
- package/dist/registry.test.js +10 -0
- package/dist/runtime.js +3 -3
- package/dist/snapshotFormatter.d.ts +1 -1
- package/dist/snapshotFormatter.js +4 -4
- package/dist/snapshotFormatter.test.d.ts +1 -1
- package/dist/snapshotFormatter.test.js +2 -2
- package/dist/types.d.ts +3 -1
- package/dist/types.js +1 -1
- package/docs/.vitepress/config.mts +2 -0
- package/docs/adapters/browser/amazon.md +53 -0
- package/docs/adapters/browser/gemini.md +72 -0
- package/docs/adapters/browser/notebooklm.md +5 -5
- package/docs/adapters/index.md +3 -1
- package/extension/dist/background.js +5 -143
- package/extension/src/background.test.ts +7 -163
- package/extension/src/background.ts +7 -157
- package/extension/src/protocol.ts +1 -5
- package/package.json +1 -1
- package/skills/opencli-explorer/SKILL.md +847 -0
- package/skills/opencli-oneshot/SKILL.md +216 -0
- package/skills/opencli-usage/SKILL.md +71 -0
- package/skills/opencli-usage/browser.md +429 -0
- package/skills/opencli-usage/desktop.md +118 -0
- package/skills/opencli-usage/plugins.md +82 -0
- package/skills/opencli-usage/public-api.md +149 -0
- package/src/browser/base-page.ts +197 -0
- package/src/browser/cdp.ts +7 -131
- package/src/browser/daemon-client.ts +3 -14
- package/src/browser/discover.ts +1 -4
- package/src/browser/errors.ts +22 -0
- package/src/browser/index.ts +1 -1
- package/src/browser/page.ts +13 -212
- package/src/browser/tabs.ts +5 -5
- package/src/browser.test.ts +15 -15
- package/src/clis/amazon/bestsellers.test.ts +22 -0
- package/src/clis/amazon/bestsellers.ts +180 -0
- package/src/clis/amazon/discussion.test.ts +38 -0
- package/src/clis/amazon/discussion.ts +131 -0
- package/src/clis/amazon/offer.test.ts +35 -0
- package/src/clis/amazon/offer.ts +185 -0
- package/src/clis/amazon/product.test.ts +26 -0
- package/src/clis/amazon/product.ts +131 -0
- package/src/clis/amazon/search.test.ts +24 -0
- package/src/clis/amazon/search.ts +128 -0
- package/src/clis/amazon/shared.test.ts +37 -0
- package/src/clis/amazon/shared.ts +316 -0
- package/src/clis/gemini/ask.ts +46 -0
- package/src/clis/gemini/image.ts +115 -0
- package/src/clis/gemini/new.ts +22 -0
- package/src/clis/gemini/utils.test.ts +36 -0
- package/src/clis/gemini/utils.ts +523 -0
- package/src/clis/notebooklm/compat.test.ts +3 -3
- package/src/clis/notebooklm/current.ts +2 -3
- package/src/clis/notebooklm/get.ts +1 -3
- package/src/clis/notebooklm/history.ts +1 -3
- package/src/clis/notebooklm/note-list.ts +1 -3
- package/src/clis/notebooklm/notes-get.ts +1 -3
- package/src/clis/notebooklm/open.test.ts +78 -0
- package/src/clis/notebooklm/open.ts +61 -0
- package/src/clis/notebooklm/source-fulltext.ts +1 -3
- package/src/clis/notebooklm/source-get.ts +1 -3
- package/src/clis/notebooklm/source-guide.ts +1 -3
- package/src/clis/notebooklm/source-list.ts +1 -3
- package/src/clis/notebooklm/status.ts +1 -2
- package/src/clis/notebooklm/summary.ts +1 -3
- package/src/clis/notebooklm/utils.ts +29 -20
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +11 -11
- package/src/clis/xiaohongshu/creator-notes-summary.test.ts +6 -6
- package/src/clis/xiaohongshu/creator-notes.test.ts +22 -22
- package/src/commanderAdapter.test.ts +47 -0
- package/src/commanderAdapter.ts +7 -3
- package/src/commands/daemon.test.ts +1 -1
- package/src/commands/daemon.ts +1 -1
- package/src/doctor.ts +7 -8
- package/src/explore.ts +1 -1
- package/src/output.test.ts +17 -0
- package/src/output.ts +27 -0
- package/src/pipeline/executor.ts +2 -7
- package/src/pipeline/steps/browser.ts +1 -1
- package/src/pipeline/template.ts +27 -4
- package/src/record.test.ts +362 -0
- package/src/record.ts +341 -62
- package/src/registry.test.ts +12 -0
- package/src/registry.ts +3 -0
- package/src/runtime.ts +3 -3
- package/src/snapshotFormatter.test.ts +2 -2
- package/src/snapshotFormatter.ts +4 -4
- package/src/types.ts +3 -1
- package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
- package/.agents/workflows/cross-project-adapter-migration.md +0 -54
- package/SKILL.md +0 -879
- package/dist/clis/notebooklm/bind-current.js +0 -29
- package/dist/clis/notebooklm/bind-current.test.d.ts +0 -1
- package/dist/clis/notebooklm/bind-current.test.js +0 -35
- package/dist/clis/notebooklm/binding.test.js +0 -44
- package/src/clis/notebooklm/bind-current.test.ts +0 -43
- package/src/clis/notebooklm/bind-current.ts +0 -36
- package/src/clis/notebooklm/binding.test.ts +0 -53
- /package/dist/browser/{mcp.d.ts โ bridge.d.ts} +0 -0
- /package/dist/browser/{mcp.js โ bridge.js} +0 -0
- /package/dist/clis/{notebooklm/bind-current.d.ts โ amazon/bestsellers.test.d.ts} +0 -0
- /package/dist/clis/{notebooklm/binding.test.d.ts โ amazon/discussion.test.d.ts} +0 -0
- /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
|
+
});
|
package/dist/registry.d.ts
CHANGED
|
@@ -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
package/dist/registry.test.js
CHANGED
|
@@ -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
|
|
49
|
+
const browser = new BrowserFactory();
|
|
50
50
|
try {
|
|
51
|
-
const page = await
|
|
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
|
|
59
|
+
await browser.close().catch(() => { });
|
|
60
60
|
}
|
|
61
61
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Aria snapshot formatter: parses
|
|
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
|
|
53
|
+
* including lines wrapped in single quotes.
|
|
54
54
|
*/
|
|
55
55
|
function parseLine(trimmed) {
|
|
56
|
-
// Unwrap outer single quotes if present (
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
@@ -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
|
|
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
|
|
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.
|
package/docs/adapters/index.md
CHANGED
|
@@ -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
|
-
| **[
|
|
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` | ๐ / ๐ |
|