@agentforge/tools 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Tom Van Schoor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @agentforge/tools
2
2
 
3
- > Production-ready tools collection for AgentForge - 68 tools for web, data, file, and utility operations
3
+ > Production-ready tools collection for AgentForge - 69 tools for web, data, file, and utility operations
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@agentforge/tools)](https://www.npmjs.com/package/@agentforge/tools)
6
6
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue)](https://www.typescriptlang.org/)
@@ -8,7 +8,7 @@
8
8
 
9
9
  ## 🎉 Status: Production Ready & Published
10
10
 
11
- **68 production-ready tools** | **Full TypeScript support** | **Comprehensive documentation** | **LangChain compatible**
11
+ **69 production-ready tools** | **Full TypeScript support** | **Comprehensive documentation** | **LangChain compatible**
12
12
 
13
13
  ## 📦 Installation
14
14
 
@@ -22,9 +22,9 @@ yarn add @agentforge/tools
22
22
 
23
23
  ## 🎯 Overview
24
24
 
25
- This package provides **68 ready-to-use tools** organized into 4 categories:
25
+ This package provides **69 ready-to-use tools** organized into 4 categories:
26
26
 
27
- - **🌐 Web Tools** (10 tools) - HTTP requests, web scraping, HTML parsing, URL manipulation
27
+ - **🌐 Web Tools** (11 tools) - HTTP requests, web search, web scraping, HTML parsing, URL manipulation
28
28
  - **📊 Data Tools** (18 tools) - JSON, CSV, XML processing and data transformation
29
29
  - **📁 File Tools** (18 tools) - File operations, directory management, path utilities
30
30
  - **🔧 Utility Tools** (22 tools) - Date/time, strings, math, validation
@@ -68,10 +68,17 @@ const result = await calculator.execute({
68
68
 
69
69
  ## 📚 Tool Categories
70
70
 
71
- ### 🌐 Web Tools (10 tools)
71
+ ### 🌐 Web Tools (11 tools)
72
72
 
73
73
  Tools for web interactions and HTTP operations.
74
74
 
75
+ #### Web Search
76
+ - **`webSearch`** - Search the web using DuckDuckGo (free) or Serper API (optional premium)
77
+ - No API key required for basic searches (uses DuckDuckGo)
78
+ - Optional Serper API for premium Google search results
79
+ - Smart fallback: automatically switches providers when needed
80
+ - Returns structured results with titles, links, and snippets
81
+
75
82
  #### HTTP Client Tools
76
83
  - **`httpClient`** - Full-featured HTTP client with all methods (GET, POST, PUT, DELETE, PATCH)
77
84
  - **`httpGet`** - Simple GET requests
@@ -182,6 +189,85 @@ General utility tools for common operations.
182
189
 
183
190
  ## 💡 Usage Examples
184
191
 
192
+ ### Web Search Example
193
+
194
+ ```typescript
195
+ import { webSearch } from '@agentforge/tools';
196
+
197
+ // Basic search (no API key needed - uses DuckDuckGo)
198
+ const result = await webSearch.execute({
199
+ query: 'TypeScript programming language',
200
+ maxResults: 10
201
+ });
202
+
203
+ console.log(`Found ${result.results.length} results from ${result.source}`);
204
+ result.results.forEach(r => {
205
+ console.log(`${r.title}: ${r.link}`);
206
+ console.log(` ${r.snippet}`);
207
+ });
208
+
209
+ // Premium search with Serper API (requires SERPER_API_KEY env var)
210
+ // Get your API key at: https://serper.dev
211
+ const premiumResult = await webSearch.execute({
212
+ query: 'Latest AI developments 2026',
213
+ maxResults: 5,
214
+ preferSerper: true // Use Serper for Google search results
215
+ });
216
+
217
+ // Check metadata
218
+ console.log(`Source: ${premiumResult.source}`);
219
+ console.log(`Fallback used: ${premiumResult.metadata?.fallbackUsed}`);
220
+ console.log(`Response time: ${premiumResult.metadata?.responseTime}ms`);
221
+ ```
222
+
223
+ **Environment Setup:**
224
+ ```bash
225
+ # Optional: Add to your .env file for premium Google search
226
+ SERPER_API_KEY=your-serper-api-key-here
227
+ ```
228
+
229
+ **Input Schema:**
230
+ ```typescript
231
+ {
232
+ query: string; // Search query (required)
233
+ maxResults?: number; // Max results to return (default: 10)
234
+ preferSerper?: boolean; // Prefer Serper over DuckDuckGo (default: false)
235
+ }
236
+ ```
237
+
238
+ **Output Schema:**
239
+ ```typescript
240
+ {
241
+ results: Array<{
242
+ title: string; // Result title
243
+ link: string; // Result URL
244
+ snippet: string; // Result description/snippet
245
+ position: number; // Result position (1-based)
246
+ }>;
247
+ source: 'duckduckgo' | 'serper'; // Which provider was used
248
+ metadata?: {
249
+ fallbackUsed: boolean; // Whether fallback to DuckDuckGo occurred
250
+ responseTime: number; // Response time in milliseconds
251
+ };
252
+ }
253
+ ```
254
+
255
+ **DuckDuckGo vs Serper:**
256
+
257
+ | Feature | DuckDuckGo (Free) | Serper (Premium) |
258
+ |---------|-------------------|------------------|
259
+ | **API Key** | ❌ Not required | ✅ Required ([get key](https://serper.dev)) |
260
+ | **Cost** | 🆓 Free | 💰 Paid (see [pricing](https://serper.dev/pricing)) |
261
+ | **Search Engine** | DuckDuckGo | Google |
262
+ | **Rate Limits** | Generous | Based on plan |
263
+ | **Result Quality** | Good | Excellent (Google results) |
264
+ | **Use Case** | Development, testing, low-volume | Production, high-quality results |
265
+ | **Fallback** | N/A | Auto-fallback to DuckDuckGo on error |
266
+
267
+ **When to use each:**
268
+ - **DuckDuckGo**: Default choice, no setup needed, great for development and testing
269
+ - **Serper**: Production use cases requiring Google-quality results, set `preferSerper: true`
270
+
185
271
  ### Web Scraping Example
186
272
 
187
273
  ```typescript
package/dist/index.cjs CHANGED
@@ -343,6 +343,352 @@ var urlQueryParser = core.toolBuilder().name("url-query-parser").description("Pa
343
343
  count: Object.keys(params).length
344
344
  };
345
345
  }).build();
346
+ var webSearchSchema = zod.z.object({
347
+ query: zod.z.string().min(1).describe("The search query"),
348
+ maxResults: zod.z.number().min(1).max(50).default(10).describe("Maximum number of results to return (1-50)"),
349
+ preferSerper: zod.z.boolean().default(false).describe("Prefer Serper API if available (requires SERPER_API_KEY)"),
350
+ timeout: zod.z.number().min(1e3).max(6e4).default(3e4).describe("Request timeout in milliseconds (1000-60000, default: 30000)")
351
+ });
352
+ var searchResultSchema = zod.z.object({
353
+ title: zod.z.string(),
354
+ link: zod.z.string().url(),
355
+ snippet: zod.z.string(),
356
+ position: zod.z.number().optional()
357
+ });
358
+ var webSearchOutputSchema = zod.z.object({
359
+ success: zod.z.boolean(),
360
+ source: zod.z.enum(["duckduckgo", "serper"]),
361
+ query: zod.z.string(),
362
+ results: zod.z.array(searchResultSchema),
363
+ totalResults: zod.z.number().optional(),
364
+ error: zod.z.string().optional(),
365
+ metadata: zod.z.object({
366
+ responseTime: zod.z.number().optional(),
367
+ fallbackUsed: zod.z.boolean().optional()
368
+ }).optional()
369
+ });
370
+
371
+ // src/web/web-search/utils.ts
372
+ function getSerperApiKey() {
373
+ return process.env.SERPER_API_KEY;
374
+ }
375
+ async function measureTime(fn) {
376
+ const start = Date.now();
377
+ const result = await fn();
378
+ const duration = Date.now() - start;
379
+ return { result, duration };
380
+ }
381
+ function sanitizeQuery(query) {
382
+ return query.trim().replace(/\s+/g, " ");
383
+ }
384
+ var DEFAULT_RETRY_CONFIG = {
385
+ maxRetries: 3,
386
+ initialDelay: 1e3,
387
+ // 1 second
388
+ maxDelay: 1e4,
389
+ // 10 seconds
390
+ backoffMultiplier: 2
391
+ };
392
+ var DEFAULT_TIMEOUT = 3e4;
393
+ function isRetryableError(error) {
394
+ if (error.code === "ECONNRESET" || error.code === "ETIMEDOUT" || error.code === "ENOTFOUND") {
395
+ return true;
396
+ }
397
+ if (error.code === "ECONNABORTED" || error.message?.includes("timeout")) {
398
+ return true;
399
+ }
400
+ if (error.response?.status >= 500 && error.response?.status < 600) {
401
+ return true;
402
+ }
403
+ if (error.response?.status === 429) {
404
+ return true;
405
+ }
406
+ return false;
407
+ }
408
+ function sleep(ms) {
409
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
410
+ }
411
+ async function retryWithBackoff(fn, config = DEFAULT_RETRY_CONFIG) {
412
+ let lastError;
413
+ let delay = config.initialDelay;
414
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
415
+ try {
416
+ return await fn();
417
+ } catch (error) {
418
+ lastError = error;
419
+ if (!isRetryableError(error)) {
420
+ throw error;
421
+ }
422
+ if (attempt === config.maxRetries) {
423
+ break;
424
+ }
425
+ await sleep(delay);
426
+ delay = Math.min(delay * config.backoffMultiplier, config.maxDelay);
427
+ }
428
+ }
429
+ throw lastError;
430
+ }
431
+
432
+ // src/web/web-search/providers/duckduckgo.ts
433
+ var DuckDuckGoProvider = class {
434
+ name = "duckduckgo";
435
+ /**
436
+ * DuckDuckGo is always available (no API key required)
437
+ */
438
+ isAvailable() {
439
+ return true;
440
+ }
441
+ /**
442
+ * Search using DuckDuckGo Instant Answer API
443
+ * @param query - Search query
444
+ * @param maxResults - Maximum number of results to return
445
+ * @param timeout - Request timeout in milliseconds (default: 30000)
446
+ */
447
+ async search(query, maxResults, timeout = DEFAULT_TIMEOUT) {
448
+ return retryWithBackoff(async () => {
449
+ try {
450
+ const response = await axios__default.default.get(
451
+ "https://api.duckduckgo.com/",
452
+ {
453
+ params: {
454
+ q: query,
455
+ format: "json"
456
+ },
457
+ headers: {
458
+ "User-Agent": "Mozilla/5.0 (compatible; AgentForge/1.0; +https://github.com/agentforge)"
459
+ },
460
+ timeout
461
+ }
462
+ );
463
+ return this.normalizeResults(response.data, maxResults);
464
+ } catch (error) {
465
+ throw new Error(`DuckDuckGo search failed: ${error.message}`);
466
+ }
467
+ });
468
+ }
469
+ /**
470
+ * Normalize DuckDuckGo response to SearchResult[]
471
+ * Optimized for performance with large result sets
472
+ */
473
+ normalizeResults(data, maxResults) {
474
+ const results = [];
475
+ if (maxResults <= 0) {
476
+ return results;
477
+ }
478
+ if (data.Abstract && data.AbstractURL) {
479
+ results.push({
480
+ title: data.Heading || "Result",
481
+ link: data.AbstractURL,
482
+ snippet: data.Abstract,
483
+ position: 1
484
+ });
485
+ if (results.length >= maxResults) {
486
+ return results;
487
+ }
488
+ }
489
+ if (data.RelatedTopics && data.RelatedTopics.length > 0) {
490
+ const remaining = maxResults - results.length;
491
+ const topicsToProcess = data.RelatedTopics.slice(0, remaining);
492
+ for (const topic of topicsToProcess) {
493
+ if (topic.Text && topic.FirstURL) {
494
+ const titleParts = topic.Text.split(" - ");
495
+ results.push({
496
+ title: titleParts[0] || topic.Text,
497
+ link: topic.FirstURL,
498
+ snippet: topic.Text,
499
+ position: results.length + 1
500
+ });
501
+ }
502
+ }
503
+ if (results.length >= maxResults) {
504
+ return results;
505
+ }
506
+ }
507
+ if (data.Results && data.Results.length > 0) {
508
+ const remaining = maxResults - results.length;
509
+ const resultsToProcess = data.Results.slice(0, remaining);
510
+ for (const result of resultsToProcess) {
511
+ if (result.Text && result.FirstURL) {
512
+ const titleParts = result.Text.split(" - ");
513
+ results.push({
514
+ title: titleParts[0] || result.Text,
515
+ link: result.FirstURL,
516
+ snippet: result.Text,
517
+ position: results.length + 1
518
+ });
519
+ }
520
+ }
521
+ }
522
+ return results;
523
+ }
524
+ };
525
+ function createDuckDuckGoProvider() {
526
+ return new DuckDuckGoProvider();
527
+ }
528
+ var SerperProvider = class {
529
+ name = "serper";
530
+ apiKey;
531
+ constructor() {
532
+ this.apiKey = getSerperApiKey();
533
+ }
534
+ /**
535
+ * Serper is available if API key is set
536
+ */
537
+ isAvailable() {
538
+ return !!this.apiKey;
539
+ }
540
+ /**
541
+ * Search using Serper API
542
+ * @param query - Search query
543
+ * @param maxResults - Maximum number of results to return
544
+ * @param timeout - Request timeout in milliseconds (default: 30000)
545
+ */
546
+ async search(query, maxResults, timeout = DEFAULT_TIMEOUT) {
547
+ if (!this.apiKey) {
548
+ throw new Error(
549
+ "Serper API key not found. Set SERPER_API_KEY environment variable. Get your key at https://serper.dev"
550
+ );
551
+ }
552
+ return retryWithBackoff(async () => {
553
+ try {
554
+ const response = await axios__default.default.post(
555
+ "https://google.serper.dev/search",
556
+ {
557
+ q: query,
558
+ num: maxResults
559
+ },
560
+ {
561
+ headers: {
562
+ "X-API-KEY": this.apiKey,
563
+ "Content-Type": "application/json"
564
+ },
565
+ timeout
566
+ }
567
+ );
568
+ return this.normalizeResults(response.data, maxResults);
569
+ } catch (error) {
570
+ if (error.response?.status === 401) {
571
+ throw new Error(
572
+ "Invalid Serper API key. Get your key at https://serper.dev"
573
+ );
574
+ }
575
+ if (error.response?.status === 429) {
576
+ throw new Error(
577
+ "Serper API rate limit exceeded. Please try again later or upgrade your plan at https://serper.dev"
578
+ );
579
+ }
580
+ throw new Error(`Serper search failed: ${error.message}`);
581
+ }
582
+ });
583
+ }
584
+ /**
585
+ * Normalize Serper response to SearchResult[]
586
+ * Optimized for performance with large result sets
587
+ */
588
+ normalizeResults(data, maxResults) {
589
+ if (!data.organic || data.organic.length === 0 || maxResults <= 0) {
590
+ return [];
591
+ }
592
+ const results = [];
593
+ const itemsToProcess = Math.min(data.organic.length, maxResults);
594
+ for (let i = 0; i < itemsToProcess; i++) {
595
+ const item = data.organic[i];
596
+ if (!item.title || !item.link || !item.snippet) {
597
+ continue;
598
+ }
599
+ results.push({
600
+ title: item.title,
601
+ link: item.link,
602
+ snippet: item.snippet,
603
+ position: item.position ?? i + 1
604
+ });
605
+ if (results.length >= maxResults) {
606
+ break;
607
+ }
608
+ }
609
+ return results;
610
+ }
611
+ };
612
+ function createSerperProvider() {
613
+ return new SerperProvider();
614
+ }
615
+
616
+ // src/web/web-search/index.ts
617
+ var webSearch = core.toolBuilder().name("web-search").description(
618
+ "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."
619
+ ).category(core.ToolCategory.WEB).tags(["search", "web", "google", "duckduckgo", "serper", "internet"]).schema(webSearchSchema).implement(async (input) => {
620
+ const {
621
+ query,
622
+ maxResults = 10,
623
+ preferSerper = false,
624
+ timeout = DEFAULT_TIMEOUT
625
+ } = input;
626
+ const sanitizedQuery = sanitizeQuery(query);
627
+ const duckduckgo = createDuckDuckGoProvider();
628
+ const serper = createSerperProvider();
629
+ let primaryProvider;
630
+ let fallbackProvider = null;
631
+ if (preferSerper && serper.isAvailable()) {
632
+ primaryProvider = serper;
633
+ fallbackProvider = duckduckgo;
634
+ } else {
635
+ primaryProvider = duckduckgo;
636
+ fallbackProvider = serper.isAvailable() ? serper : null;
637
+ }
638
+ try {
639
+ const { result: results, duration } = await measureTime(
640
+ () => primaryProvider.search(sanitizedQuery, maxResults, timeout)
641
+ );
642
+ if (results.length > 0) {
643
+ return {
644
+ success: true,
645
+ source: primaryProvider.name,
646
+ query: sanitizedQuery,
647
+ results,
648
+ totalResults: results.length,
649
+ metadata: {
650
+ responseTime: duration,
651
+ fallbackUsed: false
652
+ }
653
+ };
654
+ }
655
+ if (fallbackProvider) {
656
+ const { result: fallbackResults, duration: fallbackDuration } = await measureTime(
657
+ () => fallbackProvider.search(sanitizedQuery, maxResults, timeout)
658
+ );
659
+ return {
660
+ success: true,
661
+ source: fallbackProvider.name,
662
+ query: sanitizedQuery,
663
+ results: fallbackResults,
664
+ totalResults: fallbackResults.length,
665
+ metadata: {
666
+ responseTime: fallbackDuration,
667
+ fallbackUsed: true
668
+ }
669
+ };
670
+ }
671
+ return {
672
+ success: true,
673
+ source: primaryProvider.name,
674
+ query: sanitizedQuery,
675
+ results: [],
676
+ totalResults: 0,
677
+ metadata: {
678
+ responseTime: duration,
679
+ fallbackUsed: false
680
+ }
681
+ };
682
+ } catch (error) {
683
+ return {
684
+ success: false,
685
+ source: primaryProvider.name,
686
+ query: sanitizedQuery,
687
+ results: [],
688
+ error: error.message
689
+ };
690
+ }
691
+ }).build();
346
692
  var jsonParser = core.toolBuilder().name("json-parser").description("Parse JSON string into an object. Validates JSON syntax and returns parsed data or error details.").category(core.ToolCategory.UTILITY).tags(["json", "parse", "data"]).schema(zod.z.object({
347
693
  json: zod.z.string().describe("JSON string to parse"),
348
694
  strict: zod.z.boolean().default(true).describe("Use strict JSON parsing (no trailing commas, etc.)")
@@ -1648,11 +1994,15 @@ var uuidValidator = core.toolBuilder().name("uuid-validator").description("Valid
1648
1994
  };
1649
1995
  }).build();
1650
1996
 
1997
+ exports.DuckDuckGoProvider = DuckDuckGoProvider;
1998
+ exports.SerperProvider = SerperProvider;
1651
1999
  exports.arrayFilter = arrayFilter;
1652
2000
  exports.arrayGroupBy = arrayGroupBy;
1653
2001
  exports.arrayMap = arrayMap;
1654
2002
  exports.arraySort = arraySort;
1655
2003
  exports.calculator = calculator;
2004
+ exports.createDuckDuckGoProvider = createDuckDuckGoProvider;
2005
+ exports.createSerperProvider = createSerperProvider;
1656
2006
  exports.creditCardValidator = creditCardValidator;
1657
2007
  exports.csvGenerator = csvGenerator;
1658
2008
  exports.csvParser = csvParser;
@@ -1699,6 +2049,7 @@ exports.pathRelative = pathRelative;
1699
2049
  exports.pathResolve = pathResolve;
1700
2050
  exports.phoneValidator = phoneValidator;
1701
2051
  exports.randomNumber = randomNumber;
2052
+ exports.searchResultSchema = searchResultSchema;
1702
2053
  exports.statistics = statistics;
1703
2054
  exports.stringCaseConverter = stringCaseConverter;
1704
2055
  exports.stringJoin = stringJoin;
@@ -1713,6 +2064,9 @@ exports.urlValidator = urlValidator;
1713
2064
  exports.urlValidatorSimple = urlValidatorSimple;
1714
2065
  exports.uuidValidator = uuidValidator;
1715
2066
  exports.webScraper = webScraper;
2067
+ exports.webSearch = webSearch;
2068
+ exports.webSearchOutputSchema = webSearchOutputSchema;
2069
+ exports.webSearchSchema = webSearchSchema;
1716
2070
  exports.xmlGenerator = xmlGenerator;
1717
2071
  exports.xmlParser = xmlParser;
1718
2072
  exports.xmlToJson = xmlToJson;