@jackwener/opencli 1.5.7 → 1.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +17 -1
  3. package/README.zh-CN.md +17 -1
  4. package/dist/browser/base-page.d.ts +48 -0
  5. package/dist/browser/base-page.js +160 -0
  6. package/dist/browser/cdp.js +4 -106
  7. package/dist/browser/daemon-client.d.ts +1 -7
  8. package/dist/browser/daemon-client.js +2 -9
  9. package/dist/browser/discover.d.ts +1 -4
  10. package/dist/browser/discover.js +1 -4
  11. package/dist/browser/errors.d.ts +4 -0
  12. package/dist/browser/errors.js +20 -0
  13. package/dist/browser/index.d.ts +1 -1
  14. package/dist/browser/index.js +1 -1
  15. package/dist/browser/page.d.ts +6 -35
  16. package/dist/browser/page.js +10 -189
  17. package/dist/browser/tabs.js +5 -5
  18. package/dist/browser.test.js +15 -15
  19. package/dist/cli-manifest.json +294 -22
  20. package/dist/clis/amazon/bestsellers.d.ts +21 -0
  21. package/dist/clis/amazon/bestsellers.js +130 -0
  22. package/dist/clis/amazon/bestsellers.test.js +20 -0
  23. package/dist/clis/amazon/discussion.d.ts +20 -0
  24. package/dist/clis/amazon/discussion.js +91 -0
  25. package/dist/clis/amazon/discussion.test.js +36 -0
  26. package/dist/clis/amazon/offer.d.ts +23 -0
  27. package/dist/clis/amazon/offer.js +140 -0
  28. package/dist/clis/amazon/offer.test.d.ts +1 -0
  29. package/dist/clis/amazon/offer.test.js +29 -0
  30. package/dist/clis/amazon/product.d.ts +18 -0
  31. package/dist/clis/amazon/product.js +92 -0
  32. package/dist/clis/amazon/product.test.d.ts +1 -0
  33. package/dist/clis/amazon/product.test.js +24 -0
  34. package/dist/clis/amazon/search.d.ts +18 -0
  35. package/dist/clis/amazon/search.js +87 -0
  36. package/dist/clis/amazon/search.test.d.ts +1 -0
  37. package/dist/clis/amazon/search.test.js +22 -0
  38. package/dist/clis/amazon/shared.d.ts +64 -0
  39. package/dist/clis/amazon/shared.js +255 -0
  40. package/dist/clis/amazon/shared.test.d.ts +1 -0
  41. package/dist/clis/amazon/shared.test.js +33 -0
  42. package/dist/clis/gemini/ask.d.ts +1 -0
  43. package/dist/clis/gemini/ask.js +40 -0
  44. package/dist/clis/gemini/image.d.ts +1 -0
  45. package/dist/clis/gemini/image.js +105 -0
  46. package/dist/clis/gemini/new.d.ts +1 -0
  47. package/dist/clis/gemini/new.js +20 -0
  48. package/dist/clis/gemini/utils.d.ts +34 -0
  49. package/dist/clis/gemini/utils.js +463 -0
  50. package/dist/clis/gemini/utils.test.d.ts +1 -0
  51. package/dist/clis/gemini/utils.test.js +31 -0
  52. package/dist/clis/notebooklm/compat.test.d.ts +1 -1
  53. package/dist/clis/notebooklm/compat.test.js +3 -3
  54. package/dist/clis/notebooklm/current.js +2 -3
  55. package/dist/clis/notebooklm/get.js +2 -3
  56. package/dist/clis/notebooklm/history.js +2 -3
  57. package/dist/clis/notebooklm/note-list.js +2 -3
  58. package/dist/clis/notebooklm/notes-get.js +2 -3
  59. package/dist/clis/notebooklm/open.d.ts +1 -0
  60. package/dist/clis/notebooklm/open.js +41 -0
  61. package/dist/clis/notebooklm/open.test.d.ts +1 -0
  62. package/dist/clis/notebooklm/open.test.js +63 -0
  63. package/dist/clis/notebooklm/source-fulltext.js +2 -3
  64. package/dist/clis/notebooklm/source-get.js +2 -3
  65. package/dist/clis/notebooklm/source-guide.js +2 -3
  66. package/dist/clis/notebooklm/source-list.js +2 -3
  67. package/dist/clis/notebooklm/status.js +1 -2
  68. package/dist/clis/notebooklm/summary.js +2 -3
  69. package/dist/clis/notebooklm/utils.d.ts +2 -1
  70. package/dist/clis/notebooklm/utils.js +20 -21
  71. package/dist/clis/xiaohongshu/creator-note-detail.test.js +11 -11
  72. package/dist/clis/xiaohongshu/creator-notes-summary.test.js +6 -6
  73. package/dist/clis/xiaohongshu/creator-notes.test.js +22 -22
  74. package/dist/commanderAdapter.js +6 -3
  75. package/dist/commanderAdapter.test.js +33 -0
  76. package/dist/commands/daemon.js +1 -1
  77. package/dist/commands/daemon.test.js +1 -1
  78. package/dist/doctor.d.ts +1 -2
  79. package/dist/doctor.js +7 -8
  80. package/dist/explore.js +1 -1
  81. package/dist/extension-manifest-regression.test.js +1 -0
  82. package/dist/output.js +28 -0
  83. package/dist/output.test.js +15 -0
  84. package/dist/pipeline/executor.js +2 -7
  85. package/dist/pipeline/steps/browser.js +1 -1
  86. package/dist/pipeline/template.js +25 -3
  87. package/dist/record.d.ts +50 -0
  88. package/dist/record.js +298 -57
  89. package/dist/record.test.d.ts +1 -0
  90. package/dist/record.test.js +293 -0
  91. package/dist/registry.d.ts +2 -0
  92. package/dist/registry.js +1 -0
  93. package/dist/registry.test.js +10 -0
  94. package/dist/runtime.js +3 -3
  95. package/dist/snapshotFormatter.d.ts +1 -1
  96. package/dist/snapshotFormatter.js +4 -4
  97. package/dist/snapshotFormatter.test.d.ts +1 -1
  98. package/dist/snapshotFormatter.test.js +2 -2
  99. package/dist/types.d.ts +3 -1
  100. package/dist/types.js +1 -1
  101. package/docs/.vitepress/config.mts +2 -0
  102. package/docs/adapters/browser/amazon.md +53 -0
  103. package/docs/adapters/browser/gemini.md +72 -0
  104. package/docs/adapters/browser/notebooklm.md +5 -5
  105. package/docs/adapters/index.md +3 -1
  106. package/extension/dist/background.js +614 -794
  107. package/extension/manifest.json +2 -1
  108. package/extension/src/background.test.ts +7 -163
  109. package/extension/src/background.ts +7 -156
  110. package/extension/src/cdp.test.ts +75 -0
  111. package/extension/src/cdp.ts +77 -3
  112. package/extension/src/protocol.ts +1 -5
  113. package/package.json +1 -1
  114. package/skills/opencli-explorer/SKILL.md +847 -0
  115. package/skills/opencli-oneshot/SKILL.md +216 -0
  116. package/skills/opencli-usage/SKILL.md +71 -0
  117. package/skills/opencli-usage/browser.md +429 -0
  118. package/skills/opencli-usage/desktop.md +118 -0
  119. package/skills/opencli-usage/plugins.md +82 -0
  120. package/skills/opencli-usage/public-api.md +149 -0
  121. package/src/browser/base-page.ts +197 -0
  122. package/src/browser/cdp.ts +7 -131
  123. package/src/browser/daemon-client.ts +3 -14
  124. package/src/browser/discover.ts +1 -4
  125. package/src/browser/errors.ts +22 -0
  126. package/src/browser/index.ts +1 -1
  127. package/src/browser/page.ts +13 -212
  128. package/src/browser/tabs.ts +5 -5
  129. package/src/browser.test.ts +15 -15
  130. package/src/clis/amazon/bestsellers.test.ts +22 -0
  131. package/src/clis/amazon/bestsellers.ts +180 -0
  132. package/src/clis/amazon/discussion.test.ts +38 -0
  133. package/src/clis/amazon/discussion.ts +131 -0
  134. package/src/clis/amazon/offer.test.ts +35 -0
  135. package/src/clis/amazon/offer.ts +185 -0
  136. package/src/clis/amazon/product.test.ts +26 -0
  137. package/src/clis/amazon/product.ts +131 -0
  138. package/src/clis/amazon/search.test.ts +24 -0
  139. package/src/clis/amazon/search.ts +128 -0
  140. package/src/clis/amazon/shared.test.ts +37 -0
  141. package/src/clis/amazon/shared.ts +316 -0
  142. package/src/clis/gemini/ask.ts +46 -0
  143. package/src/clis/gemini/image.ts +115 -0
  144. package/src/clis/gemini/new.ts +22 -0
  145. package/src/clis/gemini/utils.test.ts +36 -0
  146. package/src/clis/gemini/utils.ts +523 -0
  147. package/src/clis/notebooklm/compat.test.ts +3 -3
  148. package/src/clis/notebooklm/current.ts +2 -3
  149. package/src/clis/notebooklm/get.ts +1 -3
  150. package/src/clis/notebooklm/history.ts +1 -3
  151. package/src/clis/notebooklm/note-list.ts +1 -3
  152. package/src/clis/notebooklm/notes-get.ts +1 -3
  153. package/src/clis/notebooklm/open.test.ts +78 -0
  154. package/src/clis/notebooklm/open.ts +61 -0
  155. package/src/clis/notebooklm/source-fulltext.ts +1 -3
  156. package/src/clis/notebooklm/source-get.ts +1 -3
  157. package/src/clis/notebooklm/source-guide.ts +1 -3
  158. package/src/clis/notebooklm/source-list.ts +1 -3
  159. package/src/clis/notebooklm/status.ts +1 -2
  160. package/src/clis/notebooklm/summary.ts +1 -3
  161. package/src/clis/notebooklm/utils.ts +29 -20
  162. package/src/clis/xiaohongshu/creator-note-detail.test.ts +11 -11
  163. package/src/clis/xiaohongshu/creator-notes-summary.test.ts +6 -6
  164. package/src/clis/xiaohongshu/creator-notes.test.ts +22 -22
  165. package/src/commanderAdapter.test.ts +47 -0
  166. package/src/commanderAdapter.ts +7 -3
  167. package/src/commands/daemon.test.ts +1 -1
  168. package/src/commands/daemon.ts +1 -1
  169. package/src/doctor.ts +7 -8
  170. package/src/explore.ts +1 -1
  171. package/src/extension-manifest-regression.test.ts +1 -0
  172. package/src/output.test.ts +17 -0
  173. package/src/output.ts +27 -0
  174. package/src/pipeline/executor.ts +2 -7
  175. package/src/pipeline/steps/browser.ts +1 -1
  176. package/src/pipeline/template.ts +27 -4
  177. package/src/record.test.ts +362 -0
  178. package/src/record.ts +341 -62
  179. package/src/registry.test.ts +12 -0
  180. package/src/registry.ts +3 -0
  181. package/src/runtime.ts +3 -3
  182. package/src/snapshotFormatter.test.ts +2 -2
  183. package/src/snapshotFormatter.ts +4 -4
  184. package/src/types.ts +3 -1
  185. package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
  186. package/.agents/workflows/cross-project-adapter-migration.md +0 -54
  187. package/SKILL.md +0 -879
  188. package/dist/clis/notebooklm/bind-current.js +0 -29
  189. package/dist/clis/notebooklm/bind-current.test.d.ts +0 -1
  190. package/dist/clis/notebooklm/bind-current.test.js +0 -35
  191. package/dist/clis/notebooklm/binding.test.js +0 -44
  192. package/src/clis/notebooklm/bind-current.test.ts +0 -43
  193. package/src/clis/notebooklm/bind-current.ts +0 -36
  194. package/src/clis/notebooklm/binding.test.ts +0 -53
  195. /package/dist/browser/{mcp.d.ts → bridge.d.ts} +0 -0
  196. /package/dist/browser/{mcp.js → bridge.js} +0 -0
  197. /package/dist/clis/{notebooklm/bind-current.d.ts → amazon/bestsellers.test.d.ts} +0 -0
  198. /package/dist/clis/{notebooklm/binding.test.d.ts → amazon/discussion.test.d.ts} +0 -0
  199. /package/src/browser/{mcp.ts → bridge.ts} +0 -0
@@ -0,0 +1,255 @@
1
+ import { ArgumentError, CommandExecutionError } from '../../errors.js';
2
+ export const SITE = 'amazon';
3
+ export const DOMAIN = 'amazon.com';
4
+ export const HOME_URL = 'https://www.amazon.com/';
5
+ export const BESTSELLERS_URL = 'https://www.amazon.com/Best-Sellers/zgbs';
6
+ export const SEARCH_URL_PREFIX = 'https://www.amazon.com/s?k=';
7
+ export const PRODUCT_URL_PREFIX = 'https://www.amazon.com/dp/';
8
+ export const DISCUSSION_URL_PREFIX = 'https://www.amazon.com/product-reviews/';
9
+ export const STRATEGY = 'cookie';
10
+ export const PRIMARY_PRICE_SELECTORS = [
11
+ '#corePrice_feature_div .a-offscreen',
12
+ '#corePriceDisplay_desktop_feature_div .a-offscreen',
13
+ '#corePrice_desktop .a-offscreen',
14
+ '#apex_desktop .a-offscreen',
15
+ '#newAccordionRow_0 .a-offscreen',
16
+ '#price_inside_buybox',
17
+ '#priceblock_ourprice',
18
+ '#priceblock_dealprice',
19
+ '#tp_price_block_total_price_ww',
20
+ ];
21
+ const ROBOT_TEXT_PATTERNS = [
22
+ 'Sorry, we just need to make sure you\'re not a robot',
23
+ 'Enter the characters you see below',
24
+ 'Type the characters you see in this image',
25
+ 'To discuss automated access to Amazon data please contact',
26
+ ];
27
+ export function cleanText(value) {
28
+ return typeof value === 'string'
29
+ ? value.replace(/\u00a0/g, ' ').replace(/\s+/g, ' ').trim()
30
+ : '';
31
+ }
32
+ export function cleanMultilineText(value) {
33
+ return typeof value === 'string'
34
+ ? value
35
+ .replace(/\u00a0/g, ' ')
36
+ .split('\n')
37
+ .map((line) => line.replace(/\s+/g, ' ').trim())
38
+ .filter(Boolean)
39
+ .join('\n')
40
+ : '';
41
+ }
42
+ export function uniqueNonEmpty(values) {
43
+ return [...new Set(values.map((value) => cleanText(value)).filter(Boolean))];
44
+ }
45
+ export function buildProvenance(sourceUrl) {
46
+ return {
47
+ source_url: sourceUrl,
48
+ fetched_at: new Date().toISOString(),
49
+ strategy: STRATEGY,
50
+ };
51
+ }
52
+ export function buildSearchUrl(query) {
53
+ const normalized = cleanText(query);
54
+ if (!normalized) {
55
+ throw new ArgumentError('amazon search query cannot be empty');
56
+ }
57
+ return `${SEARCH_URL_PREFIX}${encodeURIComponent(normalized)}`;
58
+ }
59
+ export function extractAsin(input) {
60
+ const normalized = cleanText(input);
61
+ if (!normalized)
62
+ return null;
63
+ if (/^[A-Z0-9]{10}$/i.test(normalized)) {
64
+ return normalized.toUpperCase();
65
+ }
66
+ const match = normalized.match(/\/(?:dp|gp\/product|product-reviews)\/([A-Z0-9]{10})/i);
67
+ return match ? match[1].toUpperCase() : null;
68
+ }
69
+ export function buildProductUrl(input) {
70
+ const asin = extractAsin(input);
71
+ if (!asin) {
72
+ throw new ArgumentError('amazon product expects an ASIN or product URL', 'Example: opencli amazon product B0FJS72893');
73
+ }
74
+ return `${PRODUCT_URL_PREFIX}${asin}`;
75
+ }
76
+ export function buildDiscussionUrl(input) {
77
+ const asin = extractAsin(input);
78
+ if (!asin) {
79
+ throw new ArgumentError('amazon discussion expects an ASIN or product URL', 'Example: opencli amazon discussion B0FJS72893');
80
+ }
81
+ return `${DISCUSSION_URL_PREFIX}${asin}`;
82
+ }
83
+ export function resolveBestsellersUrl(input) {
84
+ const normalized = cleanText(input);
85
+ if (!normalized)
86
+ return BESTSELLERS_URL;
87
+ if (normalized === 'root')
88
+ return BESTSELLERS_URL;
89
+ if (normalized.startsWith('/')) {
90
+ return new URL(normalized, HOME_URL).toString();
91
+ }
92
+ if (/^https?:\/\//i.test(normalized)) {
93
+ return canonicalizeAmazonUrl(normalized);
94
+ }
95
+ if (normalized.includes('/zgbs/')) {
96
+ return canonicalizeAmazonUrl(`https://${normalized.replace(/^\/+/, '')}`);
97
+ }
98
+ throw new ArgumentError('amazon bestsellers expects a best sellers URL or /zgbs path', 'Example: opencli amazon bestsellers https://www.amazon.com/Best-Sellers/zgbs');
99
+ }
100
+ export function canonicalizeAmazonUrl(input) {
101
+ try {
102
+ const url = new URL(input);
103
+ if (!url.hostname.endsWith(DOMAIN)) {
104
+ throw new Error('not-amazon');
105
+ }
106
+ return url.toString();
107
+ }
108
+ catch {
109
+ throw new ArgumentError('Invalid Amazon URL');
110
+ }
111
+ }
112
+ export function toAbsoluteAmazonUrl(value) {
113
+ const normalized = cleanText(value);
114
+ if (!normalized)
115
+ return null;
116
+ try {
117
+ return new URL(normalized, HOME_URL).toString();
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ export function normalizeProductUrl(value) {
124
+ const normalized = cleanText(value);
125
+ const asin = extractAsin(normalized);
126
+ if (asin)
127
+ return buildProductUrl(asin);
128
+ return toAbsoluteAmazonUrl(normalized);
129
+ }
130
+ export function parsePriceText(text) {
131
+ const normalized = cleanText(text);
132
+ const match = normalized.match(/([$€£])\s*(\d+(?:,\d{3})*(?:\.\d+)?)/);
133
+ if (!match) {
134
+ return {
135
+ price_text: normalized || null,
136
+ price_value: null,
137
+ currency: null,
138
+ };
139
+ }
140
+ const currencyMap = {
141
+ '$': 'USD',
142
+ '€': 'EUR',
143
+ '£': 'GBP',
144
+ };
145
+ return {
146
+ price_text: `${match[1]}${match[2]}`,
147
+ price_value: Number.parseFloat(match[2].replace(/,/g, '')),
148
+ currency: currencyMap[match[1]] ?? null,
149
+ };
150
+ }
151
+ export function parseRatingValue(text) {
152
+ const normalized = cleanText(text);
153
+ const match = normalized.match(/(\d+(?:\.\d+)?)\s*out of 5/i);
154
+ return match ? Number.parseFloat(match[1]) : null;
155
+ }
156
+ export function parseReviewCount(text) {
157
+ const normalized = cleanText(text);
158
+ const compactMatch = normalized.match(/(\d+(?:\.\d+)?)\s*([kKmM])/);
159
+ if (compactMatch) {
160
+ const value = Number.parseFloat(compactMatch[1]);
161
+ const multiplier = /m/i.test(compactMatch[2]) ? 1_000_000 : 1_000;
162
+ return Number.isFinite(value) ? Math.round(value * multiplier) : null;
163
+ }
164
+ const match = normalized.match(/([\d,]+)/);
165
+ return match ? Number.parseInt(match[1].replace(/,/g, ''), 10) : null;
166
+ }
167
+ export function extractReviewCountFromCardText(text) {
168
+ const normalized = cleanMultilineText(text);
169
+ const match = normalized.match(/out of 5 stars(?:, rating details)?\s*([\d,]+)/i);
170
+ if (match)
171
+ return match[1];
172
+ const numericLine = normalized
173
+ .split('\n')
174
+ .map((line) => cleanText(line))
175
+ .find((line) => /^[\d,]+$/.test(line));
176
+ return numericLine ?? null;
177
+ }
178
+ export function isAmazonEntity(text) {
179
+ const normalized = cleanText(text).toLowerCase();
180
+ return normalized.includes('amazon');
181
+ }
182
+ export function firstMeaningfulLine(text) {
183
+ return cleanMultilineText(text)
184
+ .split('\n')
185
+ .map((line) => cleanText(line))
186
+ .find(Boolean)
187
+ ?? '';
188
+ }
189
+ export function trimRatingPrefix(text) {
190
+ const normalized = cleanText(text);
191
+ if (!normalized)
192
+ return null;
193
+ return normalized.replace(/^\d+(?:\.\d+)?\s*out of 5 stars\s*/i, '').trim() || normalized;
194
+ }
195
+ export function isRobotState(state) {
196
+ const title = cleanText(state.title);
197
+ const bodyText = cleanMultilineText(state.body_text);
198
+ return ROBOT_TEXT_PATTERNS.some((pattern) => title.includes(pattern) || bodyText.includes(pattern));
199
+ }
200
+ export function buildChallengeHint(action) {
201
+ return [
202
+ `Open a clean Amazon ${action} page in the shared Chrome profile and clear any robot check first.`,
203
+ 'If you are using CDP, set OPENCLI_CDP_TARGET=amazon.com and avoid parallel Amazon commands against the same browser target.',
204
+ ].join(' ');
205
+ }
206
+ export async function readPageState(page) {
207
+ const result = await page.evaluate(`
208
+ (() => ({
209
+ href: window.location.href,
210
+ title: document.title || '',
211
+ body_text: document.body ? document.body.innerText || '' : '',
212
+ }))()
213
+ `);
214
+ return {
215
+ href: cleanText(result.href),
216
+ title: cleanText(result.title),
217
+ body_text: cleanMultilineText(result.body_text),
218
+ };
219
+ }
220
+ export async function gotoAndReadState(page, url, settleMs = 2500, action = 'page') {
221
+ try {
222
+ await page.goto(url, { settleMs });
223
+ await page.wait(1.5);
224
+ return await readPageState(page);
225
+ }
226
+ catch (error) {
227
+ const message = error instanceof Error ? error.message : String(error);
228
+ if (message.includes('Inspected target navigated or closed')
229
+ || message.includes('Cannot find context with specified id')
230
+ || message.includes('Target closed')) {
231
+ throw new CommandExecutionError(`amazon ${action} navigation lost the current browser target`, `${buildChallengeHint(action)} If CDP is attached to a stale tab, open a fresh Amazon tab and retry.`);
232
+ }
233
+ throw error;
234
+ }
235
+ }
236
+ export function assertUsableState(state, action) {
237
+ if (!isRobotState(state))
238
+ return;
239
+ throw new CommandExecutionError(`amazon ${action} hit a robot check`, buildChallengeHint(action));
240
+ }
241
+ export const __test__ = {
242
+ buildSearchUrl,
243
+ extractAsin,
244
+ buildProductUrl,
245
+ buildDiscussionUrl,
246
+ resolveBestsellersUrl,
247
+ parsePriceText,
248
+ parseRatingValue,
249
+ parseReviewCount,
250
+ extractReviewCountFromCardText,
251
+ isAmazonEntity,
252
+ trimRatingPrefix,
253
+ isRobotState,
254
+ PRIMARY_PRICE_SELECTORS,
255
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { __test__ } from './shared.js';
3
+ describe('amazon shared helpers', () => {
4
+ it('builds canonical product and discussion URLs from ASINs and product URLs', () => {
5
+ expect(__test__.buildProductUrl('B0FJS72893')).toBe('https://www.amazon.com/dp/B0FJS72893');
6
+ expect(__test__.buildProductUrl('https://www.amazon.com/dp/B0FJS72893/ref=something')).toBe('https://www.amazon.com/dp/B0FJS72893');
7
+ expect(__test__.buildDiscussionUrl('https://www.amazon.com/dp/B0FJS72893')).toBe('https://www.amazon.com/product-reviews/B0FJS72893');
8
+ });
9
+ it('parses price, rating, and review-count text', () => {
10
+ expect(__test__.parsePriceText('1 offer from $34.11')).toEqual({
11
+ price_text: '$34.11',
12
+ price_value: 34.11,
13
+ currency: 'USD',
14
+ });
15
+ expect(__test__.parseRatingValue('3.9 out of 5 stars, rating details')).toBe(3.9);
16
+ expect(__test__.parseReviewCount('27 global ratings')).toBe(27);
17
+ expect(__test__.parseReviewCount('(2.9K)')).toBe(2900);
18
+ expect(__test__.parseReviewCount('1.2M global ratings')).toBe(1200000);
19
+ expect(__test__.extractReviewCountFromCardText('Desk Shelf\n4.3 out of 5 stars\n435\n$25.92')).toBe('435');
20
+ });
21
+ it('recognizes robot checks and Amazon-owned merchants', () => {
22
+ expect(__test__.isAmazonEntity('Ships from Amazon')).toBe(true);
23
+ expect(__test__.trimRatingPrefix('5.0 out of 5 stars Great value and quality')).toBe('Great value and quality');
24
+ expect(__test__.isRobotState({
25
+ title: 'Robot Check',
26
+ body_text: 'Sorry, we just need to make sure you\'re not a robot',
27
+ })).toBe(true);
28
+ });
29
+ it('requires a real best-sellers URL or path', () => {
30
+ expect(__test__.resolveBestsellersUrl('/Best-Sellers/zgbs')).toBe('https://www.amazon.com/Best-Sellers/zgbs');
31
+ expect(() => __test__.resolveBestsellersUrl('desk shelf organizer')).toThrow('amazon bestsellers expects a best sellers URL or /zgbs path');
32
+ });
33
+ });
@@ -0,0 +1 @@
1
+ export declare const askCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,40 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { GEMINI_DOMAIN, getGeminiTranscriptLines, sendGeminiMessage, startNewGeminiChat, waitForGeminiResponse } from './utils.js';
3
+ function normalizeBooleanFlag(value) {
4
+ if (typeof value === 'boolean')
5
+ return value;
6
+ const normalized = String(value ?? '').trim().toLowerCase();
7
+ return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
8
+ }
9
+ const NO_RESPONSE_PREFIX = '[NO RESPONSE]';
10
+ export const askCommand = cli({
11
+ site: 'gemini',
12
+ name: 'ask',
13
+ description: 'Send a prompt to Gemini and return only the assistant response',
14
+ domain: GEMINI_DOMAIN,
15
+ strategy: Strategy.COOKIE,
16
+ browser: true,
17
+ navigateBefore: false,
18
+ defaultFormat: 'plain',
19
+ timeoutSeconds: 180,
20
+ args: [
21
+ { name: 'prompt', required: true, positional: true, help: 'Prompt to send' },
22
+ { name: 'timeout', required: false, help: 'Max seconds to wait (default: 60)', default: '60' },
23
+ { name: 'new', required: false, help: 'Start a new chat first (true/false, default: false)', default: 'false' },
24
+ ],
25
+ columns: ['response'],
26
+ func: async (page, kwargs) => {
27
+ const prompt = kwargs.prompt;
28
+ const timeout = parseInt(kwargs.timeout, 10) || 60;
29
+ const startFresh = normalizeBooleanFlag(kwargs.new);
30
+ if (startFresh)
31
+ await startNewGeminiChat(page);
32
+ const beforeLines = await getGeminiTranscriptLines(page);
33
+ await sendGeminiMessage(page, prompt);
34
+ const response = await waitForGeminiResponse(page, beforeLines, prompt, timeout);
35
+ if (!response) {
36
+ return [{ response: `💬 ${NO_RESPONSE_PREFIX} No Gemini response within ${timeout}s.` }];
37
+ }
38
+ return [{ response: `💬 ${response}` }];
39
+ },
40
+ });
@@ -0,0 +1 @@
1
+ export declare const imageCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,105 @@
1
+ import * as os from 'node:os';
2
+ import * as path from 'node:path';
3
+ import { cli, Strategy } from '../../registry.js';
4
+ import { saveBase64ToFile } from '../../utils.js';
5
+ import { GEMINI_DOMAIN, exportGeminiImages, getGeminiVisibleImageUrls, sendGeminiMessage, startNewGeminiChat, waitForGeminiImages } from './utils.js';
6
+ function extFromMime(mime) {
7
+ if (mime.includes('png'))
8
+ return '.png';
9
+ if (mime.includes('webp'))
10
+ return '.webp';
11
+ if (mime.includes('gif'))
12
+ return '.gif';
13
+ return '.jpg';
14
+ }
15
+ function normalizeBooleanFlag(value) {
16
+ if (typeof value === 'boolean')
17
+ return value;
18
+ const normalized = String(value ?? '').trim().toLowerCase();
19
+ return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
20
+ }
21
+ function displayPath(filePath) {
22
+ const home = os.homedir();
23
+ return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath;
24
+ }
25
+ function buildImagePrompt(prompt, options) {
26
+ const extras = [];
27
+ if (options.ratio)
28
+ extras.push(`aspect ratio ${options.ratio}`);
29
+ if (options.style)
30
+ extras.push(`style ${options.style}`);
31
+ if (extras.length === 0)
32
+ return prompt;
33
+ return `${prompt}
34
+
35
+ Image requirements: ${extras.join(', ')}.`;
36
+ }
37
+ function normalizeRatio(value) {
38
+ const normalized = value.trim();
39
+ const allowed = new Set(['1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3']);
40
+ return allowed.has(normalized) ? normalized : '1:1';
41
+ }
42
+ async function currentGeminiLink(page) {
43
+ const url = await page.evaluate('window.location.href').catch(() => '');
44
+ return typeof url === 'string' && url ? url : 'https://gemini.google.com/app';
45
+ }
46
+ export const imageCommand = cli({
47
+ site: 'gemini',
48
+ name: 'image',
49
+ description: 'Generate images with Gemini web and save them locally',
50
+ domain: GEMINI_DOMAIN,
51
+ strategy: Strategy.COOKIE,
52
+ browser: true,
53
+ navigateBefore: false,
54
+ defaultFormat: 'plain',
55
+ timeoutSeconds: 240,
56
+ args: [
57
+ { name: 'prompt', positional: true, required: true, help: 'Image prompt to send to Gemini' },
58
+ { name: 'rt', default: '1:1', help: 'Ratio shorthand for aspect ratio (1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3)' },
59
+ { name: 'st', default: '', help: 'Style shorthand, e.g. anime, icon, watercolor' },
60
+ { name: 'op', default: path.join(os.homedir(), 'tmp', 'gemini-images'), help: 'Output directory shorthand' },
61
+ { name: 'sd', type: 'boolean', default: false, help: 'Skip download shorthand; only show Gemini page link' },
62
+ ],
63
+ columns: ['status', 'file', 'link'],
64
+ func: async (page, kwargs) => {
65
+ const prompt = kwargs.prompt;
66
+ const ratio = normalizeRatio(String(kwargs.rt ?? '1:1'));
67
+ const style = String(kwargs.st ?? '').trim();
68
+ const outputDir = kwargs.op || path.join(os.homedir(), 'tmp', 'gemini-images');
69
+ const timeout = 120;
70
+ const startFresh = true;
71
+ const skipDownloadRaw = kwargs.sd;
72
+ const skipDownload = skipDownloadRaw === '' || skipDownloadRaw === true || normalizeBooleanFlag(skipDownloadRaw);
73
+ const effectivePrompt = buildImagePrompt(prompt, {
74
+ ratio,
75
+ style: style || undefined,
76
+ });
77
+ if (startFresh)
78
+ await startNewGeminiChat(page);
79
+ const beforeUrls = await getGeminiVisibleImageUrls(page);
80
+ await sendGeminiMessage(page, effectivePrompt);
81
+ const urls = await waitForGeminiImages(page, beforeUrls, timeout);
82
+ const link = await currentGeminiLink(page);
83
+ if (!urls.length) {
84
+ return [{ status: '⚠️ no-images', file: '📁 -', link: `🔗 ${link}` }];
85
+ }
86
+ if (skipDownload) {
87
+ return [{ status: '🎨 generated', file: '📁 -', link: `🔗 ${link}` }];
88
+ }
89
+ const assets = await exportGeminiImages(page, urls);
90
+ if (!assets.length) {
91
+ return [{ status: '⚠️ export-failed', file: '📁 -', link: `🔗 ${link}` }];
92
+ }
93
+ const stamp = Date.now();
94
+ const results = [];
95
+ for (let index = 0; index < assets.length; index += 1) {
96
+ const asset = assets[index];
97
+ const base64 = asset.dataUrl.replace(/^data:[^;]+;base64,/, '');
98
+ const suffix = assets.length > 1 ? `_${index + 1}` : '';
99
+ const filePath = path.join(outputDir, `gemini_${stamp}${suffix}${extFromMime(asset.mimeType)}`);
100
+ await saveBase64ToFile(base64, filePath);
101
+ results.push({ status: '✅ saved', file: `📁 ${displayPath(filePath)}`, link: `🔗 ${link}` });
102
+ }
103
+ return results;
104
+ },
105
+ });
@@ -0,0 +1 @@
1
+ export declare const newCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,20 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { GEMINI_DOMAIN, startNewGeminiChat } from './utils.js';
3
+ export const newCommand = cli({
4
+ site: 'gemini',
5
+ name: 'new',
6
+ description: 'Start a new conversation in Gemini web chat',
7
+ domain: GEMINI_DOMAIN,
8
+ strategy: Strategy.COOKIE,
9
+ browser: true,
10
+ navigateBefore: false,
11
+ args: [],
12
+ columns: ['Status', 'Action'],
13
+ func: async (page) => {
14
+ const action = await startNewGeminiChat(page);
15
+ return [{
16
+ Status: 'Success',
17
+ Action: action === 'navigate' ? 'Reloaded /app as fallback' : 'Clicked New chat',
18
+ }];
19
+ },
20
+ });
@@ -0,0 +1,34 @@
1
+ import type { IPage } from '../../types.js';
2
+ export declare const GEMINI_DOMAIN = "gemini.google.com";
3
+ export declare const GEMINI_APP_URL = "https://gemini.google.com/app";
4
+ export interface GeminiPageState {
5
+ url: string;
6
+ title: string;
7
+ isSignedIn: boolean | null;
8
+ composerLabel: string;
9
+ canSend: boolean;
10
+ }
11
+ export interface GeminiTurn {
12
+ Role: 'User' | 'Assistant' | 'System';
13
+ Text: string;
14
+ }
15
+ export declare function sanitizeGeminiResponseText(value: string, promptText: string): string;
16
+ export declare function collectGeminiTranscriptAdditions(beforeLines: string[], currentLines: string[], promptText: string): string;
17
+ export declare function isOnGemini(page: IPage): Promise<boolean>;
18
+ export declare function ensureGeminiPage(page: IPage): Promise<void>;
19
+ export declare function getGeminiPageState(page: IPage): Promise<GeminiPageState>;
20
+ export declare function startNewGeminiChat(page: IPage): Promise<'clicked' | 'navigate'>;
21
+ export declare function getGeminiVisibleTurns(page: IPage): Promise<GeminiTurn[]>;
22
+ export declare function getGeminiTranscriptLines(page: IPage): Promise<string[]>;
23
+ export declare function sendGeminiMessage(page: IPage, text: string): Promise<'button' | 'enter'>;
24
+ export declare function getGeminiVisibleImageUrls(page: IPage): Promise<string[]>;
25
+ export declare function waitForGeminiImages(page: IPage, beforeUrls: string[], timeoutSeconds: number): Promise<string[]>;
26
+ export interface GeminiImageAsset {
27
+ url: string;
28
+ dataUrl: string;
29
+ mimeType: string;
30
+ width: number;
31
+ height: number;
32
+ }
33
+ export declare function exportGeminiImages(page: IPage, urls: string[]): Promise<GeminiImageAsset[]>;
34
+ export declare function waitForGeminiResponse(page: IPage, beforeLines: string[], promptText: string, timeoutSeconds: number): Promise<string>;