@agentforge/tools 0.3.9 → 0.4.1

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/dist/index.js CHANGED
@@ -317,6 +317,352 @@ var urlQueryParser = toolBuilder().name("url-query-parser").description("Parse q
317
317
  count: Object.keys(params).length
318
318
  };
319
319
  }).build();
320
+ var webSearchSchema = z.object({
321
+ query: z.string().min(1).describe("The search query"),
322
+ maxResults: z.number().min(1).max(50).default(10).describe("Maximum number of results to return (1-50)"),
323
+ preferSerper: z.boolean().default(false).describe("Prefer Serper API if available (requires SERPER_API_KEY)"),
324
+ timeout: z.number().min(1e3).max(6e4).default(3e4).describe("Request timeout in milliseconds (1000-60000, default: 30000)")
325
+ });
326
+ var searchResultSchema = z.object({
327
+ title: z.string(),
328
+ link: z.string().url(),
329
+ snippet: z.string(),
330
+ position: z.number().optional()
331
+ });
332
+ var webSearchOutputSchema = z.object({
333
+ success: z.boolean(),
334
+ source: z.enum(["duckduckgo", "serper"]),
335
+ query: z.string(),
336
+ results: z.array(searchResultSchema),
337
+ totalResults: z.number().optional(),
338
+ error: z.string().optional(),
339
+ metadata: z.object({
340
+ responseTime: z.number().optional(),
341
+ fallbackUsed: z.boolean().optional()
342
+ }).optional()
343
+ });
344
+
345
+ // src/web/web-search/utils.ts
346
+ function getSerperApiKey() {
347
+ return process.env.SERPER_API_KEY;
348
+ }
349
+ async function measureTime(fn) {
350
+ const start = Date.now();
351
+ const result = await fn();
352
+ const duration = Date.now() - start;
353
+ return { result, duration };
354
+ }
355
+ function sanitizeQuery(query) {
356
+ return query.trim().replace(/\s+/g, " ");
357
+ }
358
+ var DEFAULT_RETRY_CONFIG = {
359
+ maxRetries: 3,
360
+ initialDelay: 1e3,
361
+ // 1 second
362
+ maxDelay: 1e4,
363
+ // 10 seconds
364
+ backoffMultiplier: 2
365
+ };
366
+ var DEFAULT_TIMEOUT = 3e4;
367
+ function isRetryableError(error) {
368
+ if (error.code === "ECONNRESET" || error.code === "ETIMEDOUT" || error.code === "ENOTFOUND") {
369
+ return true;
370
+ }
371
+ if (error.code === "ECONNABORTED" || error.message?.includes("timeout")) {
372
+ return true;
373
+ }
374
+ if (error.response?.status >= 500 && error.response?.status < 600) {
375
+ return true;
376
+ }
377
+ if (error.response?.status === 429) {
378
+ return true;
379
+ }
380
+ return false;
381
+ }
382
+ function sleep(ms) {
383
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
384
+ }
385
+ async function retryWithBackoff(fn, config = DEFAULT_RETRY_CONFIG) {
386
+ let lastError;
387
+ let delay = config.initialDelay;
388
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
389
+ try {
390
+ return await fn();
391
+ } catch (error) {
392
+ lastError = error;
393
+ if (!isRetryableError(error)) {
394
+ throw error;
395
+ }
396
+ if (attempt === config.maxRetries) {
397
+ break;
398
+ }
399
+ await sleep(delay);
400
+ delay = Math.min(delay * config.backoffMultiplier, config.maxDelay);
401
+ }
402
+ }
403
+ throw lastError;
404
+ }
405
+
406
+ // src/web/web-search/providers/duckduckgo.ts
407
+ var DuckDuckGoProvider = class {
408
+ name = "duckduckgo";
409
+ /**
410
+ * DuckDuckGo is always available (no API key required)
411
+ */
412
+ isAvailable() {
413
+ return true;
414
+ }
415
+ /**
416
+ * Search using DuckDuckGo Instant Answer API
417
+ * @param query - Search query
418
+ * @param maxResults - Maximum number of results to return
419
+ * @param timeout - Request timeout in milliseconds (default: 30000)
420
+ */
421
+ async search(query, maxResults, timeout = DEFAULT_TIMEOUT) {
422
+ return retryWithBackoff(async () => {
423
+ try {
424
+ const response = await axios.get(
425
+ "https://api.duckduckgo.com/",
426
+ {
427
+ params: {
428
+ q: query,
429
+ format: "json"
430
+ },
431
+ headers: {
432
+ "User-Agent": "Mozilla/5.0 (compatible; AgentForge/1.0; +https://github.com/agentforge)"
433
+ },
434
+ timeout
435
+ }
436
+ );
437
+ return this.normalizeResults(response.data, maxResults);
438
+ } catch (error) {
439
+ throw new Error(`DuckDuckGo search failed: ${error.message}`);
440
+ }
441
+ });
442
+ }
443
+ /**
444
+ * Normalize DuckDuckGo response to SearchResult[]
445
+ * Optimized for performance with large result sets
446
+ */
447
+ normalizeResults(data, maxResults) {
448
+ const results = [];
449
+ if (maxResults <= 0) {
450
+ return results;
451
+ }
452
+ if (data.Abstract && data.AbstractURL) {
453
+ results.push({
454
+ title: data.Heading || "Result",
455
+ link: data.AbstractURL,
456
+ snippet: data.Abstract,
457
+ position: 1
458
+ });
459
+ if (results.length >= maxResults) {
460
+ return results;
461
+ }
462
+ }
463
+ if (data.RelatedTopics && data.RelatedTopics.length > 0) {
464
+ const remaining = maxResults - results.length;
465
+ const topicsToProcess = data.RelatedTopics.slice(0, remaining);
466
+ for (const topic of topicsToProcess) {
467
+ if (topic.Text && topic.FirstURL) {
468
+ const titleParts = topic.Text.split(" - ");
469
+ results.push({
470
+ title: titleParts[0] || topic.Text,
471
+ link: topic.FirstURL,
472
+ snippet: topic.Text,
473
+ position: results.length + 1
474
+ });
475
+ }
476
+ }
477
+ if (results.length >= maxResults) {
478
+ return results;
479
+ }
480
+ }
481
+ if (data.Results && data.Results.length > 0) {
482
+ const remaining = maxResults - results.length;
483
+ const resultsToProcess = data.Results.slice(0, remaining);
484
+ for (const result of resultsToProcess) {
485
+ if (result.Text && result.FirstURL) {
486
+ const titleParts = result.Text.split(" - ");
487
+ results.push({
488
+ title: titleParts[0] || result.Text,
489
+ link: result.FirstURL,
490
+ snippet: result.Text,
491
+ position: results.length + 1
492
+ });
493
+ }
494
+ }
495
+ }
496
+ return results;
497
+ }
498
+ };
499
+ function createDuckDuckGoProvider() {
500
+ return new DuckDuckGoProvider();
501
+ }
502
+ var SerperProvider = class {
503
+ name = "serper";
504
+ apiKey;
505
+ constructor() {
506
+ this.apiKey = getSerperApiKey();
507
+ }
508
+ /**
509
+ * Serper is available if API key is set
510
+ */
511
+ isAvailable() {
512
+ return !!this.apiKey;
513
+ }
514
+ /**
515
+ * Search using Serper API
516
+ * @param query - Search query
517
+ * @param maxResults - Maximum number of results to return
518
+ * @param timeout - Request timeout in milliseconds (default: 30000)
519
+ */
520
+ async search(query, maxResults, timeout = DEFAULT_TIMEOUT) {
521
+ if (!this.apiKey) {
522
+ throw new Error(
523
+ "Serper API key not found. Set SERPER_API_KEY environment variable. Get your key at https://serper.dev"
524
+ );
525
+ }
526
+ return retryWithBackoff(async () => {
527
+ try {
528
+ const response = await axios.post(
529
+ "https://google.serper.dev/search",
530
+ {
531
+ q: query,
532
+ num: maxResults
533
+ },
534
+ {
535
+ headers: {
536
+ "X-API-KEY": this.apiKey,
537
+ "Content-Type": "application/json"
538
+ },
539
+ timeout
540
+ }
541
+ );
542
+ return this.normalizeResults(response.data, maxResults);
543
+ } catch (error) {
544
+ if (error.response?.status === 401) {
545
+ throw new Error(
546
+ "Invalid Serper API key. Get your key at https://serper.dev"
547
+ );
548
+ }
549
+ if (error.response?.status === 429) {
550
+ throw new Error(
551
+ "Serper API rate limit exceeded. Please try again later or upgrade your plan at https://serper.dev"
552
+ );
553
+ }
554
+ throw new Error(`Serper search failed: ${error.message}`);
555
+ }
556
+ });
557
+ }
558
+ /**
559
+ * Normalize Serper response to SearchResult[]
560
+ * Optimized for performance with large result sets
561
+ */
562
+ normalizeResults(data, maxResults) {
563
+ if (!data.organic || data.organic.length === 0 || maxResults <= 0) {
564
+ return [];
565
+ }
566
+ const results = [];
567
+ const itemsToProcess = Math.min(data.organic.length, maxResults);
568
+ for (let i = 0; i < itemsToProcess; i++) {
569
+ const item = data.organic[i];
570
+ if (!item.title || !item.link || !item.snippet) {
571
+ continue;
572
+ }
573
+ results.push({
574
+ title: item.title,
575
+ link: item.link,
576
+ snippet: item.snippet,
577
+ position: item.position ?? i + 1
578
+ });
579
+ if (results.length >= maxResults) {
580
+ break;
581
+ }
582
+ }
583
+ return results;
584
+ }
585
+ };
586
+ function createSerperProvider() {
587
+ return new SerperProvider();
588
+ }
589
+
590
+ // src/web/web-search/index.ts
591
+ var webSearch = toolBuilder().name("web-search").description(
592
+ "Search the web for information using DuckDuckGo (free) or Serper API (optional). Returns structured search results with titles, links, and snippets. Automatically falls back to Serper if DuckDuckGo returns no results and API key is available."
593
+ ).category(ToolCategory.WEB).tags(["search", "web", "google", "duckduckgo", "serper", "internet"]).schema(webSearchSchema).implement(async (input) => {
594
+ const {
595
+ query,
596
+ maxResults = 10,
597
+ preferSerper = false,
598
+ timeout = DEFAULT_TIMEOUT
599
+ } = input;
600
+ const sanitizedQuery = sanitizeQuery(query);
601
+ const duckduckgo = createDuckDuckGoProvider();
602
+ const serper = createSerperProvider();
603
+ let primaryProvider;
604
+ let fallbackProvider = null;
605
+ if (preferSerper && serper.isAvailable()) {
606
+ primaryProvider = serper;
607
+ fallbackProvider = duckduckgo;
608
+ } else {
609
+ primaryProvider = duckduckgo;
610
+ fallbackProvider = serper.isAvailable() ? serper : null;
611
+ }
612
+ try {
613
+ const { result: results, duration } = await measureTime(
614
+ () => primaryProvider.search(sanitizedQuery, maxResults, timeout)
615
+ );
616
+ if (results.length > 0) {
617
+ return {
618
+ success: true,
619
+ source: primaryProvider.name,
620
+ query: sanitizedQuery,
621
+ results,
622
+ totalResults: results.length,
623
+ metadata: {
624
+ responseTime: duration,
625
+ fallbackUsed: false
626
+ }
627
+ };
628
+ }
629
+ if (fallbackProvider) {
630
+ const { result: fallbackResults, duration: fallbackDuration } = await measureTime(
631
+ () => fallbackProvider.search(sanitizedQuery, maxResults, timeout)
632
+ );
633
+ return {
634
+ success: true,
635
+ source: fallbackProvider.name,
636
+ query: sanitizedQuery,
637
+ results: fallbackResults,
638
+ totalResults: fallbackResults.length,
639
+ metadata: {
640
+ responseTime: fallbackDuration,
641
+ fallbackUsed: true
642
+ }
643
+ };
644
+ }
645
+ return {
646
+ success: true,
647
+ source: primaryProvider.name,
648
+ query: sanitizedQuery,
649
+ results: [],
650
+ totalResults: 0,
651
+ metadata: {
652
+ responseTime: duration,
653
+ fallbackUsed: false
654
+ }
655
+ };
656
+ } catch (error) {
657
+ return {
658
+ success: false,
659
+ source: primaryProvider.name,
660
+ query: sanitizedQuery,
661
+ results: [],
662
+ error: error.message
663
+ };
664
+ }
665
+ }).build();
320
666
  var jsonParser = toolBuilder().name("json-parser").description("Parse JSON string into an object. Validates JSON syntax and returns parsed data or error details.").category(ToolCategory.UTILITY).tags(["json", "parse", "data"]).schema(z.object({
321
667
  json: z.string().describe("JSON string to parse"),
322
668
  strict: z.boolean().default(true).describe("Use strict JSON parsing (no trailing commas, etc.)")
@@ -1622,6 +1968,6 @@ var uuidValidator = toolBuilder().name("uuid-validator").description("Validate i
1622
1968
  };
1623
1969
  }).build();
1624
1970
 
1625
- export { arrayFilter, arrayGroupBy, arrayMap, arraySort, calculator, creditCardValidator, csvGenerator, csvParser, csvToJson, currentDateTime, dateArithmetic, dateComparison, dateDifference, dateFormatter, directoryCreate, directoryDelete, directoryList, emailValidator, extractImages, extractLinks, fileAppend, fileDelete, fileExists, fileReader, fileSearch, fileWriter, htmlParser, httpClient, httpGet, httpPost, ipValidator, jsonMerge, jsonParser, jsonQuery, jsonStringify, jsonToCsv, jsonToXml, jsonValidator, mathFunctions, objectOmit, objectPick, pathBasename, pathDirname, pathExtension, pathJoin, pathNormalize, pathParse, pathRelative, pathResolve, phoneValidator, randomNumber, statistics, stringCaseConverter, stringJoin, stringLength, stringReplace, stringSplit, stringSubstring, stringTrim, urlBuilder, urlQueryParser, urlValidator, urlValidatorSimple, uuidValidator, webScraper, xmlGenerator, xmlParser, xmlToJson };
1971
+ export { DuckDuckGoProvider, SerperProvider, arrayFilter, arrayGroupBy, arrayMap, arraySort, calculator, createDuckDuckGoProvider, createSerperProvider, creditCardValidator, csvGenerator, csvParser, csvToJson, currentDateTime, dateArithmetic, dateComparison, dateDifference, dateFormatter, directoryCreate, directoryDelete, directoryList, emailValidator, extractImages, extractLinks, fileAppend, fileDelete, fileExists, fileReader, fileSearch, fileWriter, htmlParser, httpClient, httpGet, httpPost, ipValidator, jsonMerge, jsonParser, jsonQuery, jsonStringify, jsonToCsv, jsonToXml, jsonValidator, mathFunctions, objectOmit, objectPick, pathBasename, pathDirname, pathExtension, pathJoin, pathNormalize, pathParse, pathRelative, pathResolve, phoneValidator, randomNumber, searchResultSchema, statistics, stringCaseConverter, stringJoin, stringLength, stringReplace, stringSplit, stringSubstring, stringTrim, urlBuilder, urlQueryParser, urlValidator, urlValidatorSimple, uuidValidator, webScraper, webSearch, webSearchOutputSchema, webSearchSchema, xmlGenerator, xmlParser, xmlToJson };
1626
1972
  //# sourceMappingURL=index.js.map
1627
1973
  //# sourceMappingURL=index.js.map