@jackwener/opencli 1.5.8 → 1.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +17 -1
  3. package/README.zh-CN.md +17 -1
  4. package/dist/browser/base-page.d.ts +48 -0
  5. package/dist/browser/base-page.js +160 -0
  6. package/dist/browser/cdp.js +4 -106
  7. package/dist/browser/daemon-client.d.ts +1 -7
  8. package/dist/browser/daemon-client.js +2 -9
  9. package/dist/browser/discover.d.ts +1 -4
  10. package/dist/browser/discover.js +1 -4
  11. package/dist/browser/errors.d.ts +4 -0
  12. package/dist/browser/errors.js +20 -0
  13. package/dist/browser/index.d.ts +1 -1
  14. package/dist/browser/index.js +1 -1
  15. package/dist/browser/page.d.ts +6 -35
  16. package/dist/browser/page.js +10 -189
  17. package/dist/browser/tabs.js +5 -5
  18. package/dist/browser.test.js +15 -15
  19. package/dist/cli-manifest.json +294 -22
  20. package/dist/clis/amazon/bestsellers.d.ts +21 -0
  21. package/dist/clis/amazon/bestsellers.js +130 -0
  22. package/dist/clis/amazon/bestsellers.test.js +20 -0
  23. package/dist/clis/amazon/discussion.d.ts +20 -0
  24. package/dist/clis/amazon/discussion.js +91 -0
  25. package/dist/clis/amazon/discussion.test.js +36 -0
  26. package/dist/clis/amazon/offer.d.ts +23 -0
  27. package/dist/clis/amazon/offer.js +140 -0
  28. package/dist/clis/amazon/offer.test.d.ts +1 -0
  29. package/dist/clis/amazon/offer.test.js +29 -0
  30. package/dist/clis/amazon/product.d.ts +18 -0
  31. package/dist/clis/amazon/product.js +92 -0
  32. package/dist/clis/amazon/product.test.d.ts +1 -0
  33. package/dist/clis/amazon/product.test.js +24 -0
  34. package/dist/clis/amazon/search.d.ts +18 -0
  35. package/dist/clis/amazon/search.js +87 -0
  36. package/dist/clis/amazon/search.test.d.ts +1 -0
  37. package/dist/clis/amazon/search.test.js +22 -0
  38. package/dist/clis/amazon/shared.d.ts +64 -0
  39. package/dist/clis/amazon/shared.js +255 -0
  40. package/dist/clis/amazon/shared.test.d.ts +1 -0
  41. package/dist/clis/amazon/shared.test.js +33 -0
  42. package/dist/clis/gemini/ask.d.ts +1 -0
  43. package/dist/clis/gemini/ask.js +40 -0
  44. package/dist/clis/gemini/image.d.ts +1 -0
  45. package/dist/clis/gemini/image.js +105 -0
  46. package/dist/clis/gemini/new.d.ts +1 -0
  47. package/dist/clis/gemini/new.js +20 -0
  48. package/dist/clis/gemini/utils.d.ts +34 -0
  49. package/dist/clis/gemini/utils.js +463 -0
  50. package/dist/clis/gemini/utils.test.d.ts +1 -0
  51. package/dist/clis/gemini/utils.test.js +31 -0
  52. package/dist/clis/notebooklm/compat.test.d.ts +1 -1
  53. package/dist/clis/notebooklm/compat.test.js +3 -3
  54. package/dist/clis/notebooklm/current.js +2 -3
  55. package/dist/clis/notebooklm/get.js +2 -3
  56. package/dist/clis/notebooklm/history.js +2 -3
  57. package/dist/clis/notebooklm/note-list.js +2 -3
  58. package/dist/clis/notebooklm/notes-get.js +2 -3
  59. package/dist/clis/notebooklm/open.d.ts +1 -0
  60. package/dist/clis/notebooklm/open.js +41 -0
  61. package/dist/clis/notebooklm/open.test.d.ts +1 -0
  62. package/dist/clis/notebooklm/open.test.js +63 -0
  63. package/dist/clis/notebooklm/source-fulltext.js +2 -3
  64. package/dist/clis/notebooklm/source-get.js +2 -3
  65. package/dist/clis/notebooklm/source-guide.js +2 -3
  66. package/dist/clis/notebooklm/source-list.js +2 -3
  67. package/dist/clis/notebooklm/status.js +1 -2
  68. package/dist/clis/notebooklm/summary.js +2 -3
  69. package/dist/clis/notebooklm/utils.d.ts +2 -1
  70. package/dist/clis/notebooklm/utils.js +20 -21
  71. package/dist/clis/xiaohongshu/creator-note-detail.test.js +11 -11
  72. package/dist/clis/xiaohongshu/creator-notes-summary.test.js +6 -6
  73. package/dist/clis/xiaohongshu/creator-notes.test.js +22 -22
  74. package/dist/commanderAdapter.js +6 -3
  75. package/dist/commanderAdapter.test.js +33 -0
  76. package/dist/commands/daemon.js +1 -1
  77. package/dist/commands/daemon.test.js +1 -1
  78. package/dist/doctor.d.ts +1 -2
  79. package/dist/doctor.js +7 -8
  80. package/dist/explore.js +1 -1
  81. package/dist/output.js +28 -0
  82. package/dist/output.test.js +15 -0
  83. package/dist/pipeline/executor.js +2 -7
  84. package/dist/pipeline/steps/browser.js +1 -1
  85. package/dist/pipeline/template.js +25 -3
  86. package/dist/record.d.ts +50 -0
  87. package/dist/record.js +298 -57
  88. package/dist/record.test.d.ts +1 -0
  89. package/dist/record.test.js +293 -0
  90. package/dist/registry.d.ts +2 -0
  91. package/dist/registry.js +1 -0
  92. package/dist/registry.test.js +10 -0
  93. package/dist/runtime.js +3 -3
  94. package/dist/snapshotFormatter.d.ts +1 -1
  95. package/dist/snapshotFormatter.js +4 -4
  96. package/dist/snapshotFormatter.test.d.ts +1 -1
  97. package/dist/snapshotFormatter.test.js +2 -2
  98. package/dist/types.d.ts +3 -1
  99. package/dist/types.js +1 -1
  100. package/docs/.vitepress/config.mts +2 -0
  101. package/docs/adapters/browser/amazon.md +53 -0
  102. package/docs/adapters/browser/gemini.md +72 -0
  103. package/docs/adapters/browser/notebooklm.md +5 -5
  104. package/docs/adapters/index.md +3 -1
  105. package/extension/dist/background.js +5 -143
  106. package/extension/src/background.test.ts +7 -163
  107. package/extension/src/background.ts +7 -157
  108. package/extension/src/protocol.ts +1 -5
  109. package/package.json +1 -1
  110. package/skills/opencli-explorer/SKILL.md +847 -0
  111. package/skills/opencli-oneshot/SKILL.md +216 -0
  112. package/skills/opencli-usage/SKILL.md +71 -0
  113. package/skills/opencli-usage/browser.md +429 -0
  114. package/skills/opencli-usage/desktop.md +118 -0
  115. package/skills/opencli-usage/plugins.md +82 -0
  116. package/skills/opencli-usage/public-api.md +149 -0
  117. package/src/browser/base-page.ts +197 -0
  118. package/src/browser/cdp.ts +7 -131
  119. package/src/browser/daemon-client.ts +3 -14
  120. package/src/browser/discover.ts +1 -4
  121. package/src/browser/errors.ts +22 -0
  122. package/src/browser/index.ts +1 -1
  123. package/src/browser/page.ts +13 -212
  124. package/src/browser/tabs.ts +5 -5
  125. package/src/browser.test.ts +15 -15
  126. package/src/clis/amazon/bestsellers.test.ts +22 -0
  127. package/src/clis/amazon/bestsellers.ts +180 -0
  128. package/src/clis/amazon/discussion.test.ts +38 -0
  129. package/src/clis/amazon/discussion.ts +131 -0
  130. package/src/clis/amazon/offer.test.ts +35 -0
  131. package/src/clis/amazon/offer.ts +185 -0
  132. package/src/clis/amazon/product.test.ts +26 -0
  133. package/src/clis/amazon/product.ts +131 -0
  134. package/src/clis/amazon/search.test.ts +24 -0
  135. package/src/clis/amazon/search.ts +128 -0
  136. package/src/clis/amazon/shared.test.ts +37 -0
  137. package/src/clis/amazon/shared.ts +316 -0
  138. package/src/clis/gemini/ask.ts +46 -0
  139. package/src/clis/gemini/image.ts +115 -0
  140. package/src/clis/gemini/new.ts +22 -0
  141. package/src/clis/gemini/utils.test.ts +36 -0
  142. package/src/clis/gemini/utils.ts +523 -0
  143. package/src/clis/notebooklm/compat.test.ts +3 -3
  144. package/src/clis/notebooklm/current.ts +2 -3
  145. package/src/clis/notebooklm/get.ts +1 -3
  146. package/src/clis/notebooklm/history.ts +1 -3
  147. package/src/clis/notebooklm/note-list.ts +1 -3
  148. package/src/clis/notebooklm/notes-get.ts +1 -3
  149. package/src/clis/notebooklm/open.test.ts +78 -0
  150. package/src/clis/notebooklm/open.ts +61 -0
  151. package/src/clis/notebooklm/source-fulltext.ts +1 -3
  152. package/src/clis/notebooklm/source-get.ts +1 -3
  153. package/src/clis/notebooklm/source-guide.ts +1 -3
  154. package/src/clis/notebooklm/source-list.ts +1 -3
  155. package/src/clis/notebooklm/status.ts +1 -2
  156. package/src/clis/notebooklm/summary.ts +1 -3
  157. package/src/clis/notebooklm/utils.ts +29 -20
  158. package/src/clis/xiaohongshu/creator-note-detail.test.ts +11 -11
  159. package/src/clis/xiaohongshu/creator-notes-summary.test.ts +6 -6
  160. package/src/clis/xiaohongshu/creator-notes.test.ts +22 -22
  161. package/src/commanderAdapter.test.ts +47 -0
  162. package/src/commanderAdapter.ts +7 -3
  163. package/src/commands/daemon.test.ts +1 -1
  164. package/src/commands/daemon.ts +1 -1
  165. package/src/doctor.ts +7 -8
  166. package/src/explore.ts +1 -1
  167. package/src/output.test.ts +17 -0
  168. package/src/output.ts +27 -0
  169. package/src/pipeline/executor.ts +2 -7
  170. package/src/pipeline/steps/browser.ts +1 -1
  171. package/src/pipeline/template.ts +27 -4
  172. package/src/record.test.ts +362 -0
  173. package/src/record.ts +341 -62
  174. package/src/registry.test.ts +12 -0
  175. package/src/registry.ts +3 -0
  176. package/src/runtime.ts +3 -3
  177. package/src/snapshotFormatter.test.ts +2 -2
  178. package/src/snapshotFormatter.ts +4 -4
  179. package/src/types.ts +3 -1
  180. package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
  181. package/.agents/workflows/cross-project-adapter-migration.md +0 -54
  182. package/SKILL.md +0 -879
  183. package/dist/clis/notebooklm/bind-current.js +0 -29
  184. package/dist/clis/notebooklm/bind-current.test.d.ts +0 -1
  185. package/dist/clis/notebooklm/bind-current.test.js +0 -35
  186. package/dist/clis/notebooklm/binding.test.js +0 -44
  187. package/src/clis/notebooklm/bind-current.test.ts +0 -43
  188. package/src/clis/notebooklm/bind-current.ts +0 -36
  189. package/src/clis/notebooklm/binding.test.ts +0 -53
  190. /package/dist/browser/{mcp.d.ts → bridge.d.ts} +0 -0
  191. /package/dist/browser/{mcp.js → bridge.js} +0 -0
  192. /package/dist/clis/{notebooklm/bind-current.d.ts → amazon/bestsellers.test.d.ts} +0 -0
  193. /package/dist/clis/{notebooklm/binding.test.d.ts → amazon/discussion.test.d.ts} +0 -0
  194. /package/src/browser/{mcp.ts → bridge.ts} +0 -0
@@ -0,0 +1,87 @@
1
+ import { CommandExecutionError } from '../../errors.js';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import { buildProvenance, buildSearchUrl, cleanText, extractAsin, normalizeProductUrl, parsePriceText, parseRatingValue, parseReviewCount, assertUsableState, gotoAndReadState, } from './shared.js';
4
+ function normalizeSearchCandidate(candidate, rank, sourceUrl) {
5
+ const productUrl = normalizeProductUrl(candidate.href);
6
+ const asin = extractAsin(candidate.asin ?? '') ?? extractAsin(productUrl ?? '') ?? null;
7
+ const price = parsePriceText(candidate.price_text);
8
+ const ratingText = cleanText(candidate.rating_text) || null;
9
+ const reviewCountText = cleanText(candidate.review_count_text) || null;
10
+ const provenance = buildProvenance(sourceUrl);
11
+ return {
12
+ rank,
13
+ asin,
14
+ title: cleanText(candidate.title) || null,
15
+ product_url: productUrl,
16
+ ...provenance,
17
+ price_text: price.price_text,
18
+ price_value: price.price_value,
19
+ currency: price.currency,
20
+ rating_text: ratingText,
21
+ rating_value: parseRatingValue(ratingText),
22
+ review_count_text: reviewCountText,
23
+ review_count: parseReviewCount(reviewCountText),
24
+ is_sponsored: candidate.sponsored === true,
25
+ badges: (candidate.badge_texts ?? []).map((value) => cleanText(value)).filter(Boolean),
26
+ };
27
+ }
28
+ async function readSearchPayload(page, query) {
29
+ const url = buildSearchUrl(query);
30
+ const state = await gotoAndReadState(page, url, 2500, 'search');
31
+ assertUsableState(state, 'search');
32
+ return await page.evaluate(`
33
+ (() => ({
34
+ href: window.location.href,
35
+ cards: Array.from(document.querySelectorAll('[data-component-type="s-search-result"]'))
36
+ .map((card) => ({
37
+ asin: card.getAttribute('data-asin') || '',
38
+ title: card.querySelector('h2')?.textContent || '',
39
+ href: card.querySelector('a.a-link-normal[href*="/dp/"]')?.href || '',
40
+ price_text: card.querySelector('.a-price .a-offscreen')?.textContent || '',
41
+ rating_text: card.querySelector('[aria-label*="out of 5 stars"]')?.getAttribute('aria-label') || '',
42
+ review_count_text: card.querySelector('a[href*="#customerReviews"]')?.textContent || '',
43
+ sponsored: /sponsored/i.test(card.innerText || ''),
44
+ badge_texts: Array.from(card.querySelectorAll('.a-badge-text')).map((node) => node.textContent || ''),
45
+ })),
46
+ }))()
47
+ `);
48
+ }
49
+ cli({
50
+ site: 'amazon',
51
+ name: 'search',
52
+ description: 'Amazon search results for product discovery and coarse filtering',
53
+ domain: 'amazon.com',
54
+ strategy: Strategy.COOKIE,
55
+ navigateBefore: false,
56
+ args: [
57
+ {
58
+ name: 'query',
59
+ required: true,
60
+ positional: true,
61
+ help: 'Search query, for example "desk shelf organizer"',
62
+ },
63
+ {
64
+ name: 'limit',
65
+ type: 'int',
66
+ default: 20,
67
+ help: 'Maximum number of results to return (default 20)',
68
+ },
69
+ ],
70
+ columns: ['rank', 'asin', 'title', 'price_text', 'rating_value', 'review_count'],
71
+ func: async (page, kwargs) => {
72
+ const query = String(kwargs.query ?? '');
73
+ const limit = Math.max(1, Number(kwargs.limit) || 20);
74
+ const payload = await readSearchPayload(page, query);
75
+ const sourceUrl = cleanText(payload.href) || buildSearchUrl(query);
76
+ const cards = (payload.cards ?? [])
77
+ .filter((card) => cleanText(card.asin) && cleanText(card.title))
78
+ .slice(0, limit);
79
+ if (cards.length === 0) {
80
+ throw new CommandExecutionError('amazon search did not expose any product cards', 'The search page may have changed or hit a robot check. Open the same query in Chrome, verify the page is visible, and retry.');
81
+ }
82
+ return cards.map((card, index) => normalizeSearchCandidate(card, index + 1, sourceUrl));
83
+ },
84
+ });
85
+ export const __test__ = {
86
+ normalizeSearchCandidate,
87
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { __test__ } from './search.js';
3
+ describe('amazon search normalization', () => {
4
+ it('normalizes search cards into research-friendly fields', () => {
5
+ const result = __test__.normalizeSearchCandidate({
6
+ asin: 'B0FJS72893',
7
+ title: 'White Desktop Shelf Organizer for Top of Desk',
8
+ href: 'https://www.amazon.com/KVTUKIAIT-White-Desktop-Shelf-Organizer/dp/B0FJS72893/ref=sr_1_1',
9
+ price_text: '$15.99',
10
+ rating_text: '3.9 out of 5 stars, rating details',
11
+ review_count_text: '(27)',
12
+ sponsored: false,
13
+ badge_texts: ['Limited time deal'],
14
+ }, 1, 'https://www.amazon.com/s?k=desk+shelf+organizer');
15
+ expect(result.asin).toBe('B0FJS72893');
16
+ expect(result.product_url).toBe('https://www.amazon.com/dp/B0FJS72893');
17
+ expect(result.price_value).toBe(15.99);
18
+ expect(result.rating_value).toBe(3.9);
19
+ expect(result.review_count).toBe(27);
20
+ expect(result.badges).toEqual(['Limited time deal']);
21
+ });
22
+ });
@@ -0,0 +1,64 @@
1
+ import type { IPage } from '../../types.js';
2
+ export declare const SITE = "amazon";
3
+ export declare const DOMAIN = "amazon.com";
4
+ export declare const HOME_URL = "https://www.amazon.com/";
5
+ export declare const BESTSELLERS_URL = "https://www.amazon.com/Best-Sellers/zgbs";
6
+ export declare const SEARCH_URL_PREFIX = "https://www.amazon.com/s?k=";
7
+ export declare const PRODUCT_URL_PREFIX = "https://www.amazon.com/dp/";
8
+ export declare const DISCUSSION_URL_PREFIX = "https://www.amazon.com/product-reviews/";
9
+ export declare const STRATEGY = "cookie";
10
+ export declare const PRIMARY_PRICE_SELECTORS: string[];
11
+ export interface ProvenanceFields {
12
+ source_url: string;
13
+ fetched_at: string;
14
+ strategy: string;
15
+ }
16
+ export interface PageState {
17
+ href: string;
18
+ title: string;
19
+ body_text: string;
20
+ }
21
+ export interface PriceValue {
22
+ price_text: string | null;
23
+ price_value: number | null;
24
+ currency: string | null;
25
+ }
26
+ export declare function cleanText(value: unknown): string;
27
+ export declare function cleanMultilineText(value: unknown): string;
28
+ export declare function uniqueNonEmpty(values: Array<string | null | undefined>): string[];
29
+ export declare function buildProvenance(sourceUrl: string): ProvenanceFields;
30
+ export declare function buildSearchUrl(query: string): string;
31
+ export declare function extractAsin(input: string): string | null;
32
+ export declare function buildProductUrl(input: string): string;
33
+ export declare function buildDiscussionUrl(input: string): string;
34
+ export declare function resolveBestsellersUrl(input?: string): string;
35
+ export declare function canonicalizeAmazonUrl(input: string): string;
36
+ export declare function toAbsoluteAmazonUrl(value: string | null | undefined): string | null;
37
+ export declare function normalizeProductUrl(value: string | null | undefined): string | null;
38
+ export declare function parsePriceText(text: string | null | undefined): PriceValue;
39
+ export declare function parseRatingValue(text: string | null | undefined): number | null;
40
+ export declare function parseReviewCount(text: string | null | undefined): number | null;
41
+ export declare function extractReviewCountFromCardText(text: string | null | undefined): string | null;
42
+ export declare function isAmazonEntity(text: string | null | undefined): boolean;
43
+ export declare function firstMeaningfulLine(text: string | null | undefined): string;
44
+ export declare function trimRatingPrefix(text: string | null | undefined): string | null;
45
+ export declare function isRobotState(state: Partial<PageState>): boolean;
46
+ export declare function buildChallengeHint(action: string): string;
47
+ export declare function readPageState(page: IPage): Promise<PageState>;
48
+ export declare function gotoAndReadState(page: IPage, url: string, settleMs?: number, action?: string): Promise<PageState>;
49
+ export declare function assertUsableState(state: PageState, action: string): void;
50
+ export declare const __test__: {
51
+ buildSearchUrl: typeof buildSearchUrl;
52
+ extractAsin: typeof extractAsin;
53
+ buildProductUrl: typeof buildProductUrl;
54
+ buildDiscussionUrl: typeof buildDiscussionUrl;
55
+ resolveBestsellersUrl: typeof resolveBestsellersUrl;
56
+ parsePriceText: typeof parsePriceText;
57
+ parseRatingValue: typeof parseRatingValue;
58
+ parseReviewCount: typeof parseReviewCount;
59
+ extractReviewCountFromCardText: typeof extractReviewCountFromCardText;
60
+ isAmazonEntity: typeof isAmazonEntity;
61
+ trimRatingPrefix: typeof trimRatingPrefix;
62
+ isRobotState: typeof isRobotState;
63
+ PRIMARY_PRICE_SELECTORS: string[];
64
+ };
@@ -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
+ });