@agentrhq/webcmd 0.4.2 → 0.4.3

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 (59) hide show
  1. package/cli-manifest.json +755 -174
  2. package/clis/_shared/site-auth.js +0 -1
  3. package/clis/_shared/site-auth.test.js +0 -1
  4. package/clis/amazon-in/checkout.js +0 -1
  5. package/clis/blinkit/checkout.js +0 -1
  6. package/clis/blinkit/place-order.js +0 -1
  7. package/clis/chatgpt/model.js +2 -2
  8. package/clis/chatgpt/model.test.js +7 -1
  9. package/clis/chatgpt/utils.js +8 -0
  10. package/clis/chatgpt/utils.test.js +92 -1
  11. package/clis/district/checkout.js +0 -1
  12. package/clis/district/seats.js +0 -1
  13. package/clis/district/set-location.js +0 -1
  14. package/clis/district/showtimes.js +0 -1
  15. package/clis/google/images.js +456 -0
  16. package/clis/google/images.test.js +375 -0
  17. package/clis/instagram/user.js +5 -13
  18. package/clis/instagram/user.test.js +66 -0
  19. package/clis/mercury/check-login.js +0 -1
  20. package/clis/mercury/reimbursement-draft.js +0 -1
  21. package/clis/practo/book-confirm.js +0 -1
  22. package/clis/practo/cancel.js +0 -1
  23. package/clis/trip/attraction.js +74 -0
  24. package/clis/trip/car.js +74 -0
  25. package/clis/trip/deals.js +61 -0
  26. package/clis/trip/flight-round.js +88 -0
  27. package/clis/trip/flight.js +83 -0
  28. package/clis/trip/hotel-search.js +80 -0
  29. package/clis/trip/hotel.js +54 -0
  30. package/clis/trip/package.js +93 -0
  31. package/clis/trip/search.js +43 -0
  32. package/clis/trip/tour.js +84 -0
  33. package/clis/trip/train.js +76 -0
  34. package/clis/trip/transfer.js +82 -0
  35. package/clis/trip/trip.test.js +1420 -0
  36. package/clis/trip/utils.js +911 -0
  37. package/dist/src/build-manifest.js +0 -1
  38. package/dist/src/build-manifest.test.js +4 -0
  39. package/dist/src/cli.js +7 -7
  40. package/dist/src/cli.test.js +26 -10
  41. package/dist/src/command-presentation.js +1 -1
  42. package/dist/src/command-presentation.test.js +3 -0
  43. package/dist/src/command-surface.js +1 -1
  44. package/dist/src/command-surface.test.js +4 -0
  45. package/dist/src/commands/auth.js +0 -2
  46. package/dist/src/commands/auth.test.js +0 -2
  47. package/dist/src/discovery.js +0 -1
  48. package/dist/src/execution.js +3 -3
  49. package/dist/src/execution.test.js +68 -15
  50. package/dist/src/hosted/browser-args.js +2 -2
  51. package/dist/src/hosted/browser-args.test.js +10 -0
  52. package/dist/src/hosted/client.js +2 -2
  53. package/dist/src/manifest-types.d.ts +0 -2
  54. package/dist/src/registry.d.ts +0 -2
  55. package/dist/src/registry.js +0 -1
  56. package/hosted-contract.json +767 -3
  57. package/package.json +2 -2
  58. package/skills/webcmd-browser/SKILL.md +2 -1
  59. package/skills/webcmd-usage/SKILL.md +1 -1
@@ -0,0 +1,456 @@
1
+ /**
2
+ * Google Images search via browser DOM extraction.
3
+ *
4
+ * Google Images has no stable public JSON API. This adapter navigates the
5
+ * public image search UI and extracts visible image candidates, preferring
6
+ * image URLs exposed in /imgres links and falling back to rendered thumbnails.
7
+ */
8
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
9
+ import { CommandExecutionError, EmptyResultError } from '@agentrhq/webcmd/errors';
10
+ import {
11
+ requireBoundedInteger,
12
+ requireRows,
13
+ requireSearchQuery,
14
+ runBrowserStep,
15
+ toHttpsUrl,
16
+ unwrapBrowserResult,
17
+ } from '../_shared/search-adapter.js';
18
+
19
+ function isNavigationRejected(error) {
20
+ return /Navigation rejected/i.test(String(error?.message || error));
21
+ }
22
+
23
+ async function navigateGoogleImages(page, url) {
24
+ try {
25
+ await page.goto(url);
26
+ return;
27
+ } catch (error) {
28
+ if (!isNavigationRejected(error)) {
29
+ throw error;
30
+ }
31
+ }
32
+
33
+ if (typeof page.closeWindow === 'function') {
34
+ await page.closeWindow().catch(() => {});
35
+ }
36
+
37
+ try {
38
+ await page.goto(url);
39
+ return;
40
+ } catch (error) {
41
+ if (!isNavigationRejected(error)) {
42
+ throw error;
43
+ }
44
+ if (typeof page.newTab === 'function' && typeof page.setActivePage === 'function') {
45
+ const pageId = await page.newTab(url);
46
+ if (pageId) {
47
+ await page.setActivePage(pageId);
48
+ return;
49
+ }
50
+ }
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ export async function extractGoogleImageRows(maxRows = 20, resolveOriginal = true, docArg) {
56
+ var doc = docArg || document;
57
+ var baseHref = doc.URL || (typeof location !== 'undefined' ? location.href : 'https://www.google.com/search?tbm=isch');
58
+ var candidates = [];
59
+ var seen = {};
60
+
61
+ function text(value) {
62
+ return String(value || '').replace(/\s+/g, ' ').trim();
63
+ }
64
+
65
+ function firstSrcsetUrl(srcset) {
66
+ var raw = String(srcset || '').trim();
67
+ if (!raw)
68
+ return '';
69
+ var first = raw.split(',')[0] || '';
70
+ return (first.trim().split(/\s+/)[0] || '').trim();
71
+ }
72
+
73
+ function cleanHttpUrl(value) {
74
+ var raw = text(value);
75
+ if (!raw)
76
+ return '';
77
+ try {
78
+ var url = new URL(raw, baseHref);
79
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
80
+ return '';
81
+ return url.href;
82
+ }
83
+ catch {
84
+ return '';
85
+ }
86
+ }
87
+
88
+ function sourceName(sourceUrl) {
89
+ try {
90
+ return new URL(sourceUrl).hostname.replace(/^www\./, '');
91
+ }
92
+ catch {
93
+ return '';
94
+ }
95
+ }
96
+
97
+ function pause(ms) {
98
+ return new Promise(function(resolve) { setTimeout(resolve, ms); });
99
+ }
100
+
101
+ function readDimension(img, attr, naturalProp) {
102
+ var rect = typeof img.getBoundingClientRect === 'function' ? img.getBoundingClientRect() : null;
103
+ var rectValue = attr === 'width' ? rect && rect.width : rect && rect.height;
104
+ var raw = img.getAttribute(attr) || img.style[attr] || img[naturalProp] || rectValue;
105
+ var n = Number.parseInt(String(raw || '').replace(/[^\d]/g, ''), 10);
106
+ return Number.isFinite(n) && n > 0 ? n : null;
107
+ }
108
+
109
+ function isGoogleInternalUrl(value) {
110
+ try {
111
+ var host = new URL(value).hostname.replace(/^www\./, '');
112
+ return host === 'google.com' || host.endsWith('.google.com') || host === 'gstatic.com' || host.endsWith('.gstatic.com');
113
+ }
114
+ catch {
115
+ return true;
116
+ }
117
+ }
118
+
119
+ function firstExternalLink(scope) {
120
+ if (!scope)
121
+ return '';
122
+ var links = scope.querySelectorAll('a[href]');
123
+ for (var i = 0; i < links.length; i += 1) {
124
+ var candidate = cleanHttpUrl(links[i].getAttribute('href'));
125
+ if (candidate && !isGoogleInternalUrl(candidate))
126
+ return candidate;
127
+ }
128
+ return '';
129
+ }
130
+
131
+ function resultScope(img) {
132
+ var node = img;
133
+ for (var depth = 0; node && depth < 9; depth += 1) {
134
+ if (node.querySelector && firstExternalLink(node))
135
+ return node;
136
+ node = node.parentElement;
137
+ }
138
+ return img.closest('[data-ri], [data-ved], div');
139
+ }
140
+
141
+ function imageSrc(img) {
142
+ return cleanHttpUrl(img.currentSrc)
143
+ || cleanHttpUrl(img.getAttribute('src'))
144
+ || cleanHttpUrl(img.getAttribute('data-src'))
145
+ || cleanHttpUrl(img.getAttribute('data-iurl'))
146
+ || cleanHttpUrl(firstSrcsetUrl(img.getAttribute('srcset')));
147
+ }
148
+
149
+ function add(anchor, img, scope) {
150
+ if (!img || candidates.length >= maxRows)
151
+ return;
152
+
153
+ var href = anchor ? anchor.getAttribute('href') || anchor.href || '' : '';
154
+ var imageUrl = '';
155
+ var sourceUrl = '';
156
+ if (href) {
157
+ try {
158
+ var parsed = new URL(href, baseHref);
159
+ imageUrl = cleanHttpUrl(parsed.searchParams.get('imgurl'));
160
+ sourceUrl = cleanHttpUrl(parsed.searchParams.get('imgrefurl'))
161
+ || cleanHttpUrl(parsed.searchParams.get('url'));
162
+ }
163
+ catch {
164
+ // Ignore malformed Google redirect links and fall through to DOM attributes.
165
+ }
166
+ }
167
+
168
+ var thumbnailUrl = imageSrc(img);
169
+ imageUrl = imageUrl
170
+ || cleanHttpUrl(scope && scope.getAttribute('data-iurl'))
171
+ || cleanHttpUrl(img.getAttribute('data-iurl'))
172
+ || thumbnailUrl;
173
+
174
+ if (!imageUrl)
175
+ return;
176
+
177
+ sourceUrl = sourceUrl
178
+ || cleanHttpUrl(scope && scope.getAttribute('data-lpage'))
179
+ || firstExternalLink(scope)
180
+ || (href && href.indexOf('/imgres') === -1 ? cleanHttpUrl(href) : '');
181
+
182
+ var width = readDimension(img, 'width', 'naturalWidth');
183
+ var height = readDimension(img, 'height', 'naturalHeight');
184
+ if (width !== null && height !== null && (width < 80 || height < 80))
185
+ return;
186
+ if (sourceUrl && isGoogleInternalUrl(sourceUrl) && href.indexOf('/imgres') === -1)
187
+ return;
188
+
189
+ var dedupeKey = imageUrl || (sourceUrl + '|' + text(img.getAttribute('alt')));
190
+ if (!dedupeKey || seen[dedupeKey])
191
+ return;
192
+ seen[dedupeKey] = true;
193
+
194
+ var title = text(img.getAttribute('alt'))
195
+ || text(anchor && anchor.getAttribute('aria-label'))
196
+ || text(scope && scope.getAttribute('aria-label'))
197
+ || text(scope && scope.getAttribute('title'))
198
+ || sourceName(sourceUrl);
199
+
200
+ candidates.push({
201
+ target: img.closest('[role="button"]') || anchor || img,
202
+ row: [
203
+ title,
204
+ imageUrl,
205
+ thumbnailUrl,
206
+ sourceUrl,
207
+ sourceName(sourceUrl),
208
+ width,
209
+ height,
210
+ ],
211
+ });
212
+ }
213
+
214
+ var anchors = doc.querySelectorAll('#rso a[href*="imgres"], #rso a[href*="source=images"], #rso a[href*="tbm=isch"], #rso a[href*="udm=2"], #islrg a[href*="imgres"], #islrg a[href*="source=images"], #islrg a[href*="tbm=isch"], #islrg a[href*="udm=2"]');
215
+ for (var i = 0; i < anchors.length && candidates.length < maxRows; i += 1) {
216
+ var anchor = anchors[i];
217
+ var scopedImg = anchor.querySelector('img');
218
+ if (!scopedImg) {
219
+ var nearby = anchor.closest('[data-ri], [data-ved], #rso > div, div');
220
+ scopedImg = nearby ? nearby.querySelector('img') : null;
221
+ }
222
+ add(anchor, scopedImg, scopedImg ? resultScope(scopedImg) : anchor.closest('[data-ri], [data-ved], div'));
223
+ }
224
+
225
+ var fallbackImages = doc.querySelectorAll('#rso img, #islrg img, #center_col img, #rcnt img');
226
+ for (var j = 0; j < fallbackImages.length && candidates.length < maxRows; j += 1) {
227
+ var img = fallbackImages[j];
228
+ var scope = resultScope(img);
229
+ var link = img.closest('a[href]');
230
+ add(link, img, scope);
231
+ }
232
+
233
+ function parseImgresLink(href, row) {
234
+ try {
235
+ var parsed = new URL(href, baseHref);
236
+ var originalUrl = cleanHttpUrl(parsed.searchParams.get('imgurl'));
237
+ if (!originalUrl || isGoogleInternalUrl(originalUrl))
238
+ return null;
239
+ var refUrl = cleanHttpUrl(parsed.searchParams.get('imgrefurl')) || cleanHttpUrl(parsed.searchParams.get('url'));
240
+ var score = 1;
241
+ if (refUrl && row[3] && refUrl === row[3])
242
+ score += 10;
243
+ if (refUrl && row[3] && sourceName(refUrl) === sourceName(row[3]))
244
+ score += 5;
245
+ var width = Number.parseInt(parsed.searchParams.get('w') || '', 10);
246
+ var height = Number.parseInt(parsed.searchParams.get('h') || '', 10);
247
+ if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0)
248
+ score += 2;
249
+ return [
250
+ originalUrl,
251
+ refUrl,
252
+ sourceName(refUrl),
253
+ Number.isFinite(width) && width > 0 ? width : null,
254
+ Number.isFinite(height) && height > 0 ? height : null,
255
+ score,
256
+ ];
257
+ }
258
+ catch {
259
+ return null;
260
+ }
261
+ }
262
+
263
+ function bestOriginal(row) {
264
+ var best = null;
265
+ var links = doc.querySelectorAll('a[href*="imgurl="]');
266
+ for (var i = 0; i < links.length; i += 1) {
267
+ var candidate = parseImgresLink(links[i].getAttribute('href') || links[i].href || '', row);
268
+ if (!candidate)
269
+ continue;
270
+ if (!best || candidate[5] > best[5])
271
+ best = candidate;
272
+ }
273
+ return best;
274
+ }
275
+
276
+ async function resolveOriginalFor(candidate) {
277
+ if (!candidate || !candidate.target)
278
+ return;
279
+ var target = candidate.target;
280
+ try {
281
+ if (typeof target.scrollIntoView === 'function')
282
+ target.scrollIntoView({ block: 'center', inline: 'center' });
283
+ var view = doc.defaultView || (typeof window !== 'undefined' ? window : null);
284
+ var MouseEventCtor = view && view.MouseEvent;
285
+ if (MouseEventCtor) {
286
+ target.dispatchEvent(new MouseEventCtor('mouseover', { bubbles: true, view: view }));
287
+ target.dispatchEvent(new MouseEventCtor('mousedown', { bubbles: true, view: view }));
288
+ target.dispatchEvent(new MouseEventCtor('mouseup', { bubbles: true, view: view }));
289
+ }
290
+ target.click();
291
+ }
292
+ catch {
293
+ return;
294
+ }
295
+
296
+ var original = null;
297
+ for (var attempt = 0; attempt < 8; attempt += 1) {
298
+ await pause(250);
299
+ original = bestOriginal(candidate.row);
300
+ if (original)
301
+ break;
302
+ }
303
+ if (!original)
304
+ return;
305
+
306
+ candidate.row[1] = original[0];
307
+ if (original[1])
308
+ candidate.row[3] = original[1];
309
+ if (original[2])
310
+ candidate.row[4] = original[2];
311
+ if (original[3])
312
+ candidate.row[5] = original[3];
313
+ if (original[4])
314
+ candidate.row[6] = original[4];
315
+ }
316
+
317
+ if (resolveOriginal) {
318
+ for (var k = 0; k < candidates.length; k += 1) {
319
+ await resolveOriginalFor(candidates[k]);
320
+ }
321
+ }
322
+
323
+ return candidates.map(function(candidate) { return candidate.row; });
324
+ }
325
+
326
+ export function inspectGoogleImagesPage(docArg) {
327
+ var doc = docArg || document;
328
+ var bodyText = String(doc.body && doc.body.textContent || '').replace(/\s+/g, ' ').trim().toLowerCase();
329
+ var hasResultRoot = !!doc.querySelector('#rso, #islrg, #center_col, #rcnt');
330
+ var hasImageCandidates = !!doc.querySelector('#rso img, #islrg img, #center_col img, #rcnt img');
331
+ var captchaOrConsent = /unusual traffic|captcha|not a robot|verify you'?re not a robot|detected unusual traffic|our systems have detected/.test(bodyText)
332
+ || /before you continue|accept all|reject all|consent/.test(bodyText)
333
+ || !!doc.querySelector('form[action*="/sorry/"], iframe[src*="recaptcha"], #captcha, input[name="captcha"]');
334
+ var explicitNoResults = /did not match any documents|no results found|no images found/.test(bodyText);
335
+ return { hasResultRoot: hasResultRoot, hasImageCandidates: hasImageCandidates, captchaOrConsent: captchaOrConsent, explicitNoResults: explicitNoResults };
336
+ }
337
+
338
+ function isGoogleInternalResultUrl(value) {
339
+ try {
340
+ const host = new URL(value).hostname.replace(/^www\./, '');
341
+ return host === 'google.com'
342
+ || host.endsWith('.google.com')
343
+ || host === 'gstatic.com'
344
+ || host.endsWith('.gstatic.com')
345
+ || host === 'googleusercontent.com'
346
+ || host.endsWith('.googleusercontent.com');
347
+ } catch {
348
+ return true;
349
+ }
350
+ }
351
+
352
+ function normalizeImageRows(rawRows, query, limit) {
353
+ const rows = rawRows.slice(0, limit).map((row, index) => {
354
+ if (!Array.isArray(row)) {
355
+ throw new CommandExecutionError('google images returned an unexpected row shape.');
356
+ }
357
+ const imageUrl = toHttpsUrl(row[1], 'https://www.google.com');
358
+ const thumbnailUrl = toHttpsUrl(row[2], 'https://www.google.com');
359
+ const sourceUrl = toHttpsUrl(row[3], 'https://www.google.com');
360
+ if (!imageUrl || !sourceUrl || isGoogleInternalResultUrl(sourceUrl)) {
361
+ throw new CommandExecutionError('google images returned a result row without stable external image/source identity.');
362
+ }
363
+ const source = String(row[4] || '').trim() || new URL(sourceUrl).hostname.replace(/^www\./, '');
364
+ const title = String(row[0] || '').trim() || source;
365
+ return {
366
+ rank: index + 1,
367
+ title,
368
+ imageUrl,
369
+ thumbnailUrl,
370
+ sourceUrl,
371
+ source,
372
+ width: Number.isFinite(Number(row[5])) ? Number(row[5]) : null,
373
+ height: Number.isFinite(Number(row[6])) ? Number(row[6]) : null,
374
+ };
375
+ });
376
+
377
+ if (rows.length === 0) {
378
+ throw new EmptyResultError('google images', `No Google image results matched "${query}".`);
379
+ }
380
+ return rows;
381
+ }
382
+
383
+ async function evaluateGoogleImagesPageState(page) {
384
+ const raw = await page.evaluate(inspectGoogleImagesPage);
385
+ const state = unwrapBrowserResult(raw);
386
+ if (!state || typeof state !== 'object' || Array.isArray(state)) {
387
+ throw new CommandExecutionError('google images returned an unexpected page-state payload shape.');
388
+ }
389
+ return state;
390
+ }
391
+
392
+ async function evaluateGoogleImageRows(page, limit, resolveOriginal) {
393
+ let rows = [];
394
+ for (let attempt = 0; attempt < 6; attempt += 1) {
395
+ const raw = await page.evaluate(extractGoogleImageRows, limit, resolveOriginal);
396
+ rows = requireRows(raw, 'google images');
397
+ if (rows.length > 0) {
398
+ return rows;
399
+ }
400
+ if (attempt < 5) {
401
+ if (typeof page.scroll === 'function') {
402
+ await page.scroll('down', 900).catch(() => {});
403
+ }
404
+ await page.wait(1).catch(() => {});
405
+ }
406
+ }
407
+ return rows;
408
+ }
409
+
410
+ const command = cli({
411
+ site: 'google',
412
+ name: 'images',
413
+ access: 'read',
414
+ description: 'Search Google Images for photos and image results',
415
+ domain: 'google.com',
416
+ strategy: Strategy.PUBLIC,
417
+ browser: true,
418
+ navigateBefore: false,
419
+ args: [
420
+ { name: 'keyword', positional: true, required: true, help: 'Image search query' },
421
+ { name: 'limit', type: 'int', default: 20, help: 'Number of image results (1-100)' },
422
+ { name: 'lang', default: 'en', help: 'Language short code (e.g. en, zh)' },
423
+ { name: 'resolve', type: 'bool', default: true, help: 'Click image previews to resolve original imgurl values' },
424
+ ],
425
+ columns: ['rank', 'title', 'imageUrl', 'thumbnailUrl', 'sourceUrl', 'source', 'width', 'height'],
426
+ func: async (page, args) => {
427
+ const query = requireSearchQuery(args.keyword);
428
+ const limit = requireBoundedInteger(args.limit, 20, 1, 100, '--limit');
429
+ const resolveOriginal = args.resolve !== false;
430
+ const lang = encodeURIComponent(String(args.lang || 'en'));
431
+ const pageSize = Math.max(limit, 20);
432
+ const url = `https://www.google.com/search?tbm=isch&q=${encodeURIComponent(query)}&hl=${lang}&num=${pageSize}`;
433
+
434
+ await runBrowserStep('google images navigation', () => navigateGoogleImages(page, url));
435
+ try {
436
+ await page.wait({ selector: '#rso img, #islrg img, #center_col img, #rcnt img', timeout: 8 });
437
+ }
438
+ catch {
439
+ await page.wait(2).catch(() => {});
440
+ }
441
+
442
+ const rows = await runBrowserStep('google images extraction', () => evaluateGoogleImageRows(page, limit, resolveOriginal));
443
+ if (rows.length === 0) {
444
+ const state = await runBrowserStep('google images page-state inspection', () => evaluateGoogleImagesPageState(page));
445
+ if (state.captchaOrConsent) {
446
+ throw new CommandExecutionError('google images is blocked by a Google CAPTCHA/consent/interstitial page.');
447
+ }
448
+ if (!state.explicitNoResults) {
449
+ throw new CommandExecutionError('google images returned no extractable result rows; the page layout may have changed.');
450
+ }
451
+ }
452
+ return normalizeImageRows(rows, query, limit);
453
+ },
454
+ });
455
+
456
+ export const __test__ = { command, evaluateGoogleImageRows, extractGoogleImageRows, inspectGoogleImagesPage, normalizeImageRows, navigateGoogleImages };