@jackwener/opencli 1.5.8 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +35 -1
- package/README.zh-CN.md +17 -1
- package/SKILL.md +31 -851
- package/autoresearch/baseline-browse.txt +1 -0
- package/autoresearch/baseline-skill.txt +1 -0
- package/autoresearch/browse-tasks.json +688 -0
- package/autoresearch/eval-browse.ts +185 -0
- package/autoresearch/eval-skill.ts +248 -0
- package/autoresearch/run-browse.sh +9 -0
- package/autoresearch/run-skill.sh +9 -0
- 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 +20 -7
- package/dist/browser/daemon-client.js +39 -39
- package/dist/browser/daemon-client.test.js +77 -0
- package/dist/browser/discover.d.ts +1 -4
- package/dist/browser/discover.js +9 -23
- 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 +10 -35
- package/dist/browser/page.js +55 -187
- package/dist/browser/tabs.js +5 -5
- package/dist/browser.test.js +15 -15
- package/dist/cli-manifest.json +294 -22
- package/dist/cli.js +392 -0
- 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.d.ts +1 -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/twitter/article.js +28 -1
- 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/clis/xiaohongshu/note.js +11 -0
- package/dist/clis/xiaohongshu/note.test.js +49 -0
- package/dist/commanderAdapter.js +7 -4
- package/dist/commanderAdapter.test.js +76 -0
- package/dist/commands/daemon.js +8 -47
- package/dist/commands/daemon.test.js +45 -70
- package/dist/discovery.js +27 -0
- 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 +11 -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/docs/guide/getting-started.md +21 -0
- package/docs/superpowers/specs/2026-04-02-browse-skill-testing-design.md +144 -0
- package/docs/zh/guide/getting-started.md +21 -0
- package/extension/package-lock.json +2 -2
- package/extension/src/background.test.ts +7 -163
- package/extension/src/background.ts +58 -161
- package/extension/src/cdp.ts +77 -124
- package/extension/src/protocol.ts +5 -5
- package/package.json +1 -1
- package/skills/opencli-explorer/SKILL.md +853 -0
- package/skills/opencli-oneshot/SKILL.md +222 -0
- package/skills/opencli-operate/SKILL.md +213 -0
- package/skills/opencli-usage/SKILL.md +152 -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.test.ts +103 -0
- package/src/browser/daemon-client.ts +55 -43
- package/src/browser/discover.ts +9 -21
- package/src/browser/errors.ts +22 -0
- package/src/browser/index.ts +1 -1
- package/src/browser/page.ts +57 -209
- package/src/browser/tabs.ts +5 -5
- package/src/browser.test.ts +15 -15
- package/src/cli.ts +392 -0
- 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/twitter/article.ts +31 -1
- 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/clis/xiaohongshu/note.test.ts +51 -0
- package/src/clis/xiaohongshu/note.ts +18 -0
- package/src/commanderAdapter.test.ts +109 -0
- package/src/commanderAdapter.ts +8 -4
- package/src/commands/daemon.test.ts +50 -84
- package/src/commands/daemon.ts +8 -56
- package/src/discovery.ts +22 -0
- package/src/doctor.ts +8 -9
- 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 +11 -1
- package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
- package/.agents/workflows/cross-project-adapter-migration.md +0 -54
- 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/extension/dist/background.js +0 -819
- 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 → browser/daemon-client.test.d.ts} +0 -0
- /package/dist/clis/{notebooklm/binding.test.d.ts → amazon/bestsellers.test.d.ts} +0 -0
- /package/src/browser/{mcp.ts → bridge.ts} +0 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
export const GEMINI_DOMAIN = 'gemini.google.com';
|
|
2
|
+
export const GEMINI_APP_URL = 'https://gemini.google.com/app';
|
|
3
|
+
const GEMINI_RESPONSE_NOISE_PATTERNS = [
|
|
4
|
+
/Gemini can make mistakes\.?/gi,
|
|
5
|
+
/Google Terms/gi,
|
|
6
|
+
/Google Privacy Policy/gi,
|
|
7
|
+
/Opens in a new window/gi,
|
|
8
|
+
];
|
|
9
|
+
export function sanitizeGeminiResponseText(value, promptText) {
|
|
10
|
+
let sanitized = value;
|
|
11
|
+
for (const pattern of GEMINI_RESPONSE_NOISE_PATTERNS) {
|
|
12
|
+
sanitized = sanitized.replace(pattern, '');
|
|
13
|
+
}
|
|
14
|
+
sanitized = sanitized.trim();
|
|
15
|
+
const prompt = promptText.trim();
|
|
16
|
+
if (!prompt)
|
|
17
|
+
return sanitized;
|
|
18
|
+
if (sanitized === prompt)
|
|
19
|
+
return '';
|
|
20
|
+
for (const separator of ['\n\n', '\n', '\r\n\r\n', '\r\n']) {
|
|
21
|
+
const prefix = `${prompt}${separator}`;
|
|
22
|
+
if (sanitized.startsWith(prefix)) {
|
|
23
|
+
return sanitized.slice(prefix.length).trim();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return sanitized;
|
|
27
|
+
}
|
|
28
|
+
export function collectGeminiTranscriptAdditions(beforeLines, currentLines, promptText) {
|
|
29
|
+
const beforeSet = new Set(beforeLines);
|
|
30
|
+
const additions = currentLines
|
|
31
|
+
.filter((line) => !beforeSet.has(line))
|
|
32
|
+
.map((line) => sanitizeGeminiResponseText(line, promptText))
|
|
33
|
+
.filter((line) => line && line !== promptText);
|
|
34
|
+
return additions.join('\n').trim();
|
|
35
|
+
}
|
|
36
|
+
function getStateScript() {
|
|
37
|
+
return `
|
|
38
|
+
(() => {
|
|
39
|
+
const signInNode = Array.from(document.querySelectorAll('a, button')).find((node) => {
|
|
40
|
+
const text = (node.textContent || '').trim().toLowerCase();
|
|
41
|
+
const aria = (node.getAttribute('aria-label') || '').trim().toLowerCase();
|
|
42
|
+
const href = node.getAttribute('href') || '';
|
|
43
|
+
return text === 'sign in'
|
|
44
|
+
|| aria === 'sign in'
|
|
45
|
+
|| href.includes('accounts.google.com/ServiceLogin');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const composer = document.querySelector('[aria-label="Enter a prompt for Gemini"], [aria-label*="prompt for Gemini"], .ql-editor[aria-label*="Gemini"], [contenteditable="true"][aria-label*="Gemini"]');
|
|
49
|
+
const sendButton = document.querySelector('button[aria-label="Send message"]');
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
url: window.location.href,
|
|
53
|
+
title: document.title || '',
|
|
54
|
+
isSignedIn: signInNode ? false : (composer ? true : null),
|
|
55
|
+
composerLabel: composer?.getAttribute('aria-label') || '',
|
|
56
|
+
canSend: !!(sendButton && !sendButton.disabled),
|
|
57
|
+
};
|
|
58
|
+
})()
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
function getTranscriptLinesScript() {
|
|
62
|
+
return `
|
|
63
|
+
(() => {
|
|
64
|
+
const clean = (value) => (value || '')
|
|
65
|
+
.replace(/\\u00a0/g, ' ')
|
|
66
|
+
.replace(/\\n{3,}/g, '\\n\\n')
|
|
67
|
+
.trim();
|
|
68
|
+
|
|
69
|
+
const main = document.querySelector('main') || document.body;
|
|
70
|
+
const root = main.cloneNode(true);
|
|
71
|
+
|
|
72
|
+
const removableSelectors = [
|
|
73
|
+
'button',
|
|
74
|
+
'nav',
|
|
75
|
+
'header',
|
|
76
|
+
'footer',
|
|
77
|
+
'[aria-label="Enter a prompt for Gemini"]',
|
|
78
|
+
'[aria-label*="prompt for Gemini"]',
|
|
79
|
+
'.input-area-container',
|
|
80
|
+
'.input-wrapper',
|
|
81
|
+
'.textbox-container',
|
|
82
|
+
'.ql-toolbar',
|
|
83
|
+
'.send-button',
|
|
84
|
+
'.main-menu-button',
|
|
85
|
+
'.sign-in-button',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
for (const selector of removableSelectors) {
|
|
89
|
+
root.querySelectorAll(selector).forEach((node) => node.remove());
|
|
90
|
+
}
|
|
91
|
+
root.querySelectorAll('script, style, noscript').forEach((node) => node.remove());
|
|
92
|
+
|
|
93
|
+
const stopLines = new Set([
|
|
94
|
+
'Gemini',
|
|
95
|
+
'Google Terms',
|
|
96
|
+
'Google Privacy Policy',
|
|
97
|
+
'Meet Gemini, your personal AI assistant',
|
|
98
|
+
'Conversation with Gemini',
|
|
99
|
+
'Ask Gemini 3',
|
|
100
|
+
'Write',
|
|
101
|
+
'Plan',
|
|
102
|
+
'Research',
|
|
103
|
+
'Learn',
|
|
104
|
+
'Fast',
|
|
105
|
+
'send',
|
|
106
|
+
'Microphone',
|
|
107
|
+
'Main menu',
|
|
108
|
+
'New chat',
|
|
109
|
+
'Sign in',
|
|
110
|
+
'Google Terms Opens in a new window',
|
|
111
|
+
'Google Privacy Policy Opens in a new window',
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
const noisyPatterns = [
|
|
115
|
+
/^Google Terms$/,
|
|
116
|
+
/^Google Privacy Policy$/,
|
|
117
|
+
/^Gemini is AI and can make mistakes\.?$/,
|
|
118
|
+
/^and the$/,
|
|
119
|
+
/^apply\.$/,
|
|
120
|
+
/^Opens in a new window$/,
|
|
121
|
+
/^Open mode picker$/,
|
|
122
|
+
/^Open upload file menu$/,
|
|
123
|
+
/^Tools$/,
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
return clean(root.innerText || root.textContent || '')
|
|
127
|
+
.split('\\n')
|
|
128
|
+
.map((line) => clean(line))
|
|
129
|
+
.filter((line) => line
|
|
130
|
+
&& line.length <= 4000
|
|
131
|
+
&& !stopLines.has(line)
|
|
132
|
+
&& !noisyPatterns.some((pattern) => pattern.test(line)));
|
|
133
|
+
})()
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
function getTurnsScript() {
|
|
137
|
+
return `
|
|
138
|
+
(() => {
|
|
139
|
+
const clean = (value) => (value || '')
|
|
140
|
+
.replace(/\\u00a0/g, ' ')
|
|
141
|
+
.replace(/\\n{3,}/g, '\\n\\n')
|
|
142
|
+
.trim();
|
|
143
|
+
|
|
144
|
+
const isVisible = (el) => {
|
|
145
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
146
|
+
const style = window.getComputedStyle(el);
|
|
147
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
148
|
+
const rect = el.getBoundingClientRect();
|
|
149
|
+
return rect.width > 0 && rect.height > 0;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const selectors = [
|
|
153
|
+
'[data-testid*="message"]',
|
|
154
|
+
'[data-test-id*="message"]',
|
|
155
|
+
'[class*="message"]',
|
|
156
|
+
'[class*="conversation-turn"]',
|
|
157
|
+
'[class*="query-text"]',
|
|
158
|
+
'[class*="response-text"]',
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const roots = selectors.flatMap((selector) => Array.from(document.querySelectorAll(selector)));
|
|
162
|
+
const unique = roots.filter((el, index, all) => all.indexOf(el) === index).filter(isVisible);
|
|
163
|
+
|
|
164
|
+
const turns = unique.map((el) => {
|
|
165
|
+
const text = clean(el.innerText || el.textContent || '');
|
|
166
|
+
if (!text) return null;
|
|
167
|
+
|
|
168
|
+
const roleAttr = [
|
|
169
|
+
el.getAttribute('data-message-author-role'),
|
|
170
|
+
el.getAttribute('data-role'),
|
|
171
|
+
el.getAttribute('aria-label'),
|
|
172
|
+
el.getAttribute('class'),
|
|
173
|
+
].filter(Boolean).join(' ').toLowerCase();
|
|
174
|
+
|
|
175
|
+
let role = '';
|
|
176
|
+
if (roleAttr.includes('user') || roleAttr.includes('query')) role = 'User';
|
|
177
|
+
else if (roleAttr.includes('assistant') || roleAttr.includes('model') || roleAttr.includes('response') || roleAttr.includes('gemini')) role = 'Assistant';
|
|
178
|
+
|
|
179
|
+
return role ? { Role: role, Text: text } : null;
|
|
180
|
+
}).filter(Boolean);
|
|
181
|
+
|
|
182
|
+
const deduped = [];
|
|
183
|
+
const seen = new Set();
|
|
184
|
+
for (const turn of turns) {
|
|
185
|
+
const key = turn.Role + '::' + turn.Text;
|
|
186
|
+
if (seen.has(key)) continue;
|
|
187
|
+
seen.add(key);
|
|
188
|
+
deduped.push(turn);
|
|
189
|
+
}
|
|
190
|
+
return deduped;
|
|
191
|
+
})()
|
|
192
|
+
`;
|
|
193
|
+
}
|
|
194
|
+
function fillAndSubmitComposerScript(text) {
|
|
195
|
+
return `
|
|
196
|
+
((inputText) => {
|
|
197
|
+
const cleanInsert = (el) => {
|
|
198
|
+
if (!(el instanceof HTMLElement)) throw new Error('Composer is not editable');
|
|
199
|
+
el.focus();
|
|
200
|
+
const selection = window.getSelection();
|
|
201
|
+
const range = document.createRange();
|
|
202
|
+
range.selectNodeContents(el);
|
|
203
|
+
range.collapse(false);
|
|
204
|
+
selection?.removeAllRanges();
|
|
205
|
+
selection?.addRange(range);
|
|
206
|
+
el.textContent = '';
|
|
207
|
+
document.execCommand('insertText', false, inputText);
|
|
208
|
+
el.dispatchEvent(new InputEvent('input', { bubbles: true, data: inputText, inputType: 'insertText' }));
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const composer = document.querySelector('[aria-label="Enter a prompt for Gemini"], [aria-label*="prompt for Gemini"], .ql-editor[aria-label*="Gemini"], [contenteditable="true"][aria-label*="Gemini"]');
|
|
212
|
+
if (!(composer instanceof HTMLElement)) {
|
|
213
|
+
throw new Error('Could not find Gemini composer');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
cleanInsert(composer);
|
|
217
|
+
|
|
218
|
+
const sendButton = document.querySelector('button[aria-label="Send message"]');
|
|
219
|
+
if (sendButton instanceof HTMLButtonElement && !sendButton.disabled) {
|
|
220
|
+
sendButton.click();
|
|
221
|
+
return 'button';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
composer.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
|
|
225
|
+
composer.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
|
|
226
|
+
return 'enter';
|
|
227
|
+
})(${JSON.stringify(text)})
|
|
228
|
+
`;
|
|
229
|
+
}
|
|
230
|
+
function clickNewChatScript() {
|
|
231
|
+
return `
|
|
232
|
+
(() => {
|
|
233
|
+
const isVisible = (el) => {
|
|
234
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
235
|
+
const style = window.getComputedStyle(el);
|
|
236
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
237
|
+
const rect = el.getBoundingClientRect();
|
|
238
|
+
return rect.width > 0 && rect.height > 0;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const candidates = Array.from(document.querySelectorAll('button, a')).filter((node) => {
|
|
242
|
+
const text = (node.textContent || '').trim().toLowerCase();
|
|
243
|
+
const aria = (node.getAttribute('aria-label') || '').trim().toLowerCase();
|
|
244
|
+
return isVisible(node) && (text === 'new chat' || aria === 'new chat');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const target = candidates.find((node) => !node.hasAttribute('disabled')) || candidates[0];
|
|
248
|
+
if (target instanceof HTMLElement) {
|
|
249
|
+
target.click();
|
|
250
|
+
return 'clicked';
|
|
251
|
+
}
|
|
252
|
+
return 'navigate';
|
|
253
|
+
})()
|
|
254
|
+
`;
|
|
255
|
+
}
|
|
256
|
+
function currentUrlScript() {
|
|
257
|
+
return 'window.location.href';
|
|
258
|
+
}
|
|
259
|
+
export async function isOnGemini(page) {
|
|
260
|
+
const url = await page.evaluate(currentUrlScript()).catch(() => '');
|
|
261
|
+
if (typeof url !== 'string' || !url)
|
|
262
|
+
return false;
|
|
263
|
+
try {
|
|
264
|
+
const hostname = new URL(url).hostname;
|
|
265
|
+
return hostname === GEMINI_DOMAIN || hostname.endsWith(`.${GEMINI_DOMAIN}`);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
export async function ensureGeminiPage(page) {
|
|
272
|
+
if (!(await isOnGemini(page))) {
|
|
273
|
+
await page.goto(GEMINI_APP_URL, { waitUntil: 'load', settleMs: 2500 });
|
|
274
|
+
await page.wait(1);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
export async function getGeminiPageState(page) {
|
|
278
|
+
await ensureGeminiPage(page);
|
|
279
|
+
return await page.evaluate(getStateScript());
|
|
280
|
+
}
|
|
281
|
+
export async function startNewGeminiChat(page) {
|
|
282
|
+
await ensureGeminiPage(page);
|
|
283
|
+
const action = await page.evaluate(clickNewChatScript());
|
|
284
|
+
if (action === 'navigate') {
|
|
285
|
+
await page.goto(GEMINI_APP_URL, { waitUntil: 'load', settleMs: 2500 });
|
|
286
|
+
}
|
|
287
|
+
await page.wait(1);
|
|
288
|
+
return action;
|
|
289
|
+
}
|
|
290
|
+
export async function getGeminiVisibleTurns(page) {
|
|
291
|
+
await ensureGeminiPage(page);
|
|
292
|
+
const turns = await page.evaluate(getTurnsScript());
|
|
293
|
+
if (Array.isArray(turns) && turns.length > 0)
|
|
294
|
+
return turns;
|
|
295
|
+
const lines = await getGeminiTranscriptLines(page);
|
|
296
|
+
return lines.map((line) => ({ Role: 'System', Text: line }));
|
|
297
|
+
}
|
|
298
|
+
export async function getGeminiTranscriptLines(page) {
|
|
299
|
+
await ensureGeminiPage(page);
|
|
300
|
+
return await page.evaluate(getTranscriptLinesScript());
|
|
301
|
+
}
|
|
302
|
+
export async function sendGeminiMessage(page, text) {
|
|
303
|
+
await ensureGeminiPage(page);
|
|
304
|
+
const submittedBy = await page.evaluate(fillAndSubmitComposerScript(text));
|
|
305
|
+
await page.wait(1);
|
|
306
|
+
return submittedBy;
|
|
307
|
+
}
|
|
308
|
+
export async function getGeminiVisibleImageUrls(page) {
|
|
309
|
+
await ensureGeminiPage(page);
|
|
310
|
+
return await page.evaluate(`
|
|
311
|
+
(() => {
|
|
312
|
+
const isVisible = (el) => {
|
|
313
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
314
|
+
const style = window.getComputedStyle(el);
|
|
315
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
316
|
+
const rect = el.getBoundingClientRect();
|
|
317
|
+
return rect.width > 32 && rect.height > 32;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const imgs = Array.from(document.querySelectorAll('main img')).filter((img) => img instanceof HTMLImageElement && isVisible(img));
|
|
321
|
+
const urls = [];
|
|
322
|
+
const seen = new Set();
|
|
323
|
+
|
|
324
|
+
for (const img of imgs) {
|
|
325
|
+
const src = img.currentSrc || img.src || '';
|
|
326
|
+
const alt = (img.getAttribute('alt') || '').toLowerCase();
|
|
327
|
+
const width = img.naturalWidth || img.width || 0;
|
|
328
|
+
const height = img.naturalHeight || img.height || 0;
|
|
329
|
+
if (!src) continue;
|
|
330
|
+
if (alt.includes('avatar') || alt.includes('logo') || alt.includes('icon')) continue;
|
|
331
|
+
if (width < 128 && height < 128) continue;
|
|
332
|
+
if (seen.has(src)) continue;
|
|
333
|
+
seen.add(src);
|
|
334
|
+
urls.push(src);
|
|
335
|
+
}
|
|
336
|
+
return urls;
|
|
337
|
+
})()
|
|
338
|
+
`);
|
|
339
|
+
}
|
|
340
|
+
export async function waitForGeminiImages(page, beforeUrls, timeoutSeconds) {
|
|
341
|
+
const beforeSet = new Set(beforeUrls);
|
|
342
|
+
const pollIntervalSeconds = 3;
|
|
343
|
+
const maxPolls = Math.max(1, Math.ceil(timeoutSeconds / pollIntervalSeconds));
|
|
344
|
+
let lastUrls = [];
|
|
345
|
+
let stableCount = 0;
|
|
346
|
+
for (let index = 0; index < maxPolls; index += 1) {
|
|
347
|
+
await page.wait(index === 0 ? 2 : pollIntervalSeconds);
|
|
348
|
+
const urls = (await getGeminiVisibleImageUrls(page)).filter((url) => !beforeSet.has(url));
|
|
349
|
+
if (urls.length === 0)
|
|
350
|
+
continue;
|
|
351
|
+
const key = urls.join('\n');
|
|
352
|
+
const prevKey = lastUrls.join('\n');
|
|
353
|
+
if (key == prevKey)
|
|
354
|
+
stableCount += 1;
|
|
355
|
+
else {
|
|
356
|
+
lastUrls = urls;
|
|
357
|
+
stableCount = 1;
|
|
358
|
+
}
|
|
359
|
+
if (stableCount >= 2 || index === maxPolls - 1)
|
|
360
|
+
return lastUrls;
|
|
361
|
+
}
|
|
362
|
+
return lastUrls;
|
|
363
|
+
}
|
|
364
|
+
export async function exportGeminiImages(page, urls) {
|
|
365
|
+
await ensureGeminiPage(page);
|
|
366
|
+
const urlsJson = JSON.stringify(urls);
|
|
367
|
+
return await page.evaluate(`
|
|
368
|
+
(async (targetUrls) => {
|
|
369
|
+
const blobToDataUrl = (blob) => new Promise((resolve, reject) => {
|
|
370
|
+
const reader = new FileReader();
|
|
371
|
+
reader.onloadend = () => resolve(String(reader.result || ''));
|
|
372
|
+
reader.onerror = () => reject(new Error('Failed to read blob'));
|
|
373
|
+
reader.readAsDataURL(blob);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const inferMime = (value, fallbackUrl) => {
|
|
377
|
+
if (value) return value;
|
|
378
|
+
const lower = String(fallbackUrl || '').toLowerCase();
|
|
379
|
+
if (lower.includes('.png')) return 'image/png';
|
|
380
|
+
if (lower.includes('.webp')) return 'image/webp';
|
|
381
|
+
if (lower.includes('.gif')) return 'image/gif';
|
|
382
|
+
return 'image/jpeg';
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const images = Array.from(document.querySelectorAll('main img'));
|
|
386
|
+
const results = [];
|
|
387
|
+
|
|
388
|
+
for (const targetUrl of targetUrls) {
|
|
389
|
+
const img = images.find((node) => (node.currentSrc || node.src || '') === targetUrl);
|
|
390
|
+
let dataUrl = '';
|
|
391
|
+
let mimeType = 'image/jpeg';
|
|
392
|
+
const width = img?.naturalWidth || img?.width || 0;
|
|
393
|
+
const height = img?.naturalHeight || img?.height || 0;
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
if (String(targetUrl).startsWith('data:')) {
|
|
397
|
+
dataUrl = String(targetUrl);
|
|
398
|
+
mimeType = (String(targetUrl).match(/^data:([^;]+);/i) || [])[1] || 'image/png';
|
|
399
|
+
} else {
|
|
400
|
+
const res = await fetch(String(targetUrl), { credentials: 'include' });
|
|
401
|
+
if (res.ok) {
|
|
402
|
+
const blob = await res.blob();
|
|
403
|
+
mimeType = inferMime(blob.type, targetUrl);
|
|
404
|
+
dataUrl = await blobToDataUrl(blob);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
} catch {}
|
|
408
|
+
|
|
409
|
+
if (!dataUrl && img instanceof HTMLImageElement) {
|
|
410
|
+
try {
|
|
411
|
+
const canvas = document.createElement('canvas');
|
|
412
|
+
canvas.width = img.naturalWidth || img.width;
|
|
413
|
+
canvas.height = img.naturalHeight || img.height;
|
|
414
|
+
const ctx = canvas.getContext('2d');
|
|
415
|
+
if (ctx) {
|
|
416
|
+
ctx.drawImage(img, 0, 0);
|
|
417
|
+
dataUrl = canvas.toDataURL('image/png');
|
|
418
|
+
mimeType = 'image/png';
|
|
419
|
+
}
|
|
420
|
+
} catch {}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (dataUrl) {
|
|
424
|
+
results.push({ url: String(targetUrl), dataUrl, mimeType, width, height });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return results;
|
|
429
|
+
})(${urlsJson})
|
|
430
|
+
`);
|
|
431
|
+
}
|
|
432
|
+
export async function waitForGeminiResponse(page, beforeLines, promptText, timeoutSeconds) {
|
|
433
|
+
const getCandidate = async () => {
|
|
434
|
+
const turns = await getGeminiVisibleTurns(page);
|
|
435
|
+
const assistantCandidate = [...turns].reverse().find((turn) => turn.Role === 'Assistant');
|
|
436
|
+
const visibleCandidate = assistantCandidate
|
|
437
|
+
? sanitizeGeminiResponseText(assistantCandidate.Text, promptText)
|
|
438
|
+
: '';
|
|
439
|
+
if (visibleCandidate && visibleCandidate !== promptText)
|
|
440
|
+
return visibleCandidate;
|
|
441
|
+
const lines = await getGeminiTranscriptLines(page);
|
|
442
|
+
return collectGeminiTranscriptAdditions(beforeLines, lines, promptText);
|
|
443
|
+
};
|
|
444
|
+
const pollIntervalSeconds = 2;
|
|
445
|
+
const maxPolls = Math.max(1, Math.ceil(timeoutSeconds / pollIntervalSeconds));
|
|
446
|
+
let lastCandidate = '';
|
|
447
|
+
let stableCount = 0;
|
|
448
|
+
for (let index = 0; index < maxPolls; index += 1) {
|
|
449
|
+
await page.wait(index === 0 ? 1.5 : pollIntervalSeconds);
|
|
450
|
+
const candidate = await getCandidate();
|
|
451
|
+
if (!candidate)
|
|
452
|
+
continue;
|
|
453
|
+
if (candidate === lastCandidate)
|
|
454
|
+
stableCount += 1;
|
|
455
|
+
else {
|
|
456
|
+
lastCandidate = candidate;
|
|
457
|
+
stableCount = 1;
|
|
458
|
+
}
|
|
459
|
+
if (stableCount >= 2 || index === maxPolls - 1)
|
|
460
|
+
return candidate;
|
|
461
|
+
}
|
|
462
|
+
return lastCandidate;
|
|
463
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { collectGeminiTranscriptAdditions, sanitizeGeminiResponseText } from './utils.js';
|
|
3
|
+
describe('sanitizeGeminiResponseText', () => {
|
|
4
|
+
it('strips a prompt echo only when it appears as a prefixed block', () => {
|
|
5
|
+
const prompt = 'Reply with the word opencli';
|
|
6
|
+
const value = `Reply with the word opencli\n\nopencli`;
|
|
7
|
+
expect(sanitizeGeminiResponseText(value, prompt)).toBe('opencli');
|
|
8
|
+
});
|
|
9
|
+
it('does not strip prompt text that appears later in a legitimate answer', () => {
|
|
10
|
+
const prompt = 'opencli';
|
|
11
|
+
const value = 'You asked about opencli, and opencli is the right keyword here.';
|
|
12
|
+
expect(sanitizeGeminiResponseText(value, prompt)).toBe(value);
|
|
13
|
+
});
|
|
14
|
+
it('removes known Gemini footer noise', () => {
|
|
15
|
+
const value = 'Answer body\nGemini can make mistakes.\nGoogle Terms';
|
|
16
|
+
expect(sanitizeGeminiResponseText(value, '')).toBe('Answer body');
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe('collectGeminiTranscriptAdditions', () => {
|
|
20
|
+
it('joins multiple new transcript lines instead of keeping only the last line', () => {
|
|
21
|
+
const before = ['Older answer'];
|
|
22
|
+
const current = ['Older answer', 'First new line', 'Second new line'];
|
|
23
|
+
expect(collectGeminiTranscriptAdditions(before, current, '')).toBe('First new line\nSecond new line');
|
|
24
|
+
});
|
|
25
|
+
it('filters prompt echoes out of transcript additions', () => {
|
|
26
|
+
const prompt = 'Tell me a haiku';
|
|
27
|
+
const before = ['Previous'];
|
|
28
|
+
const current = ['Previous', 'Tell me a haiku', 'Tell me a haiku\n\nSoft spring rain arrives'];
|
|
29
|
+
expect(collectGeminiTranscriptAdditions(before, current, prompt)).toBe('Soft spring rain arrives');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { getRegistry } from '../../registry.js';
|
|
3
|
-
import './bind-current.js';
|
|
4
3
|
import './get.js';
|
|
5
4
|
import './note-list.js';
|
|
5
|
+
import './open.js';
|
|
6
6
|
describe('notebooklm compatibility aliases', () => {
|
|
7
|
-
it('registers
|
|
8
|
-
expect(getRegistry().get('notebooklm/
|
|
7
|
+
it('registers select as a compatibility alias for open', () => {
|
|
8
|
+
expect(getRegistry().get('notebooklm/select')).toBe(getRegistry().get('notebooklm/open'));
|
|
9
9
|
});
|
|
10
10
|
it('registers metadata as a compatibility alias for get', () => {
|
|
11
11
|
expect(getRegistry().get('notebooklm/metadata')).toBe(getRegistry().get('notebooklm/get'));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { EmptyResultError } from '../../errors.js';
|
|
3
3
|
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getNotebooklmPageState, readCurrentNotebooklm, requireNotebooklmSession } from './utils.js';
|
|
5
5
|
cli({
|
|
6
6
|
site: NOTEBOOKLM_SITE,
|
|
7
7
|
name: 'current',
|
|
@@ -13,11 +13,10 @@ cli({
|
|
|
13
13
|
args: [],
|
|
14
14
|
columns: ['id', 'title', 'url', 'source'],
|
|
15
15
|
func: async (page) => {
|
|
16
|
-
await ensureNotebooklmNotebookBinding(page);
|
|
17
16
|
await requireNotebooklmSession(page);
|
|
18
17
|
const state = await getNotebooklmPageState(page);
|
|
19
18
|
if (state.kind !== 'notebook') {
|
|
20
|
-
throw new EmptyResultError('opencli notebooklm current', '
|
|
19
|
+
throw new EmptyResultError('opencli notebooklm current', 'No NotebookLM notebook is open in the automation workspace. Run `opencli notebooklm open <notebook>` first.');
|
|
21
20
|
}
|
|
22
21
|
const current = await readCurrentNotebooklm(page);
|
|
23
22
|
if (!current) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { EmptyResultError } from '../../errors.js';
|
|
3
3
|
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getNotebooklmDetailViaRpc, getNotebooklmPageState, readCurrentNotebooklm, requireNotebooklmSession, } from './utils.js';
|
|
5
5
|
cli({
|
|
6
6
|
site: NOTEBOOKLM_SITE,
|
|
7
7
|
name: 'get',
|
|
@@ -14,11 +14,10 @@ cli({
|
|
|
14
14
|
args: [],
|
|
15
15
|
columns: ['id', 'title', 'emoji', 'source_count', 'created_at', 'updated_at', 'url', 'source'],
|
|
16
16
|
func: async (page) => {
|
|
17
|
-
await ensureNotebooklmNotebookBinding(page);
|
|
18
17
|
await requireNotebooklmSession(page);
|
|
19
18
|
const state = await getNotebooklmPageState(page);
|
|
20
19
|
if (state.kind !== 'notebook') {
|
|
21
|
-
throw new EmptyResultError('opencli notebooklm get', '
|
|
20
|
+
throw new EmptyResultError('opencli notebooklm get', 'No NotebookLM notebook is open in the automation workspace. Run `opencli notebooklm open <notebook>` first.');
|
|
22
21
|
}
|
|
23
22
|
const rpcRow = await getNotebooklmDetailViaRpc(page).catch(() => null);
|
|
24
23
|
if (rpcRow)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { EmptyResultError } from '../../errors.js';
|
|
3
3
|
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getNotebooklmPageState, listNotebooklmHistoryViaRpc, requireNotebooklmSession, } from './utils.js';
|
|
5
5
|
cli({
|
|
6
6
|
site: NOTEBOOKLM_SITE,
|
|
7
7
|
name: 'history',
|
|
@@ -13,11 +13,10 @@ cli({
|
|
|
13
13
|
args: [],
|
|
14
14
|
columns: ['thread_id', 'item_count', 'preview', 'source', 'notebook_id', 'url'],
|
|
15
15
|
func: async (page) => {
|
|
16
|
-
await ensureNotebooklmNotebookBinding(page);
|
|
17
16
|
await requireNotebooklmSession(page);
|
|
18
17
|
const state = await getNotebooklmPageState(page);
|
|
19
18
|
if (state.kind !== 'notebook') {
|
|
20
|
-
throw new EmptyResultError('opencli notebooklm history', '
|
|
19
|
+
throw new EmptyResultError('opencli notebooklm history', 'No NotebookLM notebook is open in the automation workspace. Run `opencli notebooklm open <notebook>` first.');
|
|
21
20
|
}
|
|
22
21
|
const rows = await listNotebooklmHistoryViaRpc(page);
|
|
23
22
|
return rows;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { EmptyResultError } from '../../errors.js';
|
|
3
3
|
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getNotebooklmPageState, listNotebooklmNotesFromPage, requireNotebooklmSession, } from './utils.js';
|
|
5
5
|
cli({
|
|
6
6
|
site: NOTEBOOKLM_SITE,
|
|
7
7
|
name: 'note-list',
|
|
@@ -14,11 +14,10 @@ cli({
|
|
|
14
14
|
args: [],
|
|
15
15
|
columns: ['title', 'created_at', 'source', 'url'],
|
|
16
16
|
func: async (page) => {
|
|
17
|
-
await ensureNotebooklmNotebookBinding(page);
|
|
18
17
|
await requireNotebooklmSession(page);
|
|
19
18
|
const state = await getNotebooklmPageState(page);
|
|
20
19
|
if (state.kind !== 'notebook') {
|
|
21
|
-
throw new EmptyResultError('opencli notebooklm note-list', '
|
|
20
|
+
throw new EmptyResultError('opencli notebooklm note-list', 'No NotebookLM notebook is open in the automation workspace. Run `opencli notebooklm open <notebook>` first.');
|
|
22
21
|
}
|
|
23
22
|
const rows = await listNotebooklmNotesFromPage(page);
|
|
24
23
|
if (rows.length > 0)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { EmptyResultError } from '../../errors.js';
|
|
3
3
|
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
-
import {
|
|
4
|
+
import { findNotebooklmNoteRow, getNotebooklmPageState, listNotebooklmNotesFromPage, readNotebooklmVisibleNoteFromPage, requireNotebooklmSession, } from './utils.js';
|
|
5
5
|
function matchesNoteTitle(title, query) {
|
|
6
6
|
const needle = query.trim().toLowerCase();
|
|
7
7
|
if (!needle)
|
|
@@ -27,11 +27,10 @@ cli({
|
|
|
27
27
|
],
|
|
28
28
|
columns: ['title', 'content', 'source', 'url'],
|
|
29
29
|
func: async (page, kwargs) => {
|
|
30
|
-
await ensureNotebooklmNotebookBinding(page);
|
|
31
30
|
await requireNotebooklmSession(page);
|
|
32
31
|
const state = await getNotebooklmPageState(page);
|
|
33
32
|
if (state.kind !== 'notebook') {
|
|
34
|
-
throw new EmptyResultError('opencli notebooklm notes-get', '
|
|
33
|
+
throw new EmptyResultError('opencli notebooklm notes-get', 'No NotebookLM notebook is open in the automation workspace. Run `opencli notebooklm open <notebook>` first.');
|
|
35
34
|
}
|
|
36
35
|
const query = typeof kwargs.note === 'string' ? kwargs.note : String(kwargs.note ?? '');
|
|
37
36
|
const visible = await readNotebooklmVisibleNoteFromPage(page);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|