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,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebFetch Tool - Fetch and convert web content
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import TurndownService from 'turndown';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import type { Tool, ToolContext, ToolResult } from '../types.js';
|
|
8
|
+
import { getErrorMessage } from '../types.js';
|
|
9
|
+
import { validateUrl } from '../utils/ssrf.js';
|
|
10
|
+
|
|
11
|
+
// Constants
|
|
12
|
+
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
13
|
+
const DEFAULT_TIMEOUT = 30 * 1000; // 30 seconds
|
|
14
|
+
const MAX_TIMEOUT = 120 * 1000; // 2 minutes
|
|
15
|
+
const MAX_LINE_LENGTH = 2000;
|
|
16
|
+
const MAX_OUTPUT_LENGTH = 50000;
|
|
17
|
+
|
|
18
|
+
// Input schema
|
|
19
|
+
export const WebFetchInputSchema = z.object({
|
|
20
|
+
url: z.string().describe('The URL to fetch content from (http:// or https://)'),
|
|
21
|
+
format: z
|
|
22
|
+
.enum(['text', 'markdown', 'html'])
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('Output format: markdown (default), text, or html'),
|
|
25
|
+
timeout: z.number().optional().describe('Timeout in seconds (default: 30, max: 120)'),
|
|
26
|
+
});
|
|
27
|
+
export type WebFetchInput = z.infer<typeof WebFetchInputSchema>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get Accept header based on requested format
|
|
31
|
+
*/
|
|
32
|
+
function getAcceptHeader(format: string): string {
|
|
33
|
+
switch (format) {
|
|
34
|
+
case 'markdown':
|
|
35
|
+
return 'text/markdown, text/plain, text/html;q=0.9, */*;q=0.1';
|
|
36
|
+
case 'text':
|
|
37
|
+
return 'text/plain, text/html;q=0.8, */*;q=0.1';
|
|
38
|
+
case 'html':
|
|
39
|
+
return 'text/html, application/xhtml+xml, */*;q=0.1';
|
|
40
|
+
default:
|
|
41
|
+
return 'text/html, */*;q=0.1';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Convert HTML to Markdown using Turndown
|
|
47
|
+
*/
|
|
48
|
+
function convertHtmlToMarkdown(html: string): string {
|
|
49
|
+
const turndown = new TurndownService({
|
|
50
|
+
headingStyle: 'atx',
|
|
51
|
+
hr: '---',
|
|
52
|
+
bulletListMarker: '-',
|
|
53
|
+
codeBlockStyle: 'fenced',
|
|
54
|
+
emDelimiter: '*',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Remove script, style, meta, link, noscript tags
|
|
58
|
+
turndown.remove(['script', 'style', 'meta', 'link', 'noscript']);
|
|
59
|
+
|
|
60
|
+
return turndown.turndown(html);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Extract plain text from HTML
|
|
65
|
+
*/
|
|
66
|
+
function extractTextFromHtml(html: string): string {
|
|
67
|
+
return (
|
|
68
|
+
html
|
|
69
|
+
// Remove script and style content
|
|
70
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
71
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
72
|
+
.replace(/<noscript[^>]*>[\s\S]*?<\/noscript>/gi, '')
|
|
73
|
+
// Remove all tags
|
|
74
|
+
.replace(/<[^>]+>/g, ' ')
|
|
75
|
+
// Decode common HTML entities
|
|
76
|
+
.replace(/ /g, ' ')
|
|
77
|
+
.replace(/</g, '<')
|
|
78
|
+
.replace(/>/g, '>')
|
|
79
|
+
.replace(/&/g, '&')
|
|
80
|
+
.replace(/"/g, '"')
|
|
81
|
+
.replace(/&#(\d+);/g, (_, num) => String.fromCharCode(parseInt(num)))
|
|
82
|
+
// Normalize whitespace
|
|
83
|
+
.replace(/\s+/g, ' ')
|
|
84
|
+
.trim()
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Process content based on content type and requested format
|
|
90
|
+
*/
|
|
91
|
+
function processContent(content: string, contentType: string, format: string): string {
|
|
92
|
+
const isHtml = contentType.includes('text/html') || contentType.includes('application/xhtml');
|
|
93
|
+
|
|
94
|
+
switch (format) {
|
|
95
|
+
case 'markdown':
|
|
96
|
+
if (isHtml) {
|
|
97
|
+
return convertHtmlToMarkdown(content);
|
|
98
|
+
}
|
|
99
|
+
return content;
|
|
100
|
+
|
|
101
|
+
case 'text':
|
|
102
|
+
if (isHtml) {
|
|
103
|
+
return extractTextFromHtml(content);
|
|
104
|
+
}
|
|
105
|
+
return content;
|
|
106
|
+
|
|
107
|
+
case 'html':
|
|
108
|
+
return content;
|
|
109
|
+
|
|
110
|
+
default:
|
|
111
|
+
return content;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Format bytes to human-readable size
|
|
117
|
+
*/
|
|
118
|
+
function formatSize(bytes: number): string {
|
|
119
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
120
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
121
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Truncate output to prevent excessive content
|
|
126
|
+
*/
|
|
127
|
+
function truncateOutput(output: string): string {
|
|
128
|
+
// Truncate long lines
|
|
129
|
+
const lines = output.split('\n').map((line) => {
|
|
130
|
+
if (line.length > MAX_LINE_LENGTH) {
|
|
131
|
+
return line.slice(0, MAX_LINE_LENGTH) + '... (truncated)';
|
|
132
|
+
}
|
|
133
|
+
return line;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
let result = lines.join('\n');
|
|
137
|
+
|
|
138
|
+
// Truncate overall output
|
|
139
|
+
if (result.length > MAX_OUTPUT_LENGTH) {
|
|
140
|
+
result = result.slice(0, MAX_OUTPUT_LENGTH) + '\n\n... (output truncated)';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* WebFetch Tool
|
|
148
|
+
*/
|
|
149
|
+
export const webfetchTool: Tool<WebFetchInput> = {
|
|
150
|
+
name: 'WebFetch',
|
|
151
|
+
description: `Fetch content from a URL and return it in the specified format.
|
|
152
|
+
- Converts HTML to Markdown by default for easier reading
|
|
153
|
+
- Supports text, markdown, and html output formats
|
|
154
|
+
- Maximum response size: 5MB
|
|
155
|
+
- Timeout: 30 seconds (configurable up to 120 seconds)`,
|
|
156
|
+
parameters: WebFetchInputSchema,
|
|
157
|
+
|
|
158
|
+
async execute(input: WebFetchInput, context: ToolContext): Promise<ToolResult> {
|
|
159
|
+
const startTime = Date.now();
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// Validate URL (SSRF protection)
|
|
163
|
+
validateUrl(input.url);
|
|
164
|
+
|
|
165
|
+
// Calculate timeout
|
|
166
|
+
const timeoutMs = input.timeout
|
|
167
|
+
? Math.min(input.timeout * 1000, MAX_TIMEOUT)
|
|
168
|
+
: DEFAULT_TIMEOUT;
|
|
169
|
+
|
|
170
|
+
// Create abort controller for timeout
|
|
171
|
+
const controller = new AbortController();
|
|
172
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
173
|
+
|
|
174
|
+
// Combine with context abort signal if present
|
|
175
|
+
const signal = context.abortSignal
|
|
176
|
+
? AbortSignal.any([controller.signal, context.abortSignal])
|
|
177
|
+
: controller.signal;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
// Fetch with appropriate headers
|
|
181
|
+
const response = await fetch(input.url, {
|
|
182
|
+
signal,
|
|
183
|
+
headers: {
|
|
184
|
+
'User-Agent': 'GenCode/1.0 (+https://github.com/gencode)',
|
|
185
|
+
Accept: getAcceptHeader(input.format ?? 'markdown'),
|
|
186
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
187
|
+
},
|
|
188
|
+
redirect: 'follow',
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
clearTimeout(timeoutId);
|
|
192
|
+
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
output: '',
|
|
197
|
+
error: `HTTP ${response.status}: ${response.statusText}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check content length header
|
|
202
|
+
const contentLength = response.headers.get('content-length');
|
|
203
|
+
if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE) {
|
|
204
|
+
return {
|
|
205
|
+
success: false,
|
|
206
|
+
output: '',
|
|
207
|
+
error: `Response too large: ${contentLength} bytes (max: ${MAX_RESPONSE_SIZE})`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Read response body with size limit
|
|
212
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
213
|
+
if (arrayBuffer.byteLength > MAX_RESPONSE_SIZE) {
|
|
214
|
+
return {
|
|
215
|
+
success: false,
|
|
216
|
+
output: '',
|
|
217
|
+
error: `Response too large: ${arrayBuffer.byteLength} bytes (max: ${MAX_RESPONSE_SIZE})`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const content = new TextDecoder().decode(arrayBuffer);
|
|
222
|
+
const contentType = response.headers.get('content-type') || '';
|
|
223
|
+
|
|
224
|
+
// Process content based on format
|
|
225
|
+
let output = processContent(content, contentType, input.format ?? 'markdown');
|
|
226
|
+
|
|
227
|
+
// Truncate long lines and overall output
|
|
228
|
+
output = truncateOutput(output);
|
|
229
|
+
|
|
230
|
+
// Build result with metadata for improved display
|
|
231
|
+
const size = arrayBuffer.byteLength;
|
|
232
|
+
const duration = Date.now() - startTime;
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
success: true,
|
|
236
|
+
output: output,
|
|
237
|
+
metadata: {
|
|
238
|
+
title: `Fetch(${input.url})`,
|
|
239
|
+
subtitle: `Received ${formatSize(size)} (${response.status} ${response.statusText})`,
|
|
240
|
+
size,
|
|
241
|
+
statusCode: response.status,
|
|
242
|
+
contentType: contentType,
|
|
243
|
+
duration,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
} finally {
|
|
247
|
+
clearTimeout(timeoutId);
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
251
|
+
return {
|
|
252
|
+
success: false,
|
|
253
|
+
output: '',
|
|
254
|
+
error: 'Request timed out',
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
success: false,
|
|
259
|
+
output: '',
|
|
260
|
+
error: `Fetch failed: ${getErrorMessage(error)}`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSearch Tool - Search the web for current information
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import type { Tool, ToolContext, ToolResult } from '../types.js';
|
|
7
|
+
import { getErrorMessage } from '../types.js';
|
|
8
|
+
import {
|
|
9
|
+
createSearchProvider,
|
|
10
|
+
getCurrentSearchProviderName,
|
|
11
|
+
type SearchResult,
|
|
12
|
+
} from '../../providers/search/index.js';
|
|
13
|
+
|
|
14
|
+
// Constants
|
|
15
|
+
const DEFAULT_NUM_RESULTS = 10;
|
|
16
|
+
|
|
17
|
+
// Input schema
|
|
18
|
+
export const WebSearchInputSchema = z.object({
|
|
19
|
+
query: z
|
|
20
|
+
.string()
|
|
21
|
+
.min(2)
|
|
22
|
+
.describe('The search query (minimum 2 characters)'),
|
|
23
|
+
allowed_domains: z
|
|
24
|
+
.array(z.string())
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('Only include results from these domains'),
|
|
27
|
+
blocked_domains: z
|
|
28
|
+
.array(z.string())
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('Exclude results from these domains'),
|
|
31
|
+
num_results: z
|
|
32
|
+
.number()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe(`Number of results to return (default: ${DEFAULT_NUM_RESULTS})`),
|
|
35
|
+
});
|
|
36
|
+
export type WebSearchInput = z.infer<typeof WebSearchInputSchema>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Format search results as markdown
|
|
40
|
+
*/
|
|
41
|
+
function formatResults(results: SearchResult[], query: string): string {
|
|
42
|
+
if (results.length === 0) {
|
|
43
|
+
return `No results found for "${query}".`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lines: string[] = [`Found ${results.length} results for "${query}":\n`];
|
|
47
|
+
|
|
48
|
+
results.forEach((result, index) => {
|
|
49
|
+
lines.push(`${index + 1}. [${result.title}](${result.url})`);
|
|
50
|
+
if (result.snippet) {
|
|
51
|
+
lines.push(` ${result.snippet}\n`);
|
|
52
|
+
} else {
|
|
53
|
+
lines.push('');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* WebSearch Tool
|
|
62
|
+
*/
|
|
63
|
+
export const websearchTool: Tool<WebSearchInput> = {
|
|
64
|
+
name: 'WebSearch',
|
|
65
|
+
description: `Search the web for current information.
|
|
66
|
+
|
|
67
|
+
Use this tool when you need:
|
|
68
|
+
- Up-to-date information beyond your knowledge cutoff
|
|
69
|
+
- Current documentation or release notes
|
|
70
|
+
- Recent solutions to technical problems
|
|
71
|
+
- Current best practices
|
|
72
|
+
|
|
73
|
+
IMPORTANT: After answering, include a "Sources:" section with all relevant URLs as markdown hyperlinks.
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
[Your answer]
|
|
77
|
+
|
|
78
|
+
Sources:
|
|
79
|
+
- [Title 1](https://url1)
|
|
80
|
+
- [Title 2](https://url2)`,
|
|
81
|
+
parameters: WebSearchInputSchema,
|
|
82
|
+
|
|
83
|
+
async execute(input: WebSearchInput, context: ToolContext): Promise<ToolResult> {
|
|
84
|
+
const startTime = Date.now();
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const provider = createSearchProvider();
|
|
88
|
+
|
|
89
|
+
const results = await provider.search(input.query, {
|
|
90
|
+
numResults: input.num_results ?? DEFAULT_NUM_RESULTS,
|
|
91
|
+
allowedDomains: input.allowed_domains,
|
|
92
|
+
blockedDomains: input.blocked_domains,
|
|
93
|
+
abortSignal: context.abortSignal,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const output = formatResults(results, input.query);
|
|
97
|
+
const duration = Date.now() - startTime;
|
|
98
|
+
const providerName = getCurrentSearchProviderName();
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
output,
|
|
103
|
+
metadata: {
|
|
104
|
+
title: `Search("${input.query}")`,
|
|
105
|
+
subtitle: `Found ${results.length} results via ${providerName}`,
|
|
106
|
+
duration,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
output: '',
|
|
113
|
+
error: `Search failed: ${getErrorMessage(error)}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
};
|
package/src/tools/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ export { editTool } from './builtin/edit.js';
|
|
|
12
12
|
export { bashTool } from './builtin/bash.js';
|
|
13
13
|
export { globTool } from './builtin/glob.js';
|
|
14
14
|
export { grepTool } from './builtin/grep.js';
|
|
15
|
+
export { webfetchTool } from './builtin/webfetch.js';
|
|
16
|
+
export { websearchTool } from './builtin/websearch.js';
|
|
15
17
|
|
|
16
18
|
import { ToolRegistry } from './registry.js';
|
|
17
19
|
import { readTool } from './builtin/read.js';
|
|
@@ -20,17 +22,37 @@ import { editTool } from './builtin/edit.js';
|
|
|
20
22
|
import { bashTool } from './builtin/bash.js';
|
|
21
23
|
import { globTool } from './builtin/glob.js';
|
|
22
24
|
import { grepTool } from './builtin/grep.js';
|
|
25
|
+
import { webfetchTool } from './builtin/webfetch.js';
|
|
26
|
+
import { websearchTool } from './builtin/websearch.js';
|
|
23
27
|
|
|
24
28
|
/**
|
|
25
29
|
* Create a registry with all built-in tools
|
|
26
30
|
*/
|
|
27
31
|
export function createDefaultRegistry(): ToolRegistry {
|
|
28
32
|
const registry = new ToolRegistry();
|
|
29
|
-
registry.registerAll([
|
|
33
|
+
registry.registerAll([
|
|
34
|
+
readTool,
|
|
35
|
+
writeTool,
|
|
36
|
+
editTool,
|
|
37
|
+
bashTool,
|
|
38
|
+
globTool,
|
|
39
|
+
grepTool,
|
|
40
|
+
webfetchTool,
|
|
41
|
+
websearchTool,
|
|
42
|
+
]);
|
|
30
43
|
return registry;
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
/**
|
|
34
47
|
* All built-in tools
|
|
35
48
|
*/
|
|
36
|
-
export const builtinTools = [
|
|
49
|
+
export const builtinTools = [
|
|
50
|
+
readTool,
|
|
51
|
+
writeTool,
|
|
52
|
+
editTool,
|
|
53
|
+
bashTool,
|
|
54
|
+
globTool,
|
|
55
|
+
grepTool,
|
|
56
|
+
webfetchTool,
|
|
57
|
+
websearchTool,
|
|
58
|
+
];
|
package/src/tools/types.ts
CHANGED
|
@@ -14,10 +14,20 @@ export interface ToolContext {
|
|
|
14
14
|
abortSignal?: AbortSignal;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export interface ToolResultMetadata {
|
|
18
|
+
title?: string; // Short title, e.g., "Fetch(url)"
|
|
19
|
+
subtitle?: string; // Subtitle, e.g., "Received 540.3KB (200 OK)"
|
|
20
|
+
size?: number; // Response size in bytes
|
|
21
|
+
statusCode?: number; // HTTP status code
|
|
22
|
+
contentType?: string; // Content-Type header
|
|
23
|
+
duration?: number; // Duration in milliseconds
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
export interface ToolResult {
|
|
18
27
|
success: boolean;
|
|
19
28
|
output: string;
|
|
20
29
|
error?: string;
|
|
30
|
+
metadata?: ToolResultMetadata;
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
export interface Tool<TInput = unknown> {
|
|
@@ -88,6 +98,16 @@ export const GrepInputSchema = z.object({
|
|
|
88
98
|
});
|
|
89
99
|
export type GrepInput = z.infer<typeof GrepInputSchema>;
|
|
90
100
|
|
|
101
|
+
export const WebFetchInputSchema = z.object({
|
|
102
|
+
url: z.string().describe('The URL to fetch content from (http:// or https://)'),
|
|
103
|
+
format: z
|
|
104
|
+
.enum(['text', 'markdown', 'html'])
|
|
105
|
+
.optional()
|
|
106
|
+
.describe('Output format: markdown (default), text, or html'),
|
|
107
|
+
timeout: z.number().optional().describe('Timeout in seconds (default: 30, max: 120)'),
|
|
108
|
+
});
|
|
109
|
+
export type WebFetchInput = z.infer<typeof WebFetchInputSchema>;
|
|
110
|
+
|
|
91
111
|
// ============================================================================
|
|
92
112
|
// JSON Schema Conversion
|
|
93
113
|
// ============================================================================
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF Protection Utilities
|
|
3
|
+
* Prevents Server-Side Request Forgery by blocking internal/private addresses
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Private IP ranges (RFC 1918 + loopback + link-local + cloud metadata)
|
|
7
|
+
const PRIVATE_IP_PATTERNS = [
|
|
8
|
+
/^127\./, // Loopback (127.0.0.0/8)
|
|
9
|
+
/^10\./, // Class A private (10.0.0.0/8)
|
|
10
|
+
/^172\.(1[6-9]|2[0-9]|3[01])\./, // Class B private (172.16.0.0/12)
|
|
11
|
+
/^192\.168\./, // Class C private (192.168.0.0/16)
|
|
12
|
+
/^169\.254\./, // Link-local (169.254.0.0/16)
|
|
13
|
+
/^0\./, // "This" network
|
|
14
|
+
/^::1$/, // IPv6 loopback
|
|
15
|
+
/^fe80:/i, // IPv6 link-local
|
|
16
|
+
/^fc00:/i, // IPv6 unique local
|
|
17
|
+
/^fd[0-9a-f]{2}:/i, // IPv6 unique local
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const BLOCKED_HOSTNAMES = [
|
|
21
|
+
'localhost',
|
|
22
|
+
'localhost.localdomain',
|
|
23
|
+
'metadata.google.internal', // GCP metadata
|
|
24
|
+
'169.254.169.254', // AWS/GCP/Azure metadata
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if an IP address is in a private range
|
|
29
|
+
*/
|
|
30
|
+
export function isPrivateIP(ip: string): boolean {
|
|
31
|
+
return PRIVATE_IP_PATTERNS.some((pattern) => pattern.test(ip));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if a hostname is blocked
|
|
36
|
+
*/
|
|
37
|
+
export function isBlockedHostname(hostname: string): boolean {
|
|
38
|
+
const lower = hostname.toLowerCase();
|
|
39
|
+
|
|
40
|
+
// Direct match
|
|
41
|
+
if (BLOCKED_HOSTNAMES.includes(lower)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check .local suffix
|
|
46
|
+
if (lower.endsWith('.local')) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validate a URL for SSRF protection
|
|
55
|
+
* Throws an error if the URL is not allowed
|
|
56
|
+
*/
|
|
57
|
+
export function validateUrl(urlString: string): void {
|
|
58
|
+
let parsed: URL;
|
|
59
|
+
try {
|
|
60
|
+
parsed = new URL(urlString);
|
|
61
|
+
} catch {
|
|
62
|
+
throw new Error('Invalid URL format');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Only allow http/https protocols
|
|
66
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
67
|
+
throw new Error('Only http:// and https:// URLs are supported');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check hostname blocklist
|
|
71
|
+
if (isBlockedHostname(parsed.hostname)) {
|
|
72
|
+
throw new Error('Access to internal/local addresses is not allowed');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check if hostname is a private IP
|
|
76
|
+
if (isPrivateIP(parsed.hostname)) {
|
|
77
|
+
throw new Error('Access to private IP addresses is not allowed');
|
|
78
|
+
}
|
|
79
|
+
}
|
package/CLAUDE.md
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Project Overview
|
|
6
|
-
|
|
7
|
-
**GenCode** (npm: `gencode`) is an open-source, provider-agnostic AI coding assistant. It brings Claude Code's excellent interactive CLI experience while allowing flexible switching between different LLM providers (OpenAI, Anthropic, Google Gemini).
|
|
8
|
-
|
|
9
|
-
## Build & Run Commands
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
npm install # Install dependencies
|
|
13
|
-
npm run build # Compile TypeScript to dist/
|
|
14
|
-
npm run dev # Watch mode compilation
|
|
15
|
-
npm start # Run CLI directly via tsx
|
|
16
|
-
npm run example # Run examples/basic.ts
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Architecture
|
|
20
|
-
|
|
21
|
-
### Provider Abstraction Layer (`src/providers/`)
|
|
22
|
-
|
|
23
|
-
Unified `LLMProvider` interface abstracts API differences:
|
|
24
|
-
- `complete()` - Non-streaming completion
|
|
25
|
-
- `stream()` - Streaming completion (AsyncGenerator)
|
|
26
|
-
|
|
27
|
-
Each provider (OpenAI, Anthropic, Gemini) translates the unified message format to its native API format and back. The `createProvider()` factory instantiates providers by name.
|
|
28
|
-
|
|
29
|
-
### Tool System (`src/tools/`)
|
|
30
|
-
|
|
31
|
-
Tools are defined with Zod schemas for input validation:
|
|
32
|
-
```typescript
|
|
33
|
-
interface Tool<TInput> {
|
|
34
|
-
name: string;
|
|
35
|
-
description: string;
|
|
36
|
-
parameters: z.ZodSchema<TInput>;
|
|
37
|
-
execute(input: TInput, context: ToolContext): Promise<ToolResult>;
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
`ToolRegistry` manages tools and converts Zod schemas to JSON Schema for LLM consumption via `zodToJsonSchema()`.
|
|
42
|
-
|
|
43
|
-
### Agent Loop (`src/agent/agent.ts`)
|
|
44
|
-
|
|
45
|
-
The `Agent` class implements the core conversation loop:
|
|
46
|
-
1. User message → LLM with tools
|
|
47
|
-
2. If `stopReason === 'tool_use'`: execute tools, append results, loop back
|
|
48
|
-
3. If `stopReason !== 'tool_use'`: done
|
|
49
|
-
|
|
50
|
-
Events are yielded as `AgentEvent` (text, tool_start, tool_result, done, error).
|
|
51
|
-
|
|
52
|
-
### Session Management (`src/session/`)
|
|
53
|
-
|
|
54
|
-
Sessions persist conversation history to `~/.gencode/sessions/` as JSON files. Supports resume, fork, list, and delete operations.
|
|
55
|
-
|
|
56
|
-
## Configuration
|
|
57
|
-
|
|
58
|
-
Provider/model selection priority:
|
|
59
|
-
1. `GENCODE_PROVIDER` / `GENCODE_MODEL` env vars
|
|
60
|
-
2. Auto-detect from available API keys (ANTHROPIC_API_KEY → OPENAI_API_KEY → GOOGLE_API_KEY)
|
|
61
|
-
3. Default: Gemini
|
|
62
|
-
|
|
63
|
-
Proxy: Set `HTTP_PROXY` or `HTTPS_PROXY` for network proxy support.
|
|
64
|
-
|
|
65
|
-
## Key Patterns
|
|
66
|
-
|
|
67
|
-
- All file paths in tools should be resolved relative to `ToolContext.cwd`
|
|
68
|
-
- Tool input validation uses Zod; errors returned as `ToolResult.error`
|
|
69
|
-
- Provider implementations handle message format conversion internally
|
|
70
|
-
- CLI commands start with `/` (e.g., `/sessions`, `/resume`, `/help`)
|