gencode-ai 0.1.0 → 0.1.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/README.md +8 -90
- package/dist/agent/agent.d.ts +1 -1
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +8 -2
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/types.d.ts +9 -1
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/cli/components/AllModelsSelector.d.ts +11 -0
- package/dist/cli/components/AllModelsSelector.d.ts.map +1 -0
- package/dist/cli/components/AllModelsSelector.js +153 -0
- package/dist/cli/components/AllModelsSelector.js.map +1 -0
- package/dist/cli/components/App.d.ts.map +1 -1
- package/dist/cli/components/App.js +59 -25
- package/dist/cli/components/App.js.map +1 -1
- package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
- package/dist/cli/components/CommandSuggestions.js +1 -0
- package/dist/cli/components/CommandSuggestions.js.map +1 -1
- package/dist/cli/components/Messages.d.ts +15 -1
- package/dist/cli/components/Messages.d.ts.map +1 -1
- package/dist/cli/components/Messages.js +41 -15
- package/dist/cli/components/Messages.js.map +1 -1
- package/dist/cli/components/ModelSelector.d.ts +7 -7
- package/dist/cli/components/ModelSelector.d.ts.map +1 -1
- package/dist/cli/components/ModelSelector.js +116 -33
- package/dist/cli/components/ModelSelector.js.map +1 -1
- package/dist/cli/components/ProviderManager.d.ts +8 -0
- package/dist/cli/components/ProviderManager.d.ts.map +1 -0
- package/dist/cli/components/ProviderManager.js +280 -0
- package/dist/cli/components/ProviderManager.js.map +1 -0
- package/dist/cli/components/markdown.d.ts +9 -0
- package/dist/cli/components/markdown.d.ts.map +1 -0
- package/dist/cli/components/markdown.js +129 -0
- package/dist/cli/components/markdown.js.map +1 -0
- package/dist/cli/components/theme.d.ts +5 -0
- package/dist/cli/components/theme.d.ts.map +1 -1
- package/dist/cli/components/theme.js +7 -0
- package/dist/cli/components/theme.js.map +1 -1
- package/dist/cli/index.js +19 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/config/index.d.ts +3 -2
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +2 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/providers-config.d.ts +28 -0
- package/dist/config/providers-config.d.ts.map +1 -0
- package/dist/config/providers-config.js +79 -0
- package/dist/config/providers-config.js.map +1 -0
- package/dist/config/types.d.ts +31 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +1 -0
- package/dist/config/types.js.map +1 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +14 -3
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +5 -3
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +13 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/registry.d.ts +66 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +158 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/search/brave.d.ts +14 -0
- package/dist/providers/search/brave.d.ts.map +1 -0
- package/dist/providers/search/brave.js +87 -0
- package/dist/providers/search/brave.js.map +1 -0
- package/dist/providers/search/exa.d.ts +12 -0
- package/dist/providers/search/exa.d.ts.map +1 -0
- package/dist/providers/search/exa.js +158 -0
- package/dist/providers/search/exa.js.map +1 -0
- package/dist/providers/search/index.d.ts +31 -0
- package/dist/providers/search/index.d.ts.map +1 -0
- package/dist/providers/search/index.js +75 -0
- package/dist/providers/search/index.js.map +1 -0
- package/dist/providers/search/serper.d.ts +14 -0
- package/dist/providers/search/serper.d.ts.map +1 -0
- package/dist/providers/search/serper.js +87 -0
- package/dist/providers/search/serper.js.map +1 -0
- package/dist/providers/search/types.d.ts +21 -0
- package/dist/providers/search/types.d.ts.map +1 -0
- package/dist/providers/search/types.js +5 -0
- package/dist/providers/search/types.js.map +1 -0
- package/dist/providers/store.d.ts +104 -0
- package/dist/providers/store.d.ts.map +1 -0
- package/dist/providers/store.js +171 -0
- package/dist/providers/store.js.map +1 -0
- package/dist/providers/types.d.ts +7 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/vertex-ai.d.ts +33 -0
- package/dist/providers/vertex-ai.d.ts.map +1 -0
- package/dist/providers/vertex-ai.js +407 -0
- package/dist/providers/vertex-ai.js.map +1 -0
- package/dist/tools/builtin/webfetch.d.ts +20 -0
- package/dist/tools/builtin/webfetch.d.ts.map +1 -0
- package/dist/tools/builtin/webfetch.js +231 -0
- package/dist/tools/builtin/webfetch.js.map +1 -0
- package/dist/tools/builtin/websearch.d.ts +17 -0
- package/dist/tools/builtin/websearch.d.ts.map +1 -0
- package/dist/tools/builtin/websearch.js +101 -0
- package/dist/tools/builtin/websearch.js.map +1 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +24 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/types.d.ts +19 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js +8 -0
- package/dist/tools/types.js.map +1 -1
- package/dist/tools/utils/ssrf.d.ts +18 -0
- package/dist/tools/utils/ssrf.d.ts.map +1 -0
- package/dist/tools/utils/ssrf.js +70 -0
- package/dist/tools/utils/ssrf.js.map +1 -0
- package/docs/README.md +5 -4
- package/docs/proposals/0001-web-fetch-tool.md +32 -2
- package/docs/proposals/0002-web-search-tool.md +59 -2
- package/docs/proposals/0041-configuration-system.md +556 -0
- package/docs/proposals/README.md +3 -2
- package/docs/providers.md +220 -0
- package/package.json +7 -2
- package/src/agent/agent.ts +9 -2
- package/src/agent/types.ts +9 -1
- package/src/cli/components/App.tsx +72 -23
- package/src/cli/components/CommandSuggestions.tsx +1 -0
- package/src/cli/components/Messages.tsx +117 -29
- package/src/cli/components/ModelSelector.tsx +169 -52
- package/src/cli/components/ProviderManager.tsx +534 -0
- package/src/cli/components/markdown.ts +157 -0
- package/src/cli/components/theme.ts +7 -0
- package/src/cli/index.tsx +22 -7
- package/src/config/index.ts +3 -2
- package/src/config/providers-config.ts +85 -0
- package/src/config/types.ts +35 -1
- package/src/providers/gemini.ts +20 -4
- package/src/providers/index.ts +18 -3
- package/src/providers/registry.ts +198 -0
- package/src/providers/search/brave.ts +132 -0
- package/src/providers/search/exa.ts +217 -0
- package/src/providers/search/index.ts +79 -0
- package/src/providers/search/serper.ts +133 -0
- package/src/providers/search/types.ts +24 -0
- package/src/providers/store.ts +216 -0
- package/src/providers/types.ts +9 -1
- package/src/providers/vertex-ai.ts +594 -0
- package/src/tools/builtin/webfetch.ts +264 -0
- package/src/tools/builtin/websearch.ts +117 -0
- package/src/tools/index.ts +24 -2
- package/src/tools/types.ts +20 -0
- package/src/tools/utils/ssrf.ts +79 -0
- package/CLAUDE.md +0 -70
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exa AI Search Provider
|
|
3
|
+
*
|
|
4
|
+
* Uses Exa's public MCP endpoint (no API key required).
|
|
5
|
+
* Based on OpenCode's implementation pattern.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SearchProvider, SearchResult, SearchOptions } from './types.js';
|
|
9
|
+
|
|
10
|
+
const API_CONFIG = {
|
|
11
|
+
BASE_URL: 'https://mcp.exa.ai',
|
|
12
|
+
ENDPOINT: '/mcp',
|
|
13
|
+
DEFAULT_NUM_RESULTS: 8,
|
|
14
|
+
DEFAULT_TIMEOUT: 25000,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
interface McpSearchRequest {
|
|
18
|
+
jsonrpc: string;
|
|
19
|
+
id: number;
|
|
20
|
+
method: string;
|
|
21
|
+
params: {
|
|
22
|
+
name: string;
|
|
23
|
+
arguments: {
|
|
24
|
+
query: string;
|
|
25
|
+
numResults?: number;
|
|
26
|
+
livecrawl?: 'fallback' | 'preferred';
|
|
27
|
+
type?: 'auto' | 'fast' | 'deep';
|
|
28
|
+
contextMaxCharacters?: number;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface McpSearchResponse {
|
|
34
|
+
jsonrpc: string;
|
|
35
|
+
result: {
|
|
36
|
+
content: Array<{
|
|
37
|
+
type: string;
|
|
38
|
+
text: string;
|
|
39
|
+
}>;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse Exa's response text into structured search results
|
|
45
|
+
*
|
|
46
|
+
* Exa returns results in this format:
|
|
47
|
+
* Title: ...
|
|
48
|
+
* URL: ...
|
|
49
|
+
* Text: ...
|
|
50
|
+
*/
|
|
51
|
+
function parseExaResults(text: string): SearchResult[] {
|
|
52
|
+
const results: SearchResult[] = [];
|
|
53
|
+
const lines = text.split('\n');
|
|
54
|
+
|
|
55
|
+
let currentResult: Partial<SearchResult> = {};
|
|
56
|
+
let collectingText = false;
|
|
57
|
+
let textBuffer: string[] = [];
|
|
58
|
+
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
const trimmed = line.trim();
|
|
61
|
+
|
|
62
|
+
// Check for "Title: ..." line
|
|
63
|
+
if (trimmed.startsWith('Title:')) {
|
|
64
|
+
// Save previous result if exists
|
|
65
|
+
if (currentResult.title && currentResult.url) {
|
|
66
|
+
results.push({
|
|
67
|
+
title: currentResult.title,
|
|
68
|
+
url: currentResult.url,
|
|
69
|
+
snippet: textBuffer.join(' ').trim().substring(0, 300),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
currentResult = { title: trimmed.substring(6).trim() };
|
|
73
|
+
collectingText = false;
|
|
74
|
+
textBuffer = [];
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check for "URL: ..." line
|
|
79
|
+
if (trimmed.startsWith('URL:')) {
|
|
80
|
+
currentResult.url = trimmed.substring(4).trim();
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check for "Text: ..." line - start of snippet
|
|
85
|
+
if (trimmed.startsWith('Text:')) {
|
|
86
|
+
collectingText = true;
|
|
87
|
+
const initialText = trimmed.substring(5).trim();
|
|
88
|
+
if (initialText) {
|
|
89
|
+
textBuffer.push(initialText);
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Collect text lines until next Title
|
|
95
|
+
if (collectingText && trimmed && !trimmed.startsWith('Title:')) {
|
|
96
|
+
textBuffer.push(trimmed);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Add the last result
|
|
101
|
+
if (currentResult.title && currentResult.url) {
|
|
102
|
+
results.push({
|
|
103
|
+
title: currentResult.title,
|
|
104
|
+
url: currentResult.url,
|
|
105
|
+
snippet: textBuffer.join(' ').trim().substring(0, 300),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return results;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Filter results by allowed/blocked domains
|
|
114
|
+
*/
|
|
115
|
+
function filterByDomain(results: SearchResult[], options?: SearchOptions): SearchResult[] {
|
|
116
|
+
if (!options?.allowedDomains?.length && !options?.blockedDomains?.length) {
|
|
117
|
+
return results;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return results.filter((result) => {
|
|
121
|
+
try {
|
|
122
|
+
const domain = new URL(result.url).hostname;
|
|
123
|
+
|
|
124
|
+
if (options.allowedDomains?.length) {
|
|
125
|
+
return options.allowedDomains.some(
|
|
126
|
+
(allowed) => domain === allowed || domain.endsWith('.' + allowed)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (options.blockedDomains?.length) {
|
|
131
|
+
return !options.blockedDomains.some(
|
|
132
|
+
(blocked) => domain === blocked || domain.endsWith('.' + blocked)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return true;
|
|
137
|
+
} catch {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export class ExaProvider implements SearchProvider {
|
|
144
|
+
readonly name = 'exa' as const;
|
|
145
|
+
|
|
146
|
+
async search(query: string, options?: SearchOptions): Promise<SearchResult[]> {
|
|
147
|
+
const searchRequest: McpSearchRequest = {
|
|
148
|
+
jsonrpc: '2.0',
|
|
149
|
+
id: 1,
|
|
150
|
+
method: 'tools/call',
|
|
151
|
+
params: {
|
|
152
|
+
name: 'web_search_exa',
|
|
153
|
+
arguments: {
|
|
154
|
+
query,
|
|
155
|
+
type: 'auto',
|
|
156
|
+
numResults: options?.numResults ?? API_CONFIG.DEFAULT_NUM_RESULTS,
|
|
157
|
+
livecrawl: 'fallback',
|
|
158
|
+
contextMaxCharacters: 10000,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const timeoutId = setTimeout(
|
|
165
|
+
() => controller.abort(),
|
|
166
|
+
options?.timeout ?? API_CONFIG.DEFAULT_TIMEOUT
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const signals = options?.abortSignal
|
|
171
|
+
? [controller.signal, options.abortSignal]
|
|
172
|
+
: [controller.signal];
|
|
173
|
+
|
|
174
|
+
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINT}`, {
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: {
|
|
177
|
+
Accept: 'application/json, text/event-stream',
|
|
178
|
+
'Content-Type': 'application/json',
|
|
179
|
+
},
|
|
180
|
+
body: JSON.stringify(searchRequest),
|
|
181
|
+
signal: AbortSignal.any(signals),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
clearTimeout(timeoutId);
|
|
185
|
+
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
const errorText = await response.text();
|
|
188
|
+
throw new Error(`Exa search error (${response.status}): ${errorText}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const responseText = await response.text();
|
|
192
|
+
|
|
193
|
+
// Parse SSE response
|
|
194
|
+
const lines = responseText.split('\n');
|
|
195
|
+
for (const line of lines) {
|
|
196
|
+
if (line.startsWith('data: ')) {
|
|
197
|
+
const data: McpSearchResponse = JSON.parse(line.substring(6));
|
|
198
|
+
if (data.result?.content?.length > 0) {
|
|
199
|
+
const text = data.result.content[0].text;
|
|
200
|
+
const results = parseExaResults(text);
|
|
201
|
+
return filterByDomain(results, options);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return [];
|
|
207
|
+
} catch (error) {
|
|
208
|
+
clearTimeout(timeoutId);
|
|
209
|
+
|
|
210
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
211
|
+
throw new Error('Search request timed out');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Providers - Factory and exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export * from './types.js';
|
|
6
|
+
export { ExaProvider } from './exa.js';
|
|
7
|
+
export { SerperProvider } from './serper.js';
|
|
8
|
+
export { BraveProvider } from './brave.js';
|
|
9
|
+
|
|
10
|
+
import type { SearchProvider, SearchProviderName } from './types.js';
|
|
11
|
+
import { ExaProvider } from './exa.js';
|
|
12
|
+
import { SerperProvider } from './serper.js';
|
|
13
|
+
import { BraveProvider } from './brave.js';
|
|
14
|
+
import { getProviderStore } from '../store.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Detect search provider from environment variables
|
|
18
|
+
*/
|
|
19
|
+
function detectFromEnv(): SearchProviderName | undefined {
|
|
20
|
+
if (process.env.SERPER_API_KEY) return 'serper';
|
|
21
|
+
if (process.env.BRAVE_API_KEY) return 'brave';
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a search provider instance
|
|
27
|
+
*
|
|
28
|
+
* Priority:
|
|
29
|
+
* 1. Explicit name parameter
|
|
30
|
+
* 2. Configured in provider store
|
|
31
|
+
* 3. Detected from environment variables
|
|
32
|
+
* 4. Default: Exa (no key required)
|
|
33
|
+
*/
|
|
34
|
+
export function createSearchProvider(name?: SearchProviderName): SearchProvider {
|
|
35
|
+
const providerName = name ?? getProviderStore().getSearchProvider() ?? detectFromEnv() ?? 'exa';
|
|
36
|
+
|
|
37
|
+
switch (providerName) {
|
|
38
|
+
case 'serper':
|
|
39
|
+
return new SerperProvider();
|
|
40
|
+
case 'brave':
|
|
41
|
+
return new BraveProvider();
|
|
42
|
+
case 'exa':
|
|
43
|
+
default:
|
|
44
|
+
return new ExaProvider();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the name of the current search provider
|
|
50
|
+
*/
|
|
51
|
+
export function getCurrentSearchProviderName(): SearchProviderName {
|
|
52
|
+
return getProviderStore().getSearchProvider() ?? detectFromEnv() ?? 'exa';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if a search provider is available (has required API keys)
|
|
57
|
+
*/
|
|
58
|
+
export function isSearchProviderAvailable(name: SearchProviderName): boolean {
|
|
59
|
+
switch (name) {
|
|
60
|
+
case 'exa':
|
|
61
|
+
return true; // Always available
|
|
62
|
+
case 'serper':
|
|
63
|
+
return !!process.env.SERPER_API_KEY;
|
|
64
|
+
case 'brave':
|
|
65
|
+
return !!process.env.BRAVE_API_KEY;
|
|
66
|
+
default:
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get all available search providers
|
|
73
|
+
*/
|
|
74
|
+
export function getAvailableSearchProviders(): SearchProviderName[] {
|
|
75
|
+
const providers: SearchProviderName[] = ['exa']; // Always available
|
|
76
|
+
if (process.env.SERPER_API_KEY) providers.push('serper');
|
|
77
|
+
if (process.env.BRAVE_API_KEY) providers.push('brave');
|
|
78
|
+
return providers;
|
|
79
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serper.dev Search Provider
|
|
3
|
+
*
|
|
4
|
+
* Uses Google Search via Serper.dev API.
|
|
5
|
+
* Requires SERPER_API_KEY environment variable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SearchProvider, SearchResult, SearchOptions } from './types.js';
|
|
9
|
+
|
|
10
|
+
const API_CONFIG = {
|
|
11
|
+
BASE_URL: 'https://google.serper.dev',
|
|
12
|
+
ENDPOINT: '/search',
|
|
13
|
+
DEFAULT_NUM_RESULTS: 10,
|
|
14
|
+
DEFAULT_TIMEOUT: 10000,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
interface SerperRequest {
|
|
18
|
+
q: string;
|
|
19
|
+
num?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SerperOrganicResult {
|
|
23
|
+
title: string;
|
|
24
|
+
link: string;
|
|
25
|
+
snippet: string;
|
|
26
|
+
position: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface SerperResponse {
|
|
30
|
+
organic: SerperOrganicResult[];
|
|
31
|
+
searchParameters: {
|
|
32
|
+
q: string;
|
|
33
|
+
type: string;
|
|
34
|
+
num: number;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Filter results by allowed/blocked domains
|
|
40
|
+
*/
|
|
41
|
+
function filterByDomain(results: SearchResult[], options?: SearchOptions): SearchResult[] {
|
|
42
|
+
if (!options?.allowedDomains?.length && !options?.blockedDomains?.length) {
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return results.filter((result) => {
|
|
47
|
+
try {
|
|
48
|
+
const domain = new URL(result.url).hostname;
|
|
49
|
+
|
|
50
|
+
if (options.allowedDomains?.length) {
|
|
51
|
+
return options.allowedDomains.some(
|
|
52
|
+
(allowed) => domain === allowed || domain.endsWith('.' + allowed)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (options.blockedDomains?.length) {
|
|
57
|
+
return !options.blockedDomains.some(
|
|
58
|
+
(blocked) => domain === blocked || domain.endsWith('.' + blocked)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return true;
|
|
63
|
+
} catch {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class SerperProvider implements SearchProvider {
|
|
70
|
+
readonly name = 'serper' as const;
|
|
71
|
+
private apiKey: string;
|
|
72
|
+
|
|
73
|
+
constructor(apiKey?: string) {
|
|
74
|
+
this.apiKey = apiKey ?? process.env.SERPER_API_KEY ?? '';
|
|
75
|
+
if (!this.apiKey) {
|
|
76
|
+
throw new Error('SERPER_API_KEY environment variable is required for Serper provider');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async search(query: string, options?: SearchOptions): Promise<SearchResult[]> {
|
|
81
|
+
const searchRequest: SerperRequest = {
|
|
82
|
+
q: query,
|
|
83
|
+
num: options?.numResults ?? API_CONFIG.DEFAULT_NUM_RESULTS,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const timeoutId = setTimeout(
|
|
88
|
+
() => controller.abort(),
|
|
89
|
+
options?.timeout ?? API_CONFIG.DEFAULT_TIMEOUT
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const signals = options?.abortSignal
|
|
94
|
+
? [controller.signal, options.abortSignal]
|
|
95
|
+
: [controller.signal];
|
|
96
|
+
|
|
97
|
+
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINT}`, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: {
|
|
100
|
+
'X-API-KEY': this.apiKey,
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify(searchRequest),
|
|
104
|
+
signal: AbortSignal.any(signals),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
clearTimeout(timeoutId);
|
|
108
|
+
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
const errorText = await response.text();
|
|
111
|
+
throw new Error(`Serper search error (${response.status}): ${errorText}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const data = await response.json() as SerperResponse;
|
|
115
|
+
|
|
116
|
+
const results: SearchResult[] = (data.organic || []).map((item) => ({
|
|
117
|
+
title: item.title,
|
|
118
|
+
url: item.link,
|
|
119
|
+
snippet: item.snippet,
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
return filterByDomain(results, options);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
clearTimeout(timeoutId);
|
|
125
|
+
|
|
126
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
127
|
+
throw new Error('Search request timed out');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Provider Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type SearchProviderName = 'exa' | 'serper' | 'brave';
|
|
6
|
+
|
|
7
|
+
export interface SearchResult {
|
|
8
|
+
title: string;
|
|
9
|
+
url: string;
|
|
10
|
+
snippet: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SearchOptions {
|
|
14
|
+
numResults?: number;
|
|
15
|
+
allowedDomains?: string[];
|
|
16
|
+
blockedDomains?: string[];
|
|
17
|
+
timeout?: number;
|
|
18
|
+
abortSignal?: AbortSignal;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SearchProvider {
|
|
22
|
+
readonly name: SearchProviderName;
|
|
23
|
+
search(query: string, options?: SearchOptions): Promise<SearchResult[]>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Store - Manages provider connections and model cache
|
|
3
|
+
*
|
|
4
|
+
* Storage location: ~/.gencode/providers.json
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import type { ProviderName } from './index.js';
|
|
11
|
+
import type { SearchProviderName } from './search/types.js';
|
|
12
|
+
|
|
13
|
+
export interface ModelInfo {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ProviderConnection {
|
|
19
|
+
method: string;
|
|
20
|
+
connectedAt: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ModelCache {
|
|
24
|
+
cachedAt: string;
|
|
25
|
+
list: ModelInfo[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ProvidersConfig {
|
|
29
|
+
connections: Record<string, ProviderConnection>;
|
|
30
|
+
models: Record<string, ModelCache>;
|
|
31
|
+
searchProvider?: SearchProviderName;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const CONFIG_DIR = join(homedir(), '.gencode');
|
|
35
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'providers.json');
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Provider Store - manages connection state and model cache
|
|
39
|
+
*/
|
|
40
|
+
export class ProviderStore {
|
|
41
|
+
private config: ProvidersConfig;
|
|
42
|
+
|
|
43
|
+
constructor() {
|
|
44
|
+
this.config = this.load();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Load configuration from disk
|
|
49
|
+
*/
|
|
50
|
+
private load(): ProvidersConfig {
|
|
51
|
+
try {
|
|
52
|
+
if (existsSync(CONFIG_FILE)) {
|
|
53
|
+
const data = readFileSync(CONFIG_FILE, 'utf-8');
|
|
54
|
+
return JSON.parse(data);
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Ignore parse errors, start fresh
|
|
58
|
+
}
|
|
59
|
+
return { connections: {}, models: {} };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Save configuration to disk
|
|
64
|
+
*/
|
|
65
|
+
private save(): void {
|
|
66
|
+
try {
|
|
67
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
68
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(this.config, null, 2));
|
|
71
|
+
} catch {
|
|
72
|
+
// Silently fail if we can't write
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if a provider is connected
|
|
78
|
+
*/
|
|
79
|
+
isConnected(providerId: ProviderName): boolean {
|
|
80
|
+
return !!this.config.connections[providerId];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get connection info for a provider
|
|
85
|
+
*/
|
|
86
|
+
getConnection(providerId: ProviderName): ProviderConnection | undefined {
|
|
87
|
+
return this.config.connections[providerId];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get all connected provider IDs
|
|
92
|
+
*/
|
|
93
|
+
getConnectedProviders(): ProviderName[] {
|
|
94
|
+
return Object.keys(this.config.connections) as ProviderName[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Connect a provider
|
|
99
|
+
*/
|
|
100
|
+
connect(providerId: ProviderName, method: string): void {
|
|
101
|
+
this.config.connections[providerId] = {
|
|
102
|
+
method,
|
|
103
|
+
connectedAt: new Date().toISOString(),
|
|
104
|
+
};
|
|
105
|
+
this.save();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Disconnect a provider
|
|
110
|
+
*/
|
|
111
|
+
disconnect(providerId: ProviderName): void {
|
|
112
|
+
delete this.config.connections[providerId];
|
|
113
|
+
delete this.config.models[providerId];
|
|
114
|
+
this.save();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get cached models for a provider
|
|
119
|
+
*/
|
|
120
|
+
getModels(providerId: ProviderName): ModelInfo[] {
|
|
121
|
+
return this.config.models[providerId]?.list ?? [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get all cached models grouped by provider
|
|
126
|
+
*/
|
|
127
|
+
getAllModels(): Record<ProviderName, ModelInfo[]> {
|
|
128
|
+
const result: Record<string, ModelInfo[]> = {};
|
|
129
|
+
for (const [providerId, cache] of Object.entries(this.config.models)) {
|
|
130
|
+
result[providerId] = cache.list;
|
|
131
|
+
}
|
|
132
|
+
return result as Record<ProviderName, ModelInfo[]>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Cache models for a provider
|
|
137
|
+
*/
|
|
138
|
+
cacheModels(providerId: ProviderName, models: ModelInfo[]): void {
|
|
139
|
+
this.config.models[providerId] = {
|
|
140
|
+
cachedAt: new Date().toISOString(),
|
|
141
|
+
list: models,
|
|
142
|
+
};
|
|
143
|
+
this.save();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get cache timestamp for a provider
|
|
148
|
+
*/
|
|
149
|
+
getCacheTime(providerId: ProviderName): Date | undefined {
|
|
150
|
+
const cache = this.config.models[providerId];
|
|
151
|
+
return cache ? new Date(cache.cachedAt) : undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if model cache is stale (older than 24 hours)
|
|
156
|
+
*/
|
|
157
|
+
isCacheStale(providerId: ProviderName): boolean {
|
|
158
|
+
const cacheTime = this.getCacheTime(providerId);
|
|
159
|
+
if (!cacheTime) return true;
|
|
160
|
+
const hoursSinceCache = (Date.now() - cacheTime.getTime()) / (1000 * 60 * 60);
|
|
161
|
+
return hoursSinceCache > 24;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get total model count across all connected providers
|
|
166
|
+
*/
|
|
167
|
+
getTotalModelCount(): number {
|
|
168
|
+
return Object.values(this.config.models).reduce(
|
|
169
|
+
(sum, cache) => sum + cache.list.length,
|
|
170
|
+
0
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get model count for a specific provider
|
|
176
|
+
*/
|
|
177
|
+
getModelCount(providerId: ProviderName): number {
|
|
178
|
+
return this.config.models[providerId]?.list.length ?? 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get the configured search provider
|
|
183
|
+
*/
|
|
184
|
+
getSearchProvider(): SearchProviderName | undefined {
|
|
185
|
+
return this.config.searchProvider;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Set the search provider
|
|
190
|
+
*/
|
|
191
|
+
setSearchProvider(id: SearchProviderName): void {
|
|
192
|
+
this.config.searchProvider = id;
|
|
193
|
+
this.save();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Clear the search provider (use default)
|
|
198
|
+
*/
|
|
199
|
+
clearSearchProvider(): void {
|
|
200
|
+
delete this.config.searchProvider;
|
|
201
|
+
this.save();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Singleton instance
|
|
206
|
+
let storeInstance: ProviderStore | null = null;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get the singleton provider store instance
|
|
210
|
+
*/
|
|
211
|
+
export function getProviderStore(): ProviderStore {
|
|
212
|
+
if (!storeInstance) {
|
|
213
|
+
storeInstance = new ProviderStore();
|
|
214
|
+
}
|
|
215
|
+
return storeInstance;
|
|
216
|
+
}
|
package/src/providers/types.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface ToolUseContent {
|
|
|
19
19
|
id: string;
|
|
20
20
|
name: string;
|
|
21
21
|
input: Record<string, unknown>;
|
|
22
|
+
// Gemini 3+ thought signature for maintaining reasoning chain
|
|
23
|
+
thoughtSignature?: string;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export interface ToolResultContent {
|
|
@@ -178,4 +180,10 @@ export interface GeminiConfig {
|
|
|
178
180
|
apiKey?: string;
|
|
179
181
|
}
|
|
180
182
|
|
|
181
|
-
export
|
|
183
|
+
export interface VertexAIConfig {
|
|
184
|
+
projectId?: string;
|
|
185
|
+
region?: string;
|
|
186
|
+
accessToken?: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export type ProviderConfig = OpenAIConfig | AnthropicConfig | GeminiConfig | VertexAIConfig;
|