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.
Files changed (149) hide show
  1. package/README.md +8 -90
  2. package/dist/agent/agent.d.ts +1 -1
  3. package/dist/agent/agent.d.ts.map +1 -1
  4. package/dist/agent/agent.js +8 -2
  5. package/dist/agent/agent.js.map +1 -1
  6. package/dist/agent/types.d.ts +9 -1
  7. package/dist/agent/types.d.ts.map +1 -1
  8. package/dist/cli/components/AllModelsSelector.d.ts +11 -0
  9. package/dist/cli/components/AllModelsSelector.d.ts.map +1 -0
  10. package/dist/cli/components/AllModelsSelector.js +153 -0
  11. package/dist/cli/components/AllModelsSelector.js.map +1 -0
  12. package/dist/cli/components/App.d.ts.map +1 -1
  13. package/dist/cli/components/App.js +59 -25
  14. package/dist/cli/components/App.js.map +1 -1
  15. package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
  16. package/dist/cli/components/CommandSuggestions.js +1 -0
  17. package/dist/cli/components/CommandSuggestions.js.map +1 -1
  18. package/dist/cli/components/Messages.d.ts +15 -1
  19. package/dist/cli/components/Messages.d.ts.map +1 -1
  20. package/dist/cli/components/Messages.js +41 -15
  21. package/dist/cli/components/Messages.js.map +1 -1
  22. package/dist/cli/components/ModelSelector.d.ts +7 -7
  23. package/dist/cli/components/ModelSelector.d.ts.map +1 -1
  24. package/dist/cli/components/ModelSelector.js +116 -33
  25. package/dist/cli/components/ModelSelector.js.map +1 -1
  26. package/dist/cli/components/ProviderManager.d.ts +8 -0
  27. package/dist/cli/components/ProviderManager.d.ts.map +1 -0
  28. package/dist/cli/components/ProviderManager.js +280 -0
  29. package/dist/cli/components/ProviderManager.js.map +1 -0
  30. package/dist/cli/components/markdown.d.ts +9 -0
  31. package/dist/cli/components/markdown.d.ts.map +1 -0
  32. package/dist/cli/components/markdown.js +129 -0
  33. package/dist/cli/components/markdown.js.map +1 -0
  34. package/dist/cli/components/theme.d.ts +5 -0
  35. package/dist/cli/components/theme.d.ts.map +1 -1
  36. package/dist/cli/components/theme.js +7 -0
  37. package/dist/cli/components/theme.js.map +1 -1
  38. package/dist/cli/index.js +19 -5
  39. package/dist/cli/index.js.map +1 -1
  40. package/dist/config/index.d.ts +3 -2
  41. package/dist/config/index.d.ts.map +1 -1
  42. package/dist/config/index.js +2 -1
  43. package/dist/config/index.js.map +1 -1
  44. package/dist/config/providers-config.d.ts +28 -0
  45. package/dist/config/providers-config.d.ts.map +1 -0
  46. package/dist/config/providers-config.js +79 -0
  47. package/dist/config/providers-config.js.map +1 -0
  48. package/dist/config/types.d.ts +31 -1
  49. package/dist/config/types.d.ts.map +1 -1
  50. package/dist/config/types.js +1 -0
  51. package/dist/config/types.js.map +1 -1
  52. package/dist/providers/gemini.d.ts.map +1 -1
  53. package/dist/providers/gemini.js +14 -3
  54. package/dist/providers/gemini.js.map +1 -1
  55. package/dist/providers/index.d.ts +5 -3
  56. package/dist/providers/index.d.ts.map +1 -1
  57. package/dist/providers/index.js +13 -1
  58. package/dist/providers/index.js.map +1 -1
  59. package/dist/providers/registry.d.ts +66 -0
  60. package/dist/providers/registry.d.ts.map +1 -0
  61. package/dist/providers/registry.js +158 -0
  62. package/dist/providers/registry.js.map +1 -0
  63. package/dist/providers/search/brave.d.ts +14 -0
  64. package/dist/providers/search/brave.d.ts.map +1 -0
  65. package/dist/providers/search/brave.js +87 -0
  66. package/dist/providers/search/brave.js.map +1 -0
  67. package/dist/providers/search/exa.d.ts +12 -0
  68. package/dist/providers/search/exa.d.ts.map +1 -0
  69. package/dist/providers/search/exa.js +158 -0
  70. package/dist/providers/search/exa.js.map +1 -0
  71. package/dist/providers/search/index.d.ts +31 -0
  72. package/dist/providers/search/index.d.ts.map +1 -0
  73. package/dist/providers/search/index.js +75 -0
  74. package/dist/providers/search/index.js.map +1 -0
  75. package/dist/providers/search/serper.d.ts +14 -0
  76. package/dist/providers/search/serper.d.ts.map +1 -0
  77. package/dist/providers/search/serper.js +87 -0
  78. package/dist/providers/search/serper.js.map +1 -0
  79. package/dist/providers/search/types.d.ts +21 -0
  80. package/dist/providers/search/types.d.ts.map +1 -0
  81. package/dist/providers/search/types.js +5 -0
  82. package/dist/providers/search/types.js.map +1 -0
  83. package/dist/providers/store.d.ts +104 -0
  84. package/dist/providers/store.d.ts.map +1 -0
  85. package/dist/providers/store.js +171 -0
  86. package/dist/providers/store.js.map +1 -0
  87. package/dist/providers/types.d.ts +7 -1
  88. package/dist/providers/types.d.ts.map +1 -1
  89. package/dist/providers/vertex-ai.d.ts +33 -0
  90. package/dist/providers/vertex-ai.d.ts.map +1 -0
  91. package/dist/providers/vertex-ai.js +407 -0
  92. package/dist/providers/vertex-ai.js.map +1 -0
  93. package/dist/tools/builtin/webfetch.d.ts +20 -0
  94. package/dist/tools/builtin/webfetch.d.ts.map +1 -0
  95. package/dist/tools/builtin/webfetch.js +231 -0
  96. package/dist/tools/builtin/webfetch.js.map +1 -0
  97. package/dist/tools/builtin/websearch.d.ts +17 -0
  98. package/dist/tools/builtin/websearch.d.ts.map +1 -0
  99. package/dist/tools/builtin/websearch.js +101 -0
  100. package/dist/tools/builtin/websearch.js.map +1 -0
  101. package/dist/tools/index.d.ts +11 -0
  102. package/dist/tools/index.d.ts.map +1 -1
  103. package/dist/tools/index.js +24 -2
  104. package/dist/tools/index.js.map +1 -1
  105. package/dist/tools/types.d.ts +19 -0
  106. package/dist/tools/types.d.ts.map +1 -1
  107. package/dist/tools/types.js +8 -0
  108. package/dist/tools/types.js.map +1 -1
  109. package/dist/tools/utils/ssrf.d.ts +18 -0
  110. package/dist/tools/utils/ssrf.d.ts.map +1 -0
  111. package/dist/tools/utils/ssrf.js +70 -0
  112. package/dist/tools/utils/ssrf.js.map +1 -0
  113. package/docs/README.md +5 -4
  114. package/docs/proposals/0001-web-fetch-tool.md +32 -2
  115. package/docs/proposals/0002-web-search-tool.md +59 -2
  116. package/docs/proposals/0041-configuration-system.md +556 -0
  117. package/docs/proposals/README.md +3 -2
  118. package/docs/providers.md +220 -0
  119. package/package.json +7 -2
  120. package/src/agent/agent.ts +9 -2
  121. package/src/agent/types.ts +9 -1
  122. package/src/cli/components/App.tsx +72 -23
  123. package/src/cli/components/CommandSuggestions.tsx +1 -0
  124. package/src/cli/components/Messages.tsx +117 -29
  125. package/src/cli/components/ModelSelector.tsx +169 -52
  126. package/src/cli/components/ProviderManager.tsx +534 -0
  127. package/src/cli/components/markdown.ts +157 -0
  128. package/src/cli/components/theme.ts +7 -0
  129. package/src/cli/index.tsx +22 -7
  130. package/src/config/index.ts +3 -2
  131. package/src/config/providers-config.ts +85 -0
  132. package/src/config/types.ts +35 -1
  133. package/src/providers/gemini.ts +20 -4
  134. package/src/providers/index.ts +18 -3
  135. package/src/providers/registry.ts +198 -0
  136. package/src/providers/search/brave.ts +132 -0
  137. package/src/providers/search/exa.ts +217 -0
  138. package/src/providers/search/index.ts +79 -0
  139. package/src/providers/search/serper.ts +133 -0
  140. package/src/providers/search/types.ts +24 -0
  141. package/src/providers/store.ts +216 -0
  142. package/src/providers/types.ts +9 -1
  143. package/src/providers/vertex-ai.ts +594 -0
  144. package/src/tools/builtin/webfetch.ts +264 -0
  145. package/src/tools/builtin/websearch.ts +117 -0
  146. package/src/tools/index.ts +24 -2
  147. package/src/tools/types.ts +20 -0
  148. package/src/tools/utils/ssrf.ts +79 -0
  149. 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(/&nbsp;/g, ' ')
77
+ .replace(/&lt;/g, '<')
78
+ .replace(/&gt;/g, '>')
79
+ .replace(/&amp;/g, '&')
80
+ .replace(/&quot;/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
+ };
@@ -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([readTool, writeTool, editTool, bashTool, globTool, grepTool]);
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 = [readTool, writeTool, editTool, bashTool, globTool, grepTool];
49
+ export const builtinTools = [
50
+ readTool,
51
+ writeTool,
52
+ editTool,
53
+ bashTool,
54
+ globTool,
55
+ grepTool,
56
+ webfetchTool,
57
+ websearchTool,
58
+ ];
@@ -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`)