grokcodecli 0.1.0

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 (99) hide show
  1. package/.claude/settings.local.json +32 -0
  2. package/README.md +1464 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +61 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/loader.d.ts +34 -0
  8. package/dist/commands/loader.d.ts.map +1 -0
  9. package/dist/commands/loader.js +192 -0
  10. package/dist/commands/loader.js.map +1 -0
  11. package/dist/config/manager.d.ts +21 -0
  12. package/dist/config/manager.d.ts.map +1 -0
  13. package/dist/config/manager.js +203 -0
  14. package/dist/config/manager.js.map +1 -0
  15. package/dist/conversation/chat.d.ts +50 -0
  16. package/dist/conversation/chat.d.ts.map +1 -0
  17. package/dist/conversation/chat.js +1145 -0
  18. package/dist/conversation/chat.js.map +1 -0
  19. package/dist/conversation/history.d.ts +24 -0
  20. package/dist/conversation/history.d.ts.map +1 -0
  21. package/dist/conversation/history.js +103 -0
  22. package/dist/conversation/history.js.map +1 -0
  23. package/dist/grok/client.d.ts +86 -0
  24. package/dist/grok/client.d.ts.map +1 -0
  25. package/dist/grok/client.js +106 -0
  26. package/dist/grok/client.js.map +1 -0
  27. package/dist/index.d.ts +7 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +8 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/permissions/manager.d.ts +26 -0
  32. package/dist/permissions/manager.d.ts.map +1 -0
  33. package/dist/permissions/manager.js +170 -0
  34. package/dist/permissions/manager.js.map +1 -0
  35. package/dist/tools/bash.d.ts +8 -0
  36. package/dist/tools/bash.d.ts.map +1 -0
  37. package/dist/tools/bash.js +102 -0
  38. package/dist/tools/bash.js.map +1 -0
  39. package/dist/tools/edit.d.ts +9 -0
  40. package/dist/tools/edit.d.ts.map +1 -0
  41. package/dist/tools/edit.js +61 -0
  42. package/dist/tools/edit.js.map +1 -0
  43. package/dist/tools/glob.d.ts +7 -0
  44. package/dist/tools/glob.d.ts.map +1 -0
  45. package/dist/tools/glob.js +38 -0
  46. package/dist/tools/glob.js.map +1 -0
  47. package/dist/tools/grep.d.ts +8 -0
  48. package/dist/tools/grep.d.ts.map +1 -0
  49. package/dist/tools/grep.js +78 -0
  50. package/dist/tools/grep.js.map +1 -0
  51. package/dist/tools/read.d.ts +8 -0
  52. package/dist/tools/read.d.ts.map +1 -0
  53. package/dist/tools/read.js +96 -0
  54. package/dist/tools/read.js.map +1 -0
  55. package/dist/tools/registry.d.ts +42 -0
  56. package/dist/tools/registry.d.ts.map +1 -0
  57. package/dist/tools/registry.js +230 -0
  58. package/dist/tools/registry.js.map +1 -0
  59. package/dist/tools/webfetch.d.ts +10 -0
  60. package/dist/tools/webfetch.d.ts.map +1 -0
  61. package/dist/tools/webfetch.js +108 -0
  62. package/dist/tools/webfetch.js.map +1 -0
  63. package/dist/tools/websearch.d.ts +7 -0
  64. package/dist/tools/websearch.d.ts.map +1 -0
  65. package/dist/tools/websearch.js +180 -0
  66. package/dist/tools/websearch.js.map +1 -0
  67. package/dist/tools/write.d.ts +7 -0
  68. package/dist/tools/write.d.ts.map +1 -0
  69. package/dist/tools/write.js +80 -0
  70. package/dist/tools/write.js.map +1 -0
  71. package/dist/utils/security.d.ts +36 -0
  72. package/dist/utils/security.d.ts.map +1 -0
  73. package/dist/utils/security.js +227 -0
  74. package/dist/utils/security.js.map +1 -0
  75. package/dist/utils/ui.d.ts +49 -0
  76. package/dist/utils/ui.d.ts.map +1 -0
  77. package/dist/utils/ui.js +302 -0
  78. package/dist/utils/ui.js.map +1 -0
  79. package/package.json +45 -0
  80. package/src/cli.ts +68 -0
  81. package/src/commands/loader.ts +244 -0
  82. package/src/config/manager.ts +239 -0
  83. package/src/conversation/chat.ts +1294 -0
  84. package/src/conversation/history.ts +131 -0
  85. package/src/grok/client.ts +192 -0
  86. package/src/index.ts +8 -0
  87. package/src/permissions/manager.ts +208 -0
  88. package/src/tools/bash.ts +119 -0
  89. package/src/tools/edit.ts +73 -0
  90. package/src/tools/glob.ts +49 -0
  91. package/src/tools/grep.ts +96 -0
  92. package/src/tools/read.ts +116 -0
  93. package/src/tools/registry.ts +248 -0
  94. package/src/tools/webfetch.ts +127 -0
  95. package/src/tools/websearch.ts +219 -0
  96. package/src/tools/write.ts +94 -0
  97. package/src/utils/security.ts +259 -0
  98. package/src/utils/ui.ts +382 -0
  99. package/tsconfig.json +22 -0
@@ -0,0 +1,73 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { ToolResult } from './registry.js';
4
+
5
+ export interface EditToolParams {
6
+ file_path: string;
7
+ old_string: string;
8
+ new_string: string;
9
+ replace_all?: boolean;
10
+ }
11
+
12
+ export async function editTool(params: EditToolParams): Promise<ToolResult> {
13
+ try {
14
+ const filePath = path.resolve(params.file_path);
15
+
16
+ // Read existing content
17
+ let content: string;
18
+ try {
19
+ content = await fs.readFile(filePath, 'utf-8');
20
+ } catch (error) {
21
+ const err = error as NodeJS.ErrnoException;
22
+ if (err.code === 'ENOENT') {
23
+ return { success: false, output: '', error: `File not found: ${params.file_path}` };
24
+ }
25
+ throw error;
26
+ }
27
+
28
+ // Check if old_string exists
29
+ if (!content.includes(params.old_string)) {
30
+ return {
31
+ success: false,
32
+ output: '',
33
+ error: `String not found in file. Make sure the old_string matches exactly including whitespace.`,
34
+ };
35
+ }
36
+
37
+ // Check for uniqueness if not replace_all
38
+ if (!params.replace_all) {
39
+ const occurrences = content.split(params.old_string).length - 1;
40
+ if (occurrences > 1) {
41
+ return {
42
+ success: false,
43
+ output: '',
44
+ error: `Found ${occurrences} occurrences of the string. Use replace_all: true to replace all, or provide a more specific string.`,
45
+ };
46
+ }
47
+ }
48
+
49
+ // Perform replacement
50
+ let newContent: string;
51
+ let replacements: number;
52
+
53
+ if (params.replace_all) {
54
+ const parts = content.split(params.old_string);
55
+ replacements = parts.length - 1;
56
+ newContent = parts.join(params.new_string);
57
+ } else {
58
+ newContent = content.replace(params.old_string, params.new_string);
59
+ replacements = 1;
60
+ }
61
+
62
+ // Write back
63
+ await fs.writeFile(filePath, newContent, 'utf-8');
64
+
65
+ return {
66
+ success: true,
67
+ output: `File edited successfully: ${filePath} (${replacements} replacement${replacements > 1 ? 's' : ''})`,
68
+ };
69
+ } catch (error) {
70
+ const err = error as Error;
71
+ return { success: false, output: '', error: `Error editing file: ${err.message}` };
72
+ }
73
+ }
@@ -0,0 +1,49 @@
1
+ import { glob } from 'glob';
2
+ import * as path from 'path';
3
+ import { ToolResult } from './registry.js';
4
+
5
+ export interface GlobToolParams {
6
+ pattern: string;
7
+ path?: string;
8
+ }
9
+
10
+ export async function globTool(params: GlobToolParams): Promise<ToolResult> {
11
+ try {
12
+ const cwd = params.path ? path.resolve(params.path) : process.cwd();
13
+
14
+ const matches = await glob(params.pattern, {
15
+ cwd,
16
+ nodir: true,
17
+ absolute: false,
18
+ ignore: ['node_modules/**', '.git/**', 'dist/**', 'build/**'],
19
+ });
20
+
21
+ if (matches.length === 0) {
22
+ return {
23
+ success: true,
24
+ output: 'No files found matching the pattern.',
25
+ };
26
+ }
27
+
28
+ // Sort by path
29
+ matches.sort();
30
+
31
+ // Limit output to prevent overwhelming responses
32
+ const maxResults = 100;
33
+ const limited = matches.slice(0, maxResults);
34
+ const remaining = matches.length - maxResults;
35
+
36
+ let output = limited.join('\n');
37
+ if (remaining > 0) {
38
+ output += `\n\n... and ${remaining} more files`;
39
+ }
40
+
41
+ return {
42
+ success: true,
43
+ output: `Found ${matches.length} file(s):\n${output}`,
44
+ };
45
+ } catch (error) {
46
+ const err = error as Error;
47
+ return { success: false, output: '', error: `Glob error: ${err.message}` };
48
+ }
49
+ }
@@ -0,0 +1,96 @@
1
+ import { spawn } from 'child_process';
2
+ import { ToolResult } from './registry.js';
3
+
4
+ export interface GrepToolParams {
5
+ pattern: string;
6
+ path?: string;
7
+ include?: string;
8
+ }
9
+
10
+ export async function grepTool(params: GrepToolParams): Promise<ToolResult> {
11
+ return new Promise((resolve) => {
12
+ const args = [
13
+ '--color=never',
14
+ '-r',
15
+ '-n',
16
+ '-H',
17
+ '--include=*.{ts,tsx,js,jsx,json,md,py,go,rs,java,kt,swift,c,cpp,h,hpp,css,scss,html,xml,yaml,yml,toml}',
18
+ ];
19
+
20
+ if (params.include) {
21
+ args.push(`--include=${params.include}`);
22
+ }
23
+
24
+ // Exclude common directories
25
+ args.push('--exclude-dir=node_modules');
26
+ args.push('--exclude-dir=.git');
27
+ args.push('--exclude-dir=dist');
28
+ args.push('--exclude-dir=build');
29
+ args.push('--exclude-dir=.next');
30
+
31
+ args.push('-E'); // Extended regex
32
+ args.push(params.pattern);
33
+ args.push(params.path || '.');
34
+
35
+ const child = spawn('grep', args, {
36
+ cwd: process.cwd(),
37
+ });
38
+
39
+ let stdout = '';
40
+ let stderr = '';
41
+
42
+ child.stdout.on('data', (data) => {
43
+ stdout += data.toString();
44
+ });
45
+
46
+ child.stderr.on('data', (data) => {
47
+ stderr += data.toString();
48
+ });
49
+
50
+ child.on('close', (code) => {
51
+ // grep returns 1 if no matches, which is not an error
52
+ if (code === 1 && !stderr) {
53
+ resolve({
54
+ success: true,
55
+ output: 'No matches found.',
56
+ });
57
+ return;
58
+ }
59
+
60
+ if (code !== 0 && code !== 1) {
61
+ resolve({
62
+ success: false,
63
+ output: '',
64
+ error: stderr || `grep exited with code ${code}`,
65
+ });
66
+ return;
67
+ }
68
+
69
+ // Limit output
70
+ const lines = stdout.trim().split('\n').filter(Boolean);
71
+ const maxResults = 50;
72
+
73
+ if (lines.length > maxResults) {
74
+ const output = lines.slice(0, maxResults).join('\n');
75
+ resolve({
76
+ success: true,
77
+ output: `Found ${lines.length} matches (showing first ${maxResults}):\n${output}`,
78
+ });
79
+ } else {
80
+ resolve({
81
+ success: true,
82
+ output: lines.length > 0 ? `Found ${lines.length} match(es):\n${stdout.trim()}` : 'No matches found.',
83
+ });
84
+ }
85
+ });
86
+
87
+ child.on('error', (error) => {
88
+ // Fall back to ripgrep if grep is not available
89
+ resolve({
90
+ success: false,
91
+ output: '',
92
+ error: `grep error: ${error.message}`,
93
+ });
94
+ });
95
+ });
96
+ }
@@ -0,0 +1,116 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { ToolResult } from './registry.js';
4
+ import { validatePath } from '../utils/security.js';
5
+ import chalk from 'chalk';
6
+
7
+ export interface ReadToolParams {
8
+ file_path: string;
9
+ offset?: number;
10
+ limit?: number;
11
+ }
12
+
13
+ // Maximum file size to read (10MB)
14
+ const MAX_FILE_SIZE = 10 * 1024 * 1024;
15
+
16
+ // Maximum lines to return
17
+ const MAX_LINES = 2000;
18
+
19
+ export async function readTool(params: ReadToolParams): Promise<ToolResult> {
20
+ try {
21
+ const filePath = path.resolve(params.file_path);
22
+
23
+ // Security validation
24
+ const security = validatePath(filePath);
25
+ if (!security.allowed) {
26
+ return {
27
+ success: false,
28
+ output: '',
29
+ error: `Security: ${security.reason}`,
30
+ };
31
+ }
32
+
33
+ // Check file stats first
34
+ let stats;
35
+ try {
36
+ stats = await fs.stat(filePath);
37
+ } catch (error) {
38
+ const err = error as NodeJS.ErrnoException;
39
+ if (err.code === 'ENOENT') {
40
+ return { success: false, output: '', error: `File not found: ${params.file_path}` };
41
+ }
42
+ throw error;
43
+ }
44
+
45
+ if (stats.isDirectory()) {
46
+ return { success: false, output: '', error: `Path is a directory: ${params.file_path}` };
47
+ }
48
+
49
+ if (stats.size > MAX_FILE_SIZE) {
50
+ return {
51
+ success: false,
52
+ output: '',
53
+ error: `File too large (${(stats.size / 1024 / 1024).toFixed(1)}MB). Maximum is ${MAX_FILE_SIZE / 1024 / 1024}MB. Use offset/limit to read portions.`,
54
+ };
55
+ }
56
+
57
+ // Read the file
58
+ const content = await fs.readFile(filePath, 'utf-8');
59
+
60
+ // Handle binary files
61
+ if (content.includes('\0')) {
62
+ return {
63
+ success: false,
64
+ output: '',
65
+ error: 'File appears to be binary. Cannot display binary content.',
66
+ };
67
+ }
68
+
69
+ const lines = content.split('\n');
70
+ const totalLines = lines.length;
71
+
72
+ const offset = Math.max(0, (params.offset ?? 1) - 1);
73
+ const limit = Math.min(params.limit ?? MAX_LINES, MAX_LINES);
74
+
75
+ const selectedLines = lines.slice(offset, offset + limit);
76
+
77
+ // Format with line numbers like cat -n
78
+ const padding = String(totalLines).length;
79
+ const output = selectedLines
80
+ .map((line, i) => {
81
+ const lineNum = offset + i + 1;
82
+ // Truncate very long lines
83
+ const displayLine = line.length > 2000 ? line.slice(0, 2000) + '...' : line;
84
+ return `${String(lineNum).padStart(padding)}${chalk.gray('│')} ${displayLine}`;
85
+ })
86
+ .join('\n');
87
+
88
+ // Add metadata
89
+ let header = '';
90
+ if (offset > 0 || selectedLines.length < totalLines) {
91
+ header = chalk.gray(`Showing lines ${offset + 1}-${offset + selectedLines.length} of ${totalLines}\n`);
92
+ }
93
+
94
+ // Security warning if applicable
95
+ if (security.severity === 'medium') {
96
+ header = chalk.yellow(`⚠️ ${security.suggestion}\n`) + header;
97
+ }
98
+
99
+ return {
100
+ success: true,
101
+ output: header + (output || '(empty file)'),
102
+ };
103
+ } catch (error) {
104
+ const err = error as NodeJS.ErrnoException;
105
+ if (err.code === 'ENOENT') {
106
+ return { success: false, output: '', error: `File not found: ${params.file_path}` };
107
+ }
108
+ if (err.code === 'EACCES') {
109
+ return { success: false, output: '', error: `Permission denied: ${params.file_path}` };
110
+ }
111
+ if (err.code === 'EISDIR') {
112
+ return { success: false, output: '', error: `Path is a directory: ${params.file_path}` };
113
+ }
114
+ return { success: false, output: '', error: `Error reading file: ${err.message}` };
115
+ }
116
+ }
@@ -0,0 +1,248 @@
1
+ import { Tool } from '../grok/client.js';
2
+ import { readTool, ReadToolParams } from './read.js';
3
+ import { writeTool, WriteToolParams } from './write.js';
4
+ import { editTool, EditToolParams } from './edit.js';
5
+ import { bashTool, BashToolParams } from './bash.js';
6
+ import { globTool, GlobToolParams } from './glob.js';
7
+ import { grepTool, GrepToolParams } from './grep.js';
8
+ import { webFetchTool, WebFetchToolParams } from './webfetch.js';
9
+ import { webSearchTool, WebSearchToolParams } from './websearch.js';
10
+
11
+ export type ToolParams =
12
+ | { name: 'Read'; params: ReadToolParams }
13
+ | { name: 'Write'; params: WriteToolParams }
14
+ | { name: 'Edit'; params: EditToolParams }
15
+ | { name: 'Bash'; params: BashToolParams }
16
+ | { name: 'Glob'; params: GlobToolParams }
17
+ | { name: 'Grep'; params: GrepToolParams }
18
+ | { name: 'WebFetch'; params: WebFetchToolParams }
19
+ | { name: 'WebSearch'; params: WebSearchToolParams };
20
+
21
+ export interface ToolResult {
22
+ success: boolean;
23
+ output: string;
24
+ error?: string;
25
+ }
26
+
27
+ export const allTools: Tool[] = [
28
+ {
29
+ type: 'function',
30
+ function: {
31
+ name: 'Read',
32
+ description: 'Read the contents of a file. Returns the file content with line numbers.',
33
+ parameters: {
34
+ type: 'object',
35
+ properties: {
36
+ file_path: {
37
+ type: 'string',
38
+ description: 'The absolute or relative path to the file to read',
39
+ },
40
+ offset: {
41
+ type: 'number',
42
+ description: 'Line number to start reading from (1-based)',
43
+ },
44
+ limit: {
45
+ type: 'number',
46
+ description: 'Maximum number of lines to read',
47
+ },
48
+ },
49
+ required: ['file_path'],
50
+ },
51
+ },
52
+ },
53
+ {
54
+ type: 'function',
55
+ function: {
56
+ name: 'Write',
57
+ description: 'Write content to a file. Creates the file if it doesn\'t exist, overwrites if it does.',
58
+ parameters: {
59
+ type: 'object',
60
+ properties: {
61
+ file_path: {
62
+ type: 'string',
63
+ description: 'The absolute or relative path to the file to write',
64
+ },
65
+ content: {
66
+ type: 'string',
67
+ description: 'The content to write to the file',
68
+ },
69
+ },
70
+ required: ['file_path', 'content'],
71
+ },
72
+ },
73
+ },
74
+ {
75
+ type: 'function',
76
+ function: {
77
+ name: 'Edit',
78
+ description: 'Edit a file by replacing a specific string with a new string.',
79
+ parameters: {
80
+ type: 'object',
81
+ properties: {
82
+ file_path: {
83
+ type: 'string',
84
+ description: 'The absolute or relative path to the file to edit',
85
+ },
86
+ old_string: {
87
+ type: 'string',
88
+ description: 'The exact string to find and replace',
89
+ },
90
+ new_string: {
91
+ type: 'string',
92
+ description: 'The string to replace it with',
93
+ },
94
+ replace_all: {
95
+ type: 'boolean',
96
+ description: 'Replace all occurrences (default: false)',
97
+ },
98
+ },
99
+ required: ['file_path', 'old_string', 'new_string'],
100
+ },
101
+ },
102
+ },
103
+ {
104
+ type: 'function',
105
+ function: {
106
+ name: 'Bash',
107
+ description: 'Execute a bash command. Use for terminal operations, git commands, npm, etc.',
108
+ parameters: {
109
+ type: 'object',
110
+ properties: {
111
+ command: {
112
+ type: 'string',
113
+ description: 'The bash command to execute',
114
+ },
115
+ timeout: {
116
+ type: 'number',
117
+ description: 'Timeout in milliseconds (default: 120000)',
118
+ },
119
+ },
120
+ required: ['command'],
121
+ },
122
+ },
123
+ },
124
+ {
125
+ type: 'function',
126
+ function: {
127
+ name: 'Glob',
128
+ description: 'Find files matching a glob pattern.',
129
+ parameters: {
130
+ type: 'object',
131
+ properties: {
132
+ pattern: {
133
+ type: 'string',
134
+ description: 'The glob pattern to match (e.g., "**/*.ts", "src/**/*.js")',
135
+ },
136
+ path: {
137
+ type: 'string',
138
+ description: 'The directory to search in (default: current directory)',
139
+ },
140
+ },
141
+ required: ['pattern'],
142
+ },
143
+ },
144
+ },
145
+ {
146
+ type: 'function',
147
+ function: {
148
+ name: 'Grep',
149
+ description: 'Search for a pattern in files using regex.',
150
+ parameters: {
151
+ type: 'object',
152
+ properties: {
153
+ pattern: {
154
+ type: 'string',
155
+ description: 'The regex pattern to search for',
156
+ },
157
+ path: {
158
+ type: 'string',
159
+ description: 'The file or directory to search in',
160
+ },
161
+ include: {
162
+ type: 'string',
163
+ description: 'Glob pattern for files to include (e.g., "*.ts")',
164
+ },
165
+ },
166
+ required: ['pattern'],
167
+ },
168
+ },
169
+ },
170
+ {
171
+ type: 'function',
172
+ function: {
173
+ name: 'WebFetch',
174
+ description: 'Fetch content from a URL. Returns the response body as text. HTML is converted to readable text.',
175
+ parameters: {
176
+ type: 'object',
177
+ properties: {
178
+ url: {
179
+ type: 'string',
180
+ description: 'The URL to fetch',
181
+ },
182
+ method: {
183
+ type: 'string',
184
+ enum: ['GET', 'POST', 'PUT', 'DELETE'],
185
+ description: 'HTTP method (default: GET)',
186
+ },
187
+ headers: {
188
+ type: 'object',
189
+ description: 'Additional HTTP headers',
190
+ },
191
+ body: {
192
+ type: 'string',
193
+ description: 'Request body for POST/PUT requests',
194
+ },
195
+ timeout: {
196
+ type: 'number',
197
+ description: 'Timeout in milliseconds (default: 30000)',
198
+ },
199
+ },
200
+ required: ['url'],
201
+ },
202
+ },
203
+ },
204
+ {
205
+ type: 'function',
206
+ function: {
207
+ name: 'WebSearch',
208
+ description: 'Search the web for information. Returns search results with titles, URLs, and snippets. Use this to find current information, documentation, tutorials, or any web content.',
209
+ parameters: {
210
+ type: 'object',
211
+ properties: {
212
+ query: {
213
+ type: 'string',
214
+ description: 'The search query',
215
+ },
216
+ num_results: {
217
+ type: 'number',
218
+ description: 'Number of results to return (default: 10, max: 20)',
219
+ },
220
+ },
221
+ required: ['query'],
222
+ },
223
+ },
224
+ },
225
+ ];
226
+
227
+ export async function executeTool(name: string, params: Record<string, unknown>): Promise<ToolResult> {
228
+ switch (name) {
229
+ case 'Read':
230
+ return readTool(params as unknown as ReadToolParams);
231
+ case 'Write':
232
+ return writeTool(params as unknown as WriteToolParams);
233
+ case 'Edit':
234
+ return editTool(params as unknown as EditToolParams);
235
+ case 'Bash':
236
+ return bashTool(params as unknown as BashToolParams);
237
+ case 'Glob':
238
+ return globTool(params as unknown as GlobToolParams);
239
+ case 'Grep':
240
+ return grepTool(params as unknown as GrepToolParams);
241
+ case 'WebFetch':
242
+ return webFetchTool(params as unknown as WebFetchToolParams);
243
+ case 'WebSearch':
244
+ return webSearchTool(params as unknown as WebSearchToolParams);
245
+ default:
246
+ return { success: false, output: '', error: `Unknown tool: ${name}` };
247
+ }
248
+ }
@@ -0,0 +1,127 @@
1
+ import { ToolResult } from './registry.js';
2
+
3
+ export interface WebFetchToolParams {
4
+ url: string;
5
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
6
+ headers?: Record<string, string>;
7
+ body?: string;
8
+ timeout?: number;
9
+ }
10
+
11
+ // Simple HTML to text converter
12
+ function htmlToText(html: string): string {
13
+ return html
14
+ // Remove script and style tags with content
15
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
16
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
17
+ // Convert block elements to newlines
18
+ .replace(/<\/(p|div|h[1-6]|li|tr|br)>/gi, '\n')
19
+ .replace(/<(br|hr)\s*\/?>/gi, '\n')
20
+ // Remove remaining tags
21
+ .replace(/<[^>]+>/g, '')
22
+ // Decode common HTML entities
23
+ .replace(/&nbsp;/g, ' ')
24
+ .replace(/&amp;/g, '&')
25
+ .replace(/&lt;/g, '<')
26
+ .replace(/&gt;/g, '>')
27
+ .replace(/&quot;/g, '"')
28
+ .replace(/&#39;/g, "'")
29
+ .replace(/&apos;/g, "'")
30
+ // Clean up whitespace
31
+ .replace(/\n\s*\n/g, '\n\n')
32
+ .replace(/[ \t]+/g, ' ')
33
+ .trim();
34
+ }
35
+
36
+ export async function webFetchTool(params: WebFetchToolParams): Promise<ToolResult> {
37
+ const { url, method = 'GET', headers = {}, body, timeout = 30000 } = params;
38
+
39
+ // Validate URL
40
+ let parsedUrl: URL;
41
+ try {
42
+ parsedUrl = new URL(url);
43
+ } catch {
44
+ return {
45
+ success: false,
46
+ output: '',
47
+ error: `Invalid URL: ${url}`,
48
+ };
49
+ }
50
+
51
+ // Only allow http/https
52
+ if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
53
+ return {
54
+ success: false,
55
+ output: '',
56
+ error: `Unsupported protocol: ${parsedUrl.protocol}`,
57
+ };
58
+ }
59
+
60
+ try {
61
+ const controller = new AbortController();
62
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
63
+
64
+ const response = await fetch(url, {
65
+ method,
66
+ headers: {
67
+ 'User-Agent': 'GrokCodeCLI/1.0',
68
+ ...headers,
69
+ },
70
+ body: body && method !== 'GET' ? body : undefined,
71
+ signal: controller.signal,
72
+ });
73
+
74
+ clearTimeout(timeoutId);
75
+
76
+ const contentType = response.headers.get('content-type') || '';
77
+ let content: string;
78
+
79
+ if (contentType.includes('application/json')) {
80
+ const json = await response.json();
81
+ content = JSON.stringify(json, null, 2);
82
+ } else if (contentType.includes('text/html')) {
83
+ const html = await response.text();
84
+ content = htmlToText(html);
85
+ } else {
86
+ content = await response.text();
87
+ }
88
+
89
+ // Truncate very long responses
90
+ const maxLength = 50000;
91
+ if (content.length > maxLength) {
92
+ content = content.slice(0, maxLength) + '\n\n... (truncated)';
93
+ }
94
+
95
+ const statusInfo = `Status: ${response.status} ${response.statusText}`;
96
+ const headerInfo = `Content-Type: ${contentType}`;
97
+
98
+ if (!response.ok) {
99
+ return {
100
+ success: false,
101
+ output: content,
102
+ error: `HTTP ${response.status}: ${response.statusText}`,
103
+ };
104
+ }
105
+
106
+ return {
107
+ success: true,
108
+ output: `${statusInfo}\n${headerInfo}\n\n${content}`,
109
+ };
110
+ } catch (error) {
111
+ const err = error as Error;
112
+
113
+ if (err.name === 'AbortError') {
114
+ return {
115
+ success: false,
116
+ output: '',
117
+ error: `Request timed out after ${timeout}ms`,
118
+ };
119
+ }
120
+
121
+ return {
122
+ success: false,
123
+ output: '',
124
+ error: `Fetch error: ${err.message}`,
125
+ };
126
+ }
127
+ }