gencode-ai 0.1.0 → 0.1.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/.gencode/settings.local.json +7 -0
- package/README.md +20 -102
- package/dist/agent/agent.d.ts +43 -2
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +90 -17
- 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 +8 -1
- package/dist/cli/components/App.d.ts.map +1 -1
- package/dist/cli/components/App.js +276 -40
- 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 +3 -0
- package/dist/cli/components/CommandSuggestions.js.map +1 -1
- package/dist/cli/components/Header.d.ts +1 -1
- package/dist/cli/components/Header.d.ts.map +1 -1
- package/dist/cli/components/Header.js +4 -6
- package/dist/cli/components/Header.js.map +1 -1
- package/dist/cli/components/Logo.d.ts +1 -0
- package/dist/cli/components/Logo.d.ts.map +1 -1
- package/dist/cli/components/Logo.js +16 -3
- package/dist/cli/components/Logo.js.map +1 -1
- package/dist/cli/components/Messages.d.ts +17 -3
- package/dist/cli/components/Messages.d.ts.map +1 -1
- package/dist/cli/components/Messages.js +70 -18
- 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/PermissionPrompt.d.ts +60 -0
- package/dist/cli/components/PermissionPrompt.d.ts.map +1 -0
- package/dist/cli/components/PermissionPrompt.js +192 -0
- package/dist/cli/components/PermissionPrompt.js.map +1 -0
- 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/Spinner.d.ts +7 -2
- package/dist/cli/components/Spinner.d.ts.map +1 -1
- package/dist/cli/components/Spinner.js +116 -25
- package/dist/cli/components/Spinner.js.map +1 -1
- package/dist/cli/components/TodoList.d.ts +7 -0
- package/dist/cli/components/TodoList.d.ts.map +1 -0
- package/dist/cli/components/TodoList.js +34 -0
- package/dist/cli/components/TodoList.js.map +1 -0
- package/dist/cli/components/index.d.ts +1 -0
- package/dist/cli/components/index.d.ts.map +1 -1
- package/dist/cli/components/index.js +1 -0
- package/dist/cli/components/index.js.map +1 -1
- 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 +66 -12
- package/dist/cli/index.js.map +1 -1
- package/dist/config/index.d.ts +14 -4
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +19 -3
- package/dist/config/index.js.map +1 -1
- package/dist/config/levels.d.ts +49 -0
- package/dist/config/levels.d.ts.map +1 -0
- package/dist/config/levels.js +222 -0
- package/dist/config/levels.js.map +1 -0
- package/dist/config/loader.d.ts +46 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +153 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/manager.d.ts +115 -15
- package/dist/config/manager.d.ts.map +1 -1
- package/dist/config/manager.js +260 -34
- package/dist/config/manager.js.map +1 -1
- package/dist/config/manager.test.d.ts +5 -0
- package/dist/config/manager.test.d.ts.map +1 -0
- package/dist/config/manager.test.js +192 -0
- package/dist/config/manager.test.js.map +1 -0
- package/dist/config/merger.d.ts +56 -0
- package/dist/config/merger.d.ts.map +1 -0
- package/dist/config/merger.js +177 -0
- package/dist/config/merger.js.map +1 -0
- 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/test-utils.d.ts +24 -0
- package/dist/config/test-utils.d.ts.map +1 -0
- package/dist/config/test-utils.js +55 -0
- package/dist/config/test-utils.js.map +1 -0
- package/dist/config/types.d.ts +108 -9
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +53 -2
- package/dist/config/types.js.map +1 -1
- package/dist/memory/import-resolver.d.ts +46 -0
- package/dist/memory/import-resolver.d.ts.map +1 -0
- package/dist/memory/import-resolver.js +117 -0
- package/dist/memory/import-resolver.js.map +1 -0
- package/dist/memory/index.d.ts +7 -6
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +7 -5
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/init-prompt.d.ts +22 -0
- package/dist/memory/init-prompt.d.ts.map +1 -0
- package/dist/memory/init-prompt.js +103 -0
- package/dist/memory/init-prompt.js.map +1 -0
- package/dist/memory/memory-manager.d.ts +119 -0
- package/dist/memory/memory-manager.d.ts.map +1 -0
- package/dist/memory/memory-manager.js +587 -0
- package/dist/memory/memory-manager.js.map +1 -0
- package/dist/memory/rules-parser.d.ts +38 -0
- package/dist/memory/rules-parser.d.ts.map +1 -0
- package/dist/memory/rules-parser.js +69 -0
- package/dist/memory/rules-parser.js.map +1 -0
- package/dist/memory/test-utils.d.ts +20 -0
- package/dist/memory/test-utils.d.ts.map +1 -0
- package/dist/memory/test-utils.js +44 -0
- package/dist/memory/test-utils.js.map +1 -0
- package/dist/memory/types.d.ts +70 -63
- package/dist/memory/types.d.ts.map +1 -1
- package/dist/memory/types.js +42 -2
- package/dist/memory/types.js.map +1 -1
- package/dist/permissions/audit.d.ts +82 -0
- package/dist/permissions/audit.d.ts.map +1 -0
- package/dist/permissions/audit.js +229 -0
- package/dist/permissions/audit.js.map +1 -0
- package/dist/permissions/index.d.ts +11 -1
- package/dist/permissions/index.d.ts.map +1 -1
- package/dist/permissions/index.js +15 -0
- package/dist/permissions/index.js.map +1 -1
- package/dist/permissions/manager.d.ts +149 -13
- package/dist/permissions/manager.d.ts.map +1 -1
- package/dist/permissions/manager.js +480 -35
- package/dist/permissions/manager.js.map +1 -1
- package/dist/permissions/manager.test.d.ts +5 -0
- package/dist/permissions/manager.test.d.ts.map +1 -0
- package/dist/permissions/manager.test.js +213 -0
- package/dist/permissions/manager.test.js.map +1 -0
- package/dist/permissions/persistence.d.ts +74 -0
- package/dist/permissions/persistence.d.ts.map +1 -0
- package/dist/permissions/persistence.js +248 -0
- package/dist/permissions/persistence.js.map +1 -0
- package/dist/permissions/persistence.test.d.ts +5 -0
- package/dist/permissions/persistence.test.d.ts.map +1 -0
- package/dist/permissions/persistence.test.js +171 -0
- package/dist/permissions/persistence.test.js.map +1 -0
- package/dist/permissions/prompt-matcher.d.ts +64 -0
- package/dist/permissions/prompt-matcher.d.ts.map +1 -0
- package/dist/permissions/prompt-matcher.js +415 -0
- package/dist/permissions/prompt-matcher.js.map +1 -0
- package/dist/permissions/prompt-matcher.test.d.ts +5 -0
- package/dist/permissions/prompt-matcher.test.d.ts.map +1 -0
- package/dist/permissions/prompt-matcher.test.js +107 -0
- package/dist/permissions/prompt-matcher.test.js.map +1 -0
- package/dist/permissions/types.d.ts +157 -0
- package/dist/permissions/types.d.ts.map +1 -1
- package/dist/permissions/types.js +43 -8
- package/dist/permissions/types.js.map +1 -1
- package/dist/prompts/index.d.ts +92 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +241 -0
- package/dist/prompts/index.js.map +1 -0
- 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/bash.d.ts.map +1 -1
- package/dist/tools/builtin/bash.js +2 -1
- package/dist/tools/builtin/bash.js.map +1 -1
- package/dist/tools/builtin/edit.d.ts.map +1 -1
- package/dist/tools/builtin/edit.js +2 -1
- package/dist/tools/builtin/edit.js.map +1 -1
- package/dist/tools/builtin/glob.d.ts.map +1 -1
- package/dist/tools/builtin/glob.js +2 -1
- package/dist/tools/builtin/glob.js.map +1 -1
- package/dist/tools/builtin/grep.d.ts.map +1 -1
- package/dist/tools/builtin/grep.js +2 -1
- package/dist/tools/builtin/grep.js.map +1 -1
- package/dist/tools/builtin/read.d.ts.map +1 -1
- package/dist/tools/builtin/read.js +2 -1
- package/dist/tools/builtin/read.js.map +1 -1
- package/dist/tools/builtin/todowrite.d.ts +15 -0
- package/dist/tools/builtin/todowrite.d.ts.map +1 -0
- package/dist/tools/builtin/todowrite.js +88 -0
- package/dist/tools/builtin/todowrite.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 +228 -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 +87 -0
- package/dist/tools/builtin/websearch.js.map +1 -0
- package/dist/tools/builtin/write.d.ts.map +1 -1
- package/dist/tools/builtin/write.js +2 -1
- package/dist/tools/builtin/write.js.map +1 -1
- package/dist/tools/index.d.ts +18 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +28 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/types.d.ts +41 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js +16 -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/config-system-comparison.md +707 -0
- package/docs/memory-system.md +238 -0
- package/docs/permissions.md +368 -0
- package/docs/proposals/0001-web-fetch-tool.md +32 -2
- package/docs/proposals/0002-web-search-tool.md +59 -2
- package/docs/proposals/0005-todo-system.md +350 -85
- package/docs/proposals/0006-memory-system.md +11 -10
- package/docs/proposals/0012-ask-user-question.md +941 -206
- package/docs/proposals/0023-permission-enhancements.md +61 -2
- package/docs/proposals/0041-configuration-system.md +587 -0
- package/docs/proposals/0042-prompt-optimization.md +866 -0
- package/docs/proposals/README.md +8 -6
- package/docs/providers.md +220 -0
- package/jest.config.js +26 -0
- package/package.json +14 -3
- package/src/agent/agent.ts +120 -18
- package/src/agent/types.ts +9 -1
- package/src/cli/components/App.tsx +369 -47
- package/src/cli/components/CommandSuggestions.tsx +3 -0
- package/src/cli/components/Header.tsx +11 -17
- package/src/cli/components/Logo.tsx +76 -9
- package/src/cli/components/Messages.tsx +146 -38
- package/src/cli/components/ModelSelector.tsx +169 -52
- package/src/cli/components/PermissionPrompt.tsx +388 -0
- package/src/cli/components/ProviderManager.tsx +534 -0
- package/src/cli/components/Spinner.tsx +138 -25
- package/src/cli/components/TodoList.tsx +54 -0
- package/src/cli/components/index.ts +6 -0
- package/src/cli/components/markdown.ts +157 -0
- package/src/cli/components/theme.ts +7 -0
- package/src/cli/index.tsx +76 -13
- package/src/config/index.ts +79 -4
- package/src/config/levels.test.ts +163 -0
- package/src/config/levels.ts +285 -0
- package/src/config/loader.test.ts +120 -0
- package/src/config/loader.ts +178 -0
- package/src/config/manager.test.ts +215 -0
- package/src/config/manager.ts +328 -40
- package/src/config/merger.test.ts +360 -0
- package/src/config/merger.ts +221 -0
- package/src/config/providers-config.ts +85 -0
- package/src/config/test-utils.ts +79 -0
- package/src/config/types.ts +186 -9
- package/src/memory/import-resolver.test.ts +117 -0
- package/src/memory/import-resolver.ts +149 -0
- package/src/memory/index.ts +11 -0
- package/src/memory/init-prompt.ts +113 -0
- package/src/memory/memory-manager.test.ts +198 -0
- package/src/memory/memory-manager.ts +716 -0
- package/src/memory/rules-parser.test.ts +182 -0
- package/src/memory/rules-parser.ts +82 -0
- package/src/memory/test-utils.ts +60 -0
- package/src/memory/types.ts +119 -0
- package/src/permissions/audit.ts +284 -0
- package/src/permissions/index.ts +20 -1
- package/src/permissions/manager.test.ts +260 -0
- package/src/permissions/manager.ts +592 -40
- package/src/permissions/persistence.test.ts +220 -0
- package/src/permissions/persistence.ts +301 -0
- package/src/permissions/prompt-matcher.test.ts +213 -0
- package/src/permissions/prompt-matcher.ts +472 -0
- package/src/permissions/types.ts +236 -8
- package/src/prompts/index.test.ts +279 -0
- package/src/prompts/index.ts +306 -0
- package/src/prompts/system/anthropic.txt +29 -0
- package/src/prompts/system/base.txt +124 -0
- package/src/prompts/system/gemini.txt +35 -0
- package/src/prompts/system/generic.txt +128 -0
- package/src/prompts/system/openai.txt +29 -0
- package/src/prompts/tools/bash.txt +60 -0
- package/src/prompts/tools/edit.txt +29 -0
- package/src/prompts/tools/glob.txt +35 -0
- package/src/prompts/tools/grep.txt +43 -0
- package/src/prompts/tools/read.txt +22 -0
- package/src/prompts/tools/todowrite.txt +71 -0
- package/src/prompts/tools/webfetch.txt +34 -0
- package/src/prompts/tools/websearch.txt +41 -0
- package/src/prompts/tools/write.txt +23 -0
- 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/bash.ts +2 -1
- package/src/tools/builtin/edit.ts +2 -1
- package/src/tools/builtin/glob.ts +2 -1
- package/src/tools/builtin/grep.ts +2 -1
- package/src/tools/builtin/read.ts +2 -1
- package/src/tools/builtin/todowrite.ts +102 -0
- package/src/tools/builtin/webfetch.ts +261 -0
- package/src/tools/builtin/websearch.ts +103 -0
- package/src/tools/builtin/write.ts +2 -1
- package/src/tools/index.ts +28 -2
- package/src/tools/types.ts +32 -0
- package/src/tools/utils/ssrf.ts +79 -0
- package/tsconfig.json +1 -1
- package/CLAUDE.md +0 -70
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TodoWrite Tool - Manage task list for tracking progress
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Tool, ToolResult, TodoItem } from '../types.js';
|
|
6
|
+
import { TodoWriteInputSchema, type TodoWriteInput } from '../types.js';
|
|
7
|
+
import { loadToolDescription } from '../../prompts/index.js';
|
|
8
|
+
|
|
9
|
+
// Global todo state - shared across tool invocations
|
|
10
|
+
let currentTodos: TodoItem[] = [];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get the current todo list
|
|
14
|
+
*/
|
|
15
|
+
export function getTodos(): TodoItem[] {
|
|
16
|
+
return [...currentTodos];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Clear all todos
|
|
21
|
+
*/
|
|
22
|
+
export function clearTodos(): void {
|
|
23
|
+
currentTodos = [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Format todos for display
|
|
28
|
+
*/
|
|
29
|
+
function formatTodos(todos: TodoItem[]): string {
|
|
30
|
+
if (todos.length === 0) {
|
|
31
|
+
return 'Todo list is empty.';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const statusIcons: Record<string, string> = {
|
|
35
|
+
pending: '[ ]',
|
|
36
|
+
in_progress: '[>]',
|
|
37
|
+
completed: '[x]',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const lines = todos.map((todo, index) => {
|
|
41
|
+
const icon = statusIcons[todo.status] || '[ ]';
|
|
42
|
+
return `${index + 1}. ${icon} ${todo.content}`;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return lines.join('\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validate todo list rules
|
|
50
|
+
*/
|
|
51
|
+
function validateTodos(todos: TodoItem[]): string | null {
|
|
52
|
+
const inProgress = todos.filter((t) => t.status === 'in_progress');
|
|
53
|
+
if (inProgress.length > 1) {
|
|
54
|
+
return `Only one task should be in_progress at a time. Found ${inProgress.length} tasks in progress.`;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const todowriteTool: Tool<TodoWriteInput> = {
|
|
60
|
+
name: 'TodoWrite',
|
|
61
|
+
description: loadToolDescription('todowrite'),
|
|
62
|
+
parameters: TodoWriteInputSchema,
|
|
63
|
+
|
|
64
|
+
async execute(input): Promise<ToolResult> {
|
|
65
|
+
try {
|
|
66
|
+
// Validate the todo list
|
|
67
|
+
const validationError = validateTodos(input.todos);
|
|
68
|
+
if (validationError) {
|
|
69
|
+
return {
|
|
70
|
+
success: false,
|
|
71
|
+
output: '',
|
|
72
|
+
error: validationError,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Update the global todo state (auto-generate id if missing)
|
|
77
|
+
currentTodos = input.todos.map((todo, index) => ({
|
|
78
|
+
...todo,
|
|
79
|
+
id: todo.id || `todo-${index + 1}`,
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
// Count statistics
|
|
83
|
+
const pending = currentTodos.filter((t) => t.status === 'pending').length;
|
|
84
|
+
const inProgress = currentTodos.filter((t) => t.status === 'in_progress').length;
|
|
85
|
+
const completed = currentTodos.filter((t) => t.status === 'completed').length;
|
|
86
|
+
|
|
87
|
+
const summary = `Todos updated: ${completed} completed, ${inProgress} in progress, ${pending} pending`;
|
|
88
|
+
const formatted = formatTodos(currentTodos);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
success: true,
|
|
92
|
+
output: `${summary}\n\n${formatted}`,
|
|
93
|
+
};
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
output: '',
|
|
98
|
+
error: `Failed to update todos: ${error instanceof Error ? error.message : String(error)}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
@@ -0,0 +1,261 @@
|
|
|
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
|
+
import { loadToolDescription } from '../../prompts/index.js';
|
|
11
|
+
|
|
12
|
+
// Constants
|
|
13
|
+
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
14
|
+
const DEFAULT_TIMEOUT = 30 * 1000; // 30 seconds
|
|
15
|
+
const MAX_TIMEOUT = 120 * 1000; // 2 minutes
|
|
16
|
+
const MAX_LINE_LENGTH = 2000;
|
|
17
|
+
const MAX_OUTPUT_LENGTH = 50000;
|
|
18
|
+
|
|
19
|
+
// Input schema
|
|
20
|
+
export const WebFetchInputSchema = z.object({
|
|
21
|
+
url: z.string().describe('The URL to fetch content from (http:// or https://)'),
|
|
22
|
+
format: z
|
|
23
|
+
.enum(['text', 'markdown', 'html'])
|
|
24
|
+
.optional()
|
|
25
|
+
.describe('Output format: markdown (default), text, or html'),
|
|
26
|
+
timeout: z.number().optional().describe('Timeout in seconds (default: 30, max: 120)'),
|
|
27
|
+
});
|
|
28
|
+
export type WebFetchInput = z.infer<typeof WebFetchInputSchema>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get Accept header based on requested format
|
|
32
|
+
*/
|
|
33
|
+
function getAcceptHeader(format: string): string {
|
|
34
|
+
switch (format) {
|
|
35
|
+
case 'markdown':
|
|
36
|
+
return 'text/markdown, text/plain, text/html;q=0.9, */*;q=0.1';
|
|
37
|
+
case 'text':
|
|
38
|
+
return 'text/plain, text/html;q=0.8, */*;q=0.1';
|
|
39
|
+
case 'html':
|
|
40
|
+
return 'text/html, application/xhtml+xml, */*;q=0.1';
|
|
41
|
+
default:
|
|
42
|
+
return 'text/html, */*;q=0.1';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Convert HTML to Markdown using Turndown
|
|
48
|
+
*/
|
|
49
|
+
function convertHtmlToMarkdown(html: string): string {
|
|
50
|
+
const turndown = new TurndownService({
|
|
51
|
+
headingStyle: 'atx',
|
|
52
|
+
hr: '---',
|
|
53
|
+
bulletListMarker: '-',
|
|
54
|
+
codeBlockStyle: 'fenced',
|
|
55
|
+
emDelimiter: '*',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Remove script, style, meta, link, noscript tags
|
|
59
|
+
turndown.remove(['script', 'style', 'meta', 'link', 'noscript']);
|
|
60
|
+
|
|
61
|
+
return turndown.turndown(html);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract plain text from HTML
|
|
66
|
+
*/
|
|
67
|
+
function extractTextFromHtml(html: string): string {
|
|
68
|
+
return (
|
|
69
|
+
html
|
|
70
|
+
// Remove script and style content
|
|
71
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
72
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
73
|
+
.replace(/<noscript[^>]*>[\s\S]*?<\/noscript>/gi, '')
|
|
74
|
+
// Remove all tags
|
|
75
|
+
.replace(/<[^>]+>/g, ' ')
|
|
76
|
+
// Decode common HTML entities
|
|
77
|
+
.replace(/ /g, ' ')
|
|
78
|
+
.replace(/</g, '<')
|
|
79
|
+
.replace(/>/g, '>')
|
|
80
|
+
.replace(/&/g, '&')
|
|
81
|
+
.replace(/"/g, '"')
|
|
82
|
+
.replace(/&#(\d+);/g, (_, num) => String.fromCharCode(parseInt(num)))
|
|
83
|
+
// Normalize whitespace
|
|
84
|
+
.replace(/\s+/g, ' ')
|
|
85
|
+
.trim()
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Process content based on content type and requested format
|
|
91
|
+
*/
|
|
92
|
+
function processContent(content: string, contentType: string, format: string): string {
|
|
93
|
+
const isHtml = contentType.includes('text/html') || contentType.includes('application/xhtml');
|
|
94
|
+
|
|
95
|
+
switch (format) {
|
|
96
|
+
case 'markdown':
|
|
97
|
+
if (isHtml) {
|
|
98
|
+
return convertHtmlToMarkdown(content);
|
|
99
|
+
}
|
|
100
|
+
return content;
|
|
101
|
+
|
|
102
|
+
case 'text':
|
|
103
|
+
if (isHtml) {
|
|
104
|
+
return extractTextFromHtml(content);
|
|
105
|
+
}
|
|
106
|
+
return content;
|
|
107
|
+
|
|
108
|
+
case 'html':
|
|
109
|
+
return content;
|
|
110
|
+
|
|
111
|
+
default:
|
|
112
|
+
return content;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Format bytes to human-readable size
|
|
118
|
+
*/
|
|
119
|
+
function formatSize(bytes: number): string {
|
|
120
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
121
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
122
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Truncate output to prevent excessive content
|
|
127
|
+
*/
|
|
128
|
+
function truncateOutput(output: string): string {
|
|
129
|
+
// Truncate long lines
|
|
130
|
+
const lines = output.split('\n').map((line) => {
|
|
131
|
+
if (line.length > MAX_LINE_LENGTH) {
|
|
132
|
+
return line.slice(0, MAX_LINE_LENGTH) + '... (truncated)';
|
|
133
|
+
}
|
|
134
|
+
return line;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
let result = lines.join('\n');
|
|
138
|
+
|
|
139
|
+
// Truncate overall output
|
|
140
|
+
if (result.length > MAX_OUTPUT_LENGTH) {
|
|
141
|
+
result = result.slice(0, MAX_OUTPUT_LENGTH) + '\n\n... (output truncated)';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* WebFetch Tool
|
|
149
|
+
*/
|
|
150
|
+
export const webfetchTool: Tool<WebFetchInput> = {
|
|
151
|
+
name: 'WebFetch',
|
|
152
|
+
description: loadToolDescription('webfetch'),
|
|
153
|
+
parameters: WebFetchInputSchema,
|
|
154
|
+
|
|
155
|
+
async execute(input: WebFetchInput, context: ToolContext): Promise<ToolResult> {
|
|
156
|
+
const startTime = Date.now();
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
// Validate URL (SSRF protection)
|
|
160
|
+
validateUrl(input.url);
|
|
161
|
+
|
|
162
|
+
// Calculate timeout
|
|
163
|
+
const timeoutMs = input.timeout
|
|
164
|
+
? Math.min(input.timeout * 1000, MAX_TIMEOUT)
|
|
165
|
+
: DEFAULT_TIMEOUT;
|
|
166
|
+
|
|
167
|
+
// Create abort controller for timeout
|
|
168
|
+
const controller = new AbortController();
|
|
169
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
170
|
+
|
|
171
|
+
// Combine with context abort signal if present
|
|
172
|
+
const signal = context.abortSignal
|
|
173
|
+
? AbortSignal.any([controller.signal, context.abortSignal])
|
|
174
|
+
: controller.signal;
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
// Fetch with appropriate headers
|
|
178
|
+
const response = await fetch(input.url, {
|
|
179
|
+
signal,
|
|
180
|
+
headers: {
|
|
181
|
+
'User-Agent': 'GenCode/1.0 (+https://github.com/gencode)',
|
|
182
|
+
Accept: getAcceptHeader(input.format ?? 'markdown'),
|
|
183
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
184
|
+
},
|
|
185
|
+
redirect: 'follow',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
clearTimeout(timeoutId);
|
|
189
|
+
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
return {
|
|
192
|
+
success: false,
|
|
193
|
+
output: '',
|
|
194
|
+
error: `HTTP ${response.status}: ${response.statusText}`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Check content length header
|
|
199
|
+
const contentLength = response.headers.get('content-length');
|
|
200
|
+
if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE) {
|
|
201
|
+
return {
|
|
202
|
+
success: false,
|
|
203
|
+
output: '',
|
|
204
|
+
error: `Response too large: ${contentLength} bytes (max: ${MAX_RESPONSE_SIZE})`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Read response body with size limit
|
|
209
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
210
|
+
if (arrayBuffer.byteLength > MAX_RESPONSE_SIZE) {
|
|
211
|
+
return {
|
|
212
|
+
success: false,
|
|
213
|
+
output: '',
|
|
214
|
+
error: `Response too large: ${arrayBuffer.byteLength} bytes (max: ${MAX_RESPONSE_SIZE})`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const content = new TextDecoder().decode(arrayBuffer);
|
|
219
|
+
const contentType = response.headers.get('content-type') || '';
|
|
220
|
+
|
|
221
|
+
// Process content based on format
|
|
222
|
+
let output = processContent(content, contentType, input.format ?? 'markdown');
|
|
223
|
+
|
|
224
|
+
// Truncate long lines and overall output
|
|
225
|
+
output = truncateOutput(output);
|
|
226
|
+
|
|
227
|
+
// Build result with metadata for improved display
|
|
228
|
+
const size = arrayBuffer.byteLength;
|
|
229
|
+
const duration = Date.now() - startTime;
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
success: true,
|
|
233
|
+
output: output,
|
|
234
|
+
metadata: {
|
|
235
|
+
title: `Fetch(${input.url})`,
|
|
236
|
+
subtitle: `Received ${formatSize(size)} (${response.status} ${response.statusText})`,
|
|
237
|
+
size,
|
|
238
|
+
statusCode: response.status,
|
|
239
|
+
contentType: contentType,
|
|
240
|
+
duration,
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
} finally {
|
|
244
|
+
clearTimeout(timeoutId);
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
248
|
+
return {
|
|
249
|
+
success: false,
|
|
250
|
+
output: '',
|
|
251
|
+
error: 'Request timed out',
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
success: false,
|
|
256
|
+
output: '',
|
|
257
|
+
error: `Fetch failed: ${getErrorMessage(error)}`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
import { loadToolDescription } from '../../prompts/index.js';
|
|
14
|
+
|
|
15
|
+
// Constants
|
|
16
|
+
const DEFAULT_NUM_RESULTS = 10;
|
|
17
|
+
|
|
18
|
+
// Input schema
|
|
19
|
+
export const WebSearchInputSchema = z.object({
|
|
20
|
+
query: z
|
|
21
|
+
.string()
|
|
22
|
+
.min(2)
|
|
23
|
+
.describe('The search query (minimum 2 characters)'),
|
|
24
|
+
allowed_domains: z
|
|
25
|
+
.array(z.string())
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('Only include results from these domains'),
|
|
28
|
+
blocked_domains: z
|
|
29
|
+
.array(z.string())
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('Exclude results from these domains'),
|
|
32
|
+
num_results: z
|
|
33
|
+
.number()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe(`Number of results to return (default: ${DEFAULT_NUM_RESULTS})`),
|
|
36
|
+
});
|
|
37
|
+
export type WebSearchInput = z.infer<typeof WebSearchInputSchema>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Format search results as markdown
|
|
41
|
+
*/
|
|
42
|
+
function formatResults(results: SearchResult[], query: string): string {
|
|
43
|
+
if (results.length === 0) {
|
|
44
|
+
return `No results found for "${query}".`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lines: string[] = [`Found ${results.length} results for "${query}":\n`];
|
|
48
|
+
|
|
49
|
+
results.forEach((result, index) => {
|
|
50
|
+
lines.push(`${index + 1}. [${result.title}](${result.url})`);
|
|
51
|
+
if (result.snippet) {
|
|
52
|
+
lines.push(` ${result.snippet}\n`);
|
|
53
|
+
} else {
|
|
54
|
+
lines.push('');
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return lines.join('\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* WebSearch Tool
|
|
63
|
+
*/
|
|
64
|
+
export const websearchTool: Tool<WebSearchInput> = {
|
|
65
|
+
name: 'WebSearch',
|
|
66
|
+
description: loadToolDescription('websearch'),
|
|
67
|
+
parameters: WebSearchInputSchema,
|
|
68
|
+
|
|
69
|
+
async execute(input: WebSearchInput, context: ToolContext): Promise<ToolResult> {
|
|
70
|
+
const startTime = Date.now();
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const provider = createSearchProvider();
|
|
74
|
+
|
|
75
|
+
const results = await provider.search(input.query, {
|
|
76
|
+
numResults: input.num_results ?? DEFAULT_NUM_RESULTS,
|
|
77
|
+
allowedDomains: input.allowed_domains,
|
|
78
|
+
blockedDomains: input.blocked_domains,
|
|
79
|
+
abortSignal: context.abortSignal,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const output = formatResults(results, input.query);
|
|
83
|
+
const duration = Date.now() - startTime;
|
|
84
|
+
const providerName = getCurrentSearchProviderName();
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
output,
|
|
89
|
+
metadata: {
|
|
90
|
+
title: `Search("${input.query}")`,
|
|
91
|
+
subtitle: `Found ${results.length} results via ${providerName}`,
|
|
92
|
+
duration,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
output: '',
|
|
99
|
+
error: `Search failed: ${getErrorMessage(error)}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|
|
@@ -6,10 +6,11 @@ import * as fs from 'fs/promises';
|
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import type { Tool, ToolResult } from '../types.js';
|
|
8
8
|
import { WriteInputSchema, type WriteInput, resolvePath, getErrorMessage } from '../types.js';
|
|
9
|
+
import { loadToolDescription } from '../../prompts/index.js';
|
|
9
10
|
|
|
10
11
|
export const writeTool: Tool<WriteInput> = {
|
|
11
12
|
name: 'Write',
|
|
12
|
-
description: '
|
|
13
|
+
description: loadToolDescription('write'),
|
|
13
14
|
parameters: WriteInputSchema,
|
|
14
15
|
|
|
15
16
|
async execute(input, context): Promise<ToolResult> {
|
package/src/tools/index.ts
CHANGED
|
@@ -12,6 +12,9 @@ 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';
|
|
17
|
+
export { todowriteTool, getTodos, clearTodos } from './builtin/todowrite.js';
|
|
15
18
|
|
|
16
19
|
import { ToolRegistry } from './registry.js';
|
|
17
20
|
import { readTool } from './builtin/read.js';
|
|
@@ -20,17 +23,40 @@ import { editTool } from './builtin/edit.js';
|
|
|
20
23
|
import { bashTool } from './builtin/bash.js';
|
|
21
24
|
import { globTool } from './builtin/glob.js';
|
|
22
25
|
import { grepTool } from './builtin/grep.js';
|
|
26
|
+
import { webfetchTool } from './builtin/webfetch.js';
|
|
27
|
+
import { websearchTool } from './builtin/websearch.js';
|
|
28
|
+
import { todowriteTool } from './builtin/todowrite.js';
|
|
23
29
|
|
|
24
30
|
/**
|
|
25
31
|
* Create a registry with all built-in tools
|
|
26
32
|
*/
|
|
27
33
|
export function createDefaultRegistry(): ToolRegistry {
|
|
28
34
|
const registry = new ToolRegistry();
|
|
29
|
-
registry.registerAll([
|
|
35
|
+
registry.registerAll([
|
|
36
|
+
readTool,
|
|
37
|
+
writeTool,
|
|
38
|
+
editTool,
|
|
39
|
+
bashTool,
|
|
40
|
+
globTool,
|
|
41
|
+
grepTool,
|
|
42
|
+
webfetchTool,
|
|
43
|
+
websearchTool,
|
|
44
|
+
todowriteTool,
|
|
45
|
+
]);
|
|
30
46
|
return registry;
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
/**
|
|
34
50
|
* All built-in tools
|
|
35
51
|
*/
|
|
36
|
-
export const builtinTools = [
|
|
52
|
+
export const builtinTools = [
|
|
53
|
+
readTool,
|
|
54
|
+
writeTool,
|
|
55
|
+
editTool,
|
|
56
|
+
bashTool,
|
|
57
|
+
globTool,
|
|
58
|
+
grepTool,
|
|
59
|
+
webfetchTool,
|
|
60
|
+
websearchTool,
|
|
61
|
+
todowriteTool,
|
|
62
|
+
];
|
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,28 @@ 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
|
+
|
|
111
|
+
export const TodoItemSchema = z.object({
|
|
112
|
+
content: z.string().min(1).describe('The task description'),
|
|
113
|
+
status: z.enum(['pending', 'in_progress', 'completed']).describe('Current status of the task'),
|
|
114
|
+
id: z.string().optional().describe('Unique task identifier'),
|
|
115
|
+
});
|
|
116
|
+
export type TodoItem = z.infer<typeof TodoItemSchema>;
|
|
117
|
+
|
|
118
|
+
export const TodoWriteInputSchema = z.object({
|
|
119
|
+
todos: z.array(TodoItemSchema).describe('The complete todo list to write'),
|
|
120
|
+
});
|
|
121
|
+
export type TodoWriteInput = z.infer<typeof TodoWriteInputSchema>;
|
|
122
|
+
|
|
91
123
|
// ============================================================================
|
|
92
124
|
// JSON Schema Conversion
|
|
93
125
|
// ============================================================================
|
|
@@ -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/tsconfig.json
CHANGED