forked from onlook-dev/onlook
-
Notifications
You must be signed in to change notification settings - Fork 25
Expand file tree
/
Copy pathindex.ts
More file actions
195 lines (187 loc) · 7.3 KB
/
index.ts
File metadata and controls
195 lines (187 loc) · 7.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import { anthropic } from '@ai-sdk/anthropic';
import { tool, type ToolSet } from 'ai';
import { readFile } from 'fs/promises';
import { z } from 'zod';
import { ONLOOK_PROMPT } from '../prompt/onlook';
import { getAllFiles } from './helpers';
import { CrawlerService } from './crawler';
export const listFilesTool = tool({
description: 'List all files in the current directory, including subdirectories',
parameters: z.object({
path: z
.string()
.describe(
'The absolute path to the directory to get files from. This should be the root directory of the project.',
),
}),
execute: async ({ path }) => {
const res = await getAllFiles(path);
if (!res.success) {
return { error: res.error };
}
return res.files;
},
});
export const readFilesTool = tool({
description: 'Read the contents of files',
parameters: z.object({
paths: z.array(z.string()).describe('The absolute paths to the files to read'),
}),
execute: async ({ paths }) => {
try {
const files = await Promise.all(
paths.map(async (path) => {
const file = await readFile(path, 'utf8');
return { path, content: file };
}),
);
return files;
} catch (error) {
return `Error: ${error instanceof Error ? error.message : error}`;
}
},
});
export const onlookInstructionsTool = tool({
description: 'Get the instructions for the Onlook AI',
parameters: z.object({}),
execute: async () => {
return ONLOOK_PROMPT;
},
});
// https://docs.anthropic.com/en/docs/agents-and-tools/computer-use#understand-anthropic-defined-tools
// https://sdk.vercel.ai/docs/guides/computer-use#get-started-with-computer-use
// We currently can't use this because it doens't support streaming
interface FileOperationHandlers {
readFile: (path: string) => Promise<string>;
writeFile: (path: string, content: string) => Promise<boolean>;
undoEdit?: () => Promise<boolean>;
}
export const getStrReplaceEditorTool = (handlers: FileOperationHandlers) => {
const strReplaceEditorTool = anthropic.tools.textEditor_20250124({
execute: async ({
command,
path,
file_text,
insert_line,
new_str,
old_str,
view_range,
}) => {
try {
switch (command) {
case 'view': {
const content = await handlers.readFile(path);
if (view_range) {
const lines = content.split('\n');
const [start, end] = view_range;
return lines.slice(start - 1, end).join('\n');
}
return content;
}
case 'create': {
if (!file_text) {
throw new Error('file_text is required for create command');
}
await handlers.writeFile(path, file_text);
return `File created successfully at ${path}`;
}
case 'str_replace': {
if (!old_str) {
throw new Error('old_str is required for str_replace command');
}
const content = await handlers.readFile(path);
const newContent = content.replace(old_str, new_str || '');
await handlers.writeFile(path, newContent);
return `String replaced successfully in ${path}`;
}
case 'insert': {
if (!new_str || insert_line === undefined) {
throw new Error(
'new_str and insert_line are required for insert command',
);
}
const content = await handlers.readFile(path);
const lines = content.split('\n');
lines.splice(insert_line, 0, new_str);
await handlers.writeFile(path, lines.join('\n'));
return `Content inserted successfully at line ${insert_line} in ${path}`;
}
case 'undo_edit': {
if (handlers.undoEdit) {
await handlers.undoEdit();
return 'Edit undone successfully';
}
return 'Undo operation not implemented';
}
default: {
throw new Error(`Unknown command: ${command}`);
}
}
} catch (error) {
return `Error: ${error instanceof Error ? error.message : 'Unknown error'}`;
}
},
});
return strReplaceEditorTool;
};
export const crawlUrlTool = tool({
description: 'Crawl webpage content from provided URL',
parameters: z.object({
urls: z.array(z.string()).describe('Array of URLs to crawl'),
options: z
.object({
limit: z.number().optional(),
scrapeOptions: z
.object({
formats: z
.array(
z.enum([
'markdown',
'html',
'rawHtml',
'content',
'links',
'screenshot',
'screenshot@fullPage',
'extract',
'json',
'changeTracking',
]),
)
.optional(),
})
.optional(),
})
.optional(),
}),
execute: async ({ urls, options }) => {
try {
const crawler = CrawlerService.getInstance();
const results = await Promise.all(
urls.map(async (url) => {
try {
const result = await crawler.crawlUrl(url, options);
if (!result.success) {
return { url, error: result.status };
}
return { url, data: result.data[0] };
} catch (error) {
return {
url,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}),
);
return results;
} catch (error) {
return `Error: ${error instanceof Error ? error.message : 'Unknown error'}`;
}
},
});
export const chatToolSet: ToolSet = {
list_files: listFilesTool,
read_files: readFilesTool,
onlook_instructions: onlookInstructionsTool,
crawl_url: crawlUrlTool,
};