@gotza02/sequential-thinking 2026.2.1 → 2026.2.2

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/tools/web.js CHANGED
@@ -9,74 +9,90 @@ export function registerWebTools(server) {
9
9
  query: z.string().min(1).describe("The search query"),
10
10
  provider: z.enum(['brave', 'exa', 'google']).optional().describe("Preferred search provider")
11
11
  }, async ({ query, provider }) => {
12
- try {
13
- // Priority: User Preference > Brave > Exa > Google
14
- let selectedProvider = provider;
15
- if (!selectedProvider) {
16
- if (process.env.BRAVE_API_KEY)
17
- selectedProvider = 'brave';
18
- else if (process.env.EXA_API_KEY)
19
- selectedProvider = 'exa';
20
- else if (process.env.GOOGLE_SEARCH_API_KEY)
21
- selectedProvider = 'google';
22
- else
23
- return { content: [{ type: "text", text: "Error: No search provider configured. Please set BRAVE_API_KEY, EXA_API_KEY, or GOOGLE_SEARCH_API_KEY." }], isError: true };
12
+ const errors = [];
13
+ // 1. Identify available providers
14
+ const availableProviders = [];
15
+ if (process.env.BRAVE_API_KEY)
16
+ availableProviders.push('brave');
17
+ if (process.env.EXA_API_KEY)
18
+ availableProviders.push('exa');
19
+ if (process.env.GOOGLE_SEARCH_API_KEY && process.env.GOOGLE_SEARCH_CX)
20
+ availableProviders.push('google');
21
+ if (availableProviders.length === 0) {
22
+ return {
23
+ content: [{ type: "text", text: "Error: No search provider configured. Please set BRAVE_API_KEY, EXA_API_KEY, or GOOGLE_SEARCH_API_KEY." }],
24
+ isError: true
25
+ };
26
+ }
27
+ // 2. Determine execution order
28
+ let attemptOrder = [...availableProviders];
29
+ if (provider) {
30
+ if (availableProviders.includes(provider)) {
31
+ // Move requested provider to the front
32
+ attemptOrder = [provider, ...availableProviders.filter(p => p !== provider)];
24
33
  }
25
- if (selectedProvider === 'brave') {
26
- if (!process.env.BRAVE_API_KEY)
27
- throw new Error("BRAVE_API_KEY not found");
28
- const response = await fetchWithRetry(`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=5`, {
29
- headers: {
30
- 'X-Subscription-Token': process.env.BRAVE_API_KEY,
31
- 'Accept': 'application/json'
32
- }
33
- });
34
- if (!response.ok)
35
- throw new Error(`Brave API error: ${response.statusText}`);
36
- const data = await response.json();
37
- return { content: [{ type: "text", text: JSON.stringify(data.web?.results || data, null, 2) }] };
34
+ else {
35
+ return {
36
+ content: [{ type: "text", text: `Error: Requested provider '${provider}' is not configured or unavailable.` }],
37
+ isError: true
38
+ };
38
39
  }
39
- if (selectedProvider === 'exa') {
40
- if (!process.env.EXA_API_KEY)
41
- throw new Error("EXA_API_KEY not found");
42
- const response = await fetchWithRetry('https://api.exa.ai/search', {
43
- method: 'POST',
44
- headers: {
45
- 'x-api-key': process.env.EXA_API_KEY,
46
- 'Content-Type': 'application/json'
47
- },
48
- body: JSON.stringify({ query, numResults: 5 })
49
- });
50
- if (!response.ok)
51
- throw new Error(`Exa API error: ${response.statusText}`);
52
- const data = await response.json();
53
- return { content: [{ type: "text", text: JSON.stringify(data.results || data, null, 2) }] };
40
+ }
41
+ // Default priority if no preference: Brave > Exa > Google (Already respected by push order if mapped correctly, but let's be explicit if needed. Here they are added in that order.)
42
+ // 3. Try providers sequentially
43
+ for (const currentProvider of attemptOrder) {
44
+ try {
45
+ if (currentProvider === 'brave') {
46
+ const response = await fetchWithRetry(`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=5`, {
47
+ headers: {
48
+ 'X-Subscription-Token': process.env.BRAVE_API_KEY,
49
+ 'Accept': 'application/json'
50
+ }
51
+ });
52
+ if (!response.ok)
53
+ throw new Error(`Brave API error: ${response.status} ${response.statusText}`);
54
+ const data = await response.json();
55
+ return { content: [{ type: "text", text: JSON.stringify(data.web?.results || data, null, 2) }] };
56
+ }
57
+ if (currentProvider === 'exa') {
58
+ const response = await fetchWithRetry('https://api.exa.ai/search', {
59
+ method: 'POST',
60
+ headers: {
61
+ 'x-api-key': process.env.EXA_API_KEY,
62
+ 'Content-Type': 'application/json'
63
+ },
64
+ body: JSON.stringify({ query, numResults: 5 })
65
+ });
66
+ if (!response.ok)
67
+ throw new Error(`Exa API error: ${response.status} ${response.statusText}`);
68
+ const data = await response.json();
69
+ return { content: [{ type: "text", text: JSON.stringify(data.results || data, null, 2) }] };
70
+ }
71
+ if (currentProvider === 'google') {
72
+ const response = await fetchWithRetry(`https://www.googleapis.com/customsearch/v1?key=${process.env.GOOGLE_SEARCH_API_KEY}&cx=${process.env.GOOGLE_SEARCH_CX}&q=${encodeURIComponent(query)}&num=5`);
73
+ if (!response.ok)
74
+ throw new Error(`Google API error: ${response.status} ${response.statusText}`);
75
+ const data = await response.json();
76
+ // Extract relevant fields to keep output clean
77
+ const results = data.items?.map((item) => ({
78
+ title: item.title,
79
+ link: item.link,
80
+ snippet: item.snippet
81
+ })) || [];
82
+ return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
83
+ }
54
84
  }
55
- if (selectedProvider === 'google') {
56
- if (!process.env.GOOGLE_SEARCH_API_KEY)
57
- throw new Error("GOOGLE_SEARCH_API_KEY not found");
58
- if (!process.env.GOOGLE_SEARCH_CX)
59
- throw new Error("GOOGLE_SEARCH_CX (Search Engine ID) not found");
60
- const response = await fetchWithRetry(`https://www.googleapis.com/customsearch/v1?key=${process.env.GOOGLE_SEARCH_API_KEY}&cx=${process.env.GOOGLE_SEARCH_CX}&q=${encodeURIComponent(query)}&num=5`);
61
- if (!response.ok)
62
- throw new Error(`Google API error: ${response.statusText}`);
63
- const data = await response.json();
64
- // Extract relevant fields to keep output clean
65
- const results = data.items?.map((item) => ({
66
- title: item.title,
67
- link: item.link,
68
- snippet: item.snippet
69
- })) || [];
70
- return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
85
+ catch (error) {
86
+ const errorMsg = `[${currentProvider}] ${error instanceof Error ? error.message : String(error)}`;
87
+ errors.push(errorMsg);
88
+ // Continue to next provider
71
89
  }
72
- return { content: [{ type: "text", text: "Error: Unsupported or unconfigured provider." }], isError: true };
73
- }
74
- catch (error) {
75
- return {
76
- content: [{ type: "text", text: `Search Error: ${error instanceof Error ? error.message : String(error)}` }],
77
- isError: true
78
- };
79
90
  }
91
+ // 4. All failed
92
+ return {
93
+ content: [{ type: "text", text: `All search providers failed:\n${errors.join('\n')}` }],
94
+ isError: true
95
+ };
80
96
  });
81
97
  // 2. fetch
82
98
  server.tool("fetch", "Perform an HTTP request to a specific URL.", {
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { registerWebTools } from './tools/web.js';
3
+ import * as utils from './utils.js';
4
+ // Mock utils
5
+ vi.mock('./utils.js', async (importOriginal) => {
6
+ const actual = await importOriginal();
7
+ return {
8
+ ...actual,
9
+ fetchWithRetry: vi.fn(),
10
+ };
11
+ });
12
+ describe('web_search fallback', () => {
13
+ let mockToolCallback;
14
+ const mockServer = {
15
+ tool: vi.fn((name, desc, schema, callback) => {
16
+ if (name === 'web_search') {
17
+ mockToolCallback = callback;
18
+ }
19
+ })
20
+ };
21
+ const originalEnv = process.env;
22
+ beforeEach(() => {
23
+ process.env = { ...originalEnv };
24
+ vi.clearAllMocks();
25
+ });
26
+ afterEach(() => {
27
+ process.env = originalEnv;
28
+ });
29
+ it('should use Brave if configured and no provider specified', async () => {
30
+ process.env.BRAVE_API_KEY = 'test-brave-key';
31
+ delete process.env.EXA_API_KEY;
32
+ delete process.env.GOOGLE_SEARCH_API_KEY;
33
+ registerWebTools(mockServer);
34
+ const mockResponse = {
35
+ ok: true,
36
+ json: async () => ({ web: { results: ['brave result'] } })
37
+ };
38
+ utils.fetchWithRetry.mockResolvedValue(mockResponse);
39
+ const result = await mockToolCallback({ query: 'test' });
40
+ expect(utils.fetchWithRetry).toHaveBeenCalledWith(expect.stringContaining('api.search.brave.com'), expect.anything());
41
+ expect(result.isError).toBeUndefined();
42
+ expect(JSON.parse(result.content[0].text)).toEqual(['brave result']);
43
+ });
44
+ it('should fallback to Exa if Brave fails', async () => {
45
+ process.env.BRAVE_API_KEY = 'test-brave-key';
46
+ process.env.EXA_API_KEY = 'test-exa-key';
47
+ registerWebTools(mockServer);
48
+ // First call (Brave) fails
49
+ utils.fetchWithRetry
50
+ .mockResolvedValueOnce({ ok: false, statusText: 'Brave Error', status: 500 })
51
+ // Second call (Exa) succeeds
52
+ .mockResolvedValueOnce({
53
+ ok: true,
54
+ json: async () => ({ results: ['exa result'] })
55
+ });
56
+ const result = await mockToolCallback({ query: 'test' });
57
+ expect(utils.fetchWithRetry).toHaveBeenCalledTimes(2);
58
+ // 1. Brave
59
+ expect(utils.fetchWithRetry).toHaveBeenNthCalledWith(1, expect.stringContaining('api.search.brave.com'), expect.anything());
60
+ // 2. Exa
61
+ expect(utils.fetchWithRetry).toHaveBeenNthCalledWith(2, expect.stringContaining('api.exa.ai'), expect.anything());
62
+ expect(result.isError).toBeUndefined();
63
+ expect(JSON.parse(result.content[0].text)).toEqual(['exa result']);
64
+ });
65
+ it('should respect requested provider and verify its availability', async () => {
66
+ process.env.BRAVE_API_KEY = 'test-brave-key';
67
+ // Exa not configured
68
+ delete process.env.EXA_API_KEY;
69
+ registerWebTools(mockServer);
70
+ const result = await mockToolCallback({ query: 'test', provider: 'exa' });
71
+ expect(result.isError).toBe(true);
72
+ expect(result.content[0].text).toContain("Requested provider 'exa' is not configured");
73
+ });
74
+ it('should try requested provider first, then fallback', async () => {
75
+ process.env.BRAVE_API_KEY = 'test-brave-key';
76
+ process.env.EXA_API_KEY = 'test-exa-key';
77
+ registerWebTools(mockServer);
78
+ // Request Exa
79
+ // Mock Exa fail, Brave success
80
+ utils.fetchWithRetry
81
+ .mockResolvedValueOnce({ ok: false, statusText: 'Exa Error', status: 500 })
82
+ .mockResolvedValueOnce({
83
+ ok: true,
84
+ json: async () => ({ web: { results: ['brave result'] } })
85
+ });
86
+ const result = await mockToolCallback({ query: 'test', provider: 'exa' });
87
+ expect(utils.fetchWithRetry).toHaveBeenCalledTimes(2);
88
+ // 1. Exa (requested)
89
+ expect(utils.fetchWithRetry).toHaveBeenNthCalledWith(1, expect.stringContaining('api.exa.ai'), expect.anything());
90
+ // 2. Brave (fallback)
91
+ expect(utils.fetchWithRetry).toHaveBeenNthCalledWith(2, expect.stringContaining('api.search.brave.com'), expect.anything());
92
+ expect(result.isError).toBeUndefined();
93
+ });
94
+ it('should return error if all fail', async () => {
95
+ process.env.BRAVE_API_KEY = 'test-brave-key';
96
+ registerWebTools(mockServer);
97
+ utils.fetchWithRetry.mockResolvedValue({ ok: false, statusText: 'Some Error', status: 500 });
98
+ const result = await mockToolCallback({ query: 'test' });
99
+ expect(result.isError).toBe(true);
100
+ expect(result.content[0].text).toContain("All search providers failed");
101
+ expect(result.content[0].text).toContain("Brave API error");
102
+ });
103
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotza02/sequential-thinking",
3
- "version": "2026.2.1",
3
+ "version": "2026.2.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },