@cloudflare/think 0.0.0 → 0.0.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/README.md +241 -0
- package/dist/classPrivateFieldSet2-COLddhya.js +27 -0
- package/dist/classPrivateMethodInitSpec-CdQXQy1O.js +7 -0
- package/dist/extensions/index.d.ts +20 -0
- package/dist/extensions/index.js +62 -0
- package/dist/extensions/index.js.map +1 -0
- package/dist/index-BlcvIdWK.d.ts +171 -0
- package/dist/index-C4OTSwUW.d.ts +193 -0
- package/dist/manager-DIV0gQf3.js +214 -0
- package/dist/manager-DIV0gQf3.js.map +1 -0
- package/dist/message-builder.d.ts +51 -0
- package/dist/message-builder.js +217 -0
- package/dist/message-builder.js.map +1 -0
- package/dist/session/index.d.ts +22 -0
- package/dist/session/index.js +2 -0
- package/dist/session-C6ZU_1zM.js +507 -0
- package/dist/session-C6ZU_1zM.js.map +1 -0
- package/dist/think.d.ts +315 -0
- package/dist/think.js +701 -0
- package/dist/think.js.map +1 -0
- package/dist/tools/execute.d.ts +105 -0
- package/dist/tools/execute.js +64 -0
- package/dist/tools/execute.js.map +1 -0
- package/dist/tools/extensions.d.ts +67 -0
- package/dist/tools/extensions.js +85 -0
- package/dist/tools/extensions.js.map +1 -0
- package/dist/tools/workspace.d.ts +303 -0
- package/dist/tools/workspace.js +398 -0
- package/dist/tools/workspace.js.map +1 -0
- package/dist/transport.d.ts +69 -0
- package/dist/transport.js +166 -0
- package/dist/transport.js.map +1 -0
- package/package.json +83 -9
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import * as ai from "ai";
|
|
2
|
+
import { FileInfo, Workspace } from "agents/experimental/workspace";
|
|
3
|
+
|
|
4
|
+
//#region src/tools/workspace.d.ts
|
|
5
|
+
interface ReadOperations {
|
|
6
|
+
readFile(path: string): Promise<string | null>;
|
|
7
|
+
stat(path: string): Promise<FileInfo | null> | FileInfo | null;
|
|
8
|
+
}
|
|
9
|
+
interface WriteOperations {
|
|
10
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
11
|
+
mkdir(
|
|
12
|
+
path: string,
|
|
13
|
+
opts?: {
|
|
14
|
+
recursive?: boolean;
|
|
15
|
+
}
|
|
16
|
+
): Promise<void> | void;
|
|
17
|
+
}
|
|
18
|
+
interface EditOperations {
|
|
19
|
+
readFile(path: string): Promise<string | null>;
|
|
20
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
interface ListOperations {
|
|
23
|
+
readDir(
|
|
24
|
+
dir: string,
|
|
25
|
+
opts?: {
|
|
26
|
+
limit?: number;
|
|
27
|
+
offset?: number;
|
|
28
|
+
}
|
|
29
|
+
): Promise<FileInfo[]> | FileInfo[];
|
|
30
|
+
}
|
|
31
|
+
interface FindOperations {
|
|
32
|
+
glob(pattern: string): Promise<FileInfo[]> | FileInfo[];
|
|
33
|
+
}
|
|
34
|
+
interface DeleteOperations {
|
|
35
|
+
rm(
|
|
36
|
+
path: string,
|
|
37
|
+
opts?: {
|
|
38
|
+
recursive?: boolean;
|
|
39
|
+
force?: boolean;
|
|
40
|
+
}
|
|
41
|
+
): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
interface GrepOperations {
|
|
44
|
+
glob(pattern: string): Promise<FileInfo[]> | FileInfo[];
|
|
45
|
+
readFile(path: string): Promise<string | null>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Create a complete set of AI SDK tools backed by a Workspace instance.
|
|
49
|
+
*
|
|
50
|
+
* ```ts
|
|
51
|
+
* import { Workspace } from "agents/experimental/workspace";
|
|
52
|
+
* import { createWorkspaceTools } from "@cloudflare/think";
|
|
53
|
+
*
|
|
54
|
+
* class MyAgent extends Agent<Env> {
|
|
55
|
+
* workspace = new Workspace(this);
|
|
56
|
+
*
|
|
57
|
+
* async onChatMessage() {
|
|
58
|
+
* const tools = createWorkspaceTools(this.workspace);
|
|
59
|
+
* const result = streamText({ model, tools, messages });
|
|
60
|
+
* return result.toUIMessageStreamResponse();
|
|
61
|
+
* }
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
declare function createWorkspaceTools(workspace: Workspace): {
|
|
66
|
+
read: ai.Tool<
|
|
67
|
+
{
|
|
68
|
+
path: string;
|
|
69
|
+
offset?: number | undefined;
|
|
70
|
+
limit?: number | undefined;
|
|
71
|
+
},
|
|
72
|
+
Record<string, unknown>
|
|
73
|
+
>;
|
|
74
|
+
write: ai.Tool<
|
|
75
|
+
{
|
|
76
|
+
path: string;
|
|
77
|
+
content: string;
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
path: string;
|
|
81
|
+
bytesWritten: number;
|
|
82
|
+
lines: number;
|
|
83
|
+
}
|
|
84
|
+
>;
|
|
85
|
+
edit: ai.Tool<
|
|
86
|
+
{
|
|
87
|
+
path: string;
|
|
88
|
+
old_string: string;
|
|
89
|
+
new_string: string;
|
|
90
|
+
},
|
|
91
|
+
| {
|
|
92
|
+
error: string;
|
|
93
|
+
path?: undefined;
|
|
94
|
+
created?: undefined;
|
|
95
|
+
lines?: undefined;
|
|
96
|
+
replaced?: undefined;
|
|
97
|
+
fuzzyMatch?: undefined;
|
|
98
|
+
}
|
|
99
|
+
| {
|
|
100
|
+
path: string;
|
|
101
|
+
created: boolean;
|
|
102
|
+
lines: number;
|
|
103
|
+
error?: undefined;
|
|
104
|
+
replaced?: undefined;
|
|
105
|
+
fuzzyMatch?: undefined;
|
|
106
|
+
}
|
|
107
|
+
| {
|
|
108
|
+
path: string;
|
|
109
|
+
replaced: boolean;
|
|
110
|
+
fuzzyMatch: boolean;
|
|
111
|
+
lines: number;
|
|
112
|
+
error?: undefined;
|
|
113
|
+
created?: undefined;
|
|
114
|
+
}
|
|
115
|
+
| {
|
|
116
|
+
path: string;
|
|
117
|
+
replaced: boolean;
|
|
118
|
+
lines: number;
|
|
119
|
+
error?: undefined;
|
|
120
|
+
created?: undefined;
|
|
121
|
+
fuzzyMatch?: undefined;
|
|
122
|
+
}
|
|
123
|
+
>;
|
|
124
|
+
list: ai.Tool<
|
|
125
|
+
{
|
|
126
|
+
path: string;
|
|
127
|
+
limit?: number | undefined;
|
|
128
|
+
offset?: number | undefined;
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
path: string;
|
|
132
|
+
count: number;
|
|
133
|
+
entries: string[];
|
|
134
|
+
}
|
|
135
|
+
>;
|
|
136
|
+
find: ai.Tool<
|
|
137
|
+
{
|
|
138
|
+
pattern: string;
|
|
139
|
+
},
|
|
140
|
+
Record<string, unknown>
|
|
141
|
+
>;
|
|
142
|
+
grep: ai.Tool<
|
|
143
|
+
{
|
|
144
|
+
query: string;
|
|
145
|
+
include?: string | undefined;
|
|
146
|
+
fixedString?: boolean | undefined;
|
|
147
|
+
caseSensitive?: boolean | undefined;
|
|
148
|
+
contextLines?: number | undefined;
|
|
149
|
+
},
|
|
150
|
+
Record<string, unknown>
|
|
151
|
+
>;
|
|
152
|
+
delete: ai.Tool<
|
|
153
|
+
{
|
|
154
|
+
path: string;
|
|
155
|
+
recursive?: boolean | undefined;
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
deleted: string;
|
|
159
|
+
}
|
|
160
|
+
>;
|
|
161
|
+
};
|
|
162
|
+
interface ReadToolOptions {
|
|
163
|
+
ops: ReadOperations;
|
|
164
|
+
}
|
|
165
|
+
declare function createReadTool(options: ReadToolOptions): ai.Tool<
|
|
166
|
+
{
|
|
167
|
+
path: string;
|
|
168
|
+
offset?: number | undefined;
|
|
169
|
+
limit?: number | undefined;
|
|
170
|
+
},
|
|
171
|
+
Record<string, unknown>
|
|
172
|
+
>;
|
|
173
|
+
interface WriteToolOptions {
|
|
174
|
+
ops: WriteOperations;
|
|
175
|
+
}
|
|
176
|
+
declare function createWriteTool(options: WriteToolOptions): ai.Tool<
|
|
177
|
+
{
|
|
178
|
+
path: string;
|
|
179
|
+
content: string;
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
path: string;
|
|
183
|
+
bytesWritten: number;
|
|
184
|
+
lines: number;
|
|
185
|
+
}
|
|
186
|
+
>;
|
|
187
|
+
interface EditToolOptions {
|
|
188
|
+
ops: EditOperations;
|
|
189
|
+
}
|
|
190
|
+
declare function createEditTool(options: EditToolOptions): ai.Tool<
|
|
191
|
+
{
|
|
192
|
+
path: string;
|
|
193
|
+
old_string: string;
|
|
194
|
+
new_string: string;
|
|
195
|
+
},
|
|
196
|
+
| {
|
|
197
|
+
error: string;
|
|
198
|
+
path?: undefined;
|
|
199
|
+
created?: undefined;
|
|
200
|
+
lines?: undefined;
|
|
201
|
+
replaced?: undefined;
|
|
202
|
+
fuzzyMatch?: undefined;
|
|
203
|
+
}
|
|
204
|
+
| {
|
|
205
|
+
path: string;
|
|
206
|
+
created: boolean;
|
|
207
|
+
lines: number;
|
|
208
|
+
error?: undefined;
|
|
209
|
+
replaced?: undefined;
|
|
210
|
+
fuzzyMatch?: undefined;
|
|
211
|
+
}
|
|
212
|
+
| {
|
|
213
|
+
path: string;
|
|
214
|
+
replaced: boolean;
|
|
215
|
+
fuzzyMatch: boolean;
|
|
216
|
+
lines: number;
|
|
217
|
+
error?: undefined;
|
|
218
|
+
created?: undefined;
|
|
219
|
+
}
|
|
220
|
+
| {
|
|
221
|
+
path: string;
|
|
222
|
+
replaced: boolean;
|
|
223
|
+
lines: number;
|
|
224
|
+
error?: undefined;
|
|
225
|
+
created?: undefined;
|
|
226
|
+
fuzzyMatch?: undefined;
|
|
227
|
+
}
|
|
228
|
+
>;
|
|
229
|
+
interface ListToolOptions {
|
|
230
|
+
ops: ListOperations;
|
|
231
|
+
}
|
|
232
|
+
declare function createListTool(options: ListToolOptions): ai.Tool<
|
|
233
|
+
{
|
|
234
|
+
path: string;
|
|
235
|
+
limit?: number | undefined;
|
|
236
|
+
offset?: number | undefined;
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
path: string;
|
|
240
|
+
count: number;
|
|
241
|
+
entries: string[];
|
|
242
|
+
}
|
|
243
|
+
>;
|
|
244
|
+
interface FindToolOptions {
|
|
245
|
+
ops: FindOperations;
|
|
246
|
+
}
|
|
247
|
+
declare function createFindTool(options: FindToolOptions): ai.Tool<
|
|
248
|
+
{
|
|
249
|
+
pattern: string;
|
|
250
|
+
},
|
|
251
|
+
Record<string, unknown>
|
|
252
|
+
>;
|
|
253
|
+
interface GrepToolOptions {
|
|
254
|
+
ops: GrepOperations;
|
|
255
|
+
}
|
|
256
|
+
declare function createGrepTool(options: GrepToolOptions): ai.Tool<
|
|
257
|
+
{
|
|
258
|
+
query: string;
|
|
259
|
+
include?: string | undefined;
|
|
260
|
+
fixedString?: boolean | undefined;
|
|
261
|
+
caseSensitive?: boolean | undefined;
|
|
262
|
+
contextLines?: number | undefined;
|
|
263
|
+
},
|
|
264
|
+
Record<string, unknown>
|
|
265
|
+
>;
|
|
266
|
+
interface DeleteToolOptions {
|
|
267
|
+
ops: DeleteOperations;
|
|
268
|
+
}
|
|
269
|
+
declare function createDeleteTool(options: DeleteToolOptions): ai.Tool<
|
|
270
|
+
{
|
|
271
|
+
path: string;
|
|
272
|
+
recursive?: boolean | undefined;
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
deleted: string;
|
|
276
|
+
}
|
|
277
|
+
>;
|
|
278
|
+
//#endregion
|
|
279
|
+
export {
|
|
280
|
+
DeleteOperations,
|
|
281
|
+
DeleteToolOptions,
|
|
282
|
+
EditOperations,
|
|
283
|
+
EditToolOptions,
|
|
284
|
+
FindOperations,
|
|
285
|
+
FindToolOptions,
|
|
286
|
+
GrepOperations,
|
|
287
|
+
GrepToolOptions,
|
|
288
|
+
ListOperations,
|
|
289
|
+
ListToolOptions,
|
|
290
|
+
ReadOperations,
|
|
291
|
+
ReadToolOptions,
|
|
292
|
+
WriteOperations,
|
|
293
|
+
WriteToolOptions,
|
|
294
|
+
createDeleteTool,
|
|
295
|
+
createEditTool,
|
|
296
|
+
createFindTool,
|
|
297
|
+
createGrepTool,
|
|
298
|
+
createListTool,
|
|
299
|
+
createReadTool,
|
|
300
|
+
createWorkspaceTools,
|
|
301
|
+
createWriteTool
|
|
302
|
+
};
|
|
303
|
+
//# sourceMappingURL=workspace.d.ts.map
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/tools/workspace.ts
|
|
4
|
+
function workspaceReadOps(ws) {
|
|
5
|
+
return {
|
|
6
|
+
readFile: (path) => ws.readFile(path),
|
|
7
|
+
stat: (path) => ws.stat(path)
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function workspaceWriteOps(ws) {
|
|
11
|
+
return {
|
|
12
|
+
writeFile: (path, content) => ws.writeFile(path, content),
|
|
13
|
+
mkdir: (path, opts) => ws.mkdir(path, opts)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function workspaceEditOps(ws) {
|
|
17
|
+
return {
|
|
18
|
+
readFile: (path) => ws.readFile(path),
|
|
19
|
+
writeFile: (path, content) => ws.writeFile(path, content)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function workspaceListOps(ws) {
|
|
23
|
+
return { readDir: (dir, opts) => ws.readDir(dir, opts) };
|
|
24
|
+
}
|
|
25
|
+
function workspaceFindOps(ws) {
|
|
26
|
+
return { glob: (pattern) => ws.glob(pattern) };
|
|
27
|
+
}
|
|
28
|
+
function workspaceDeleteOps(ws) {
|
|
29
|
+
return { rm: (path, opts) => ws.rm(path, opts) };
|
|
30
|
+
}
|
|
31
|
+
function workspaceGrepOps(ws) {
|
|
32
|
+
return {
|
|
33
|
+
glob: (pattern) => ws.glob(pattern),
|
|
34
|
+
readFile: (path) => ws.readFile(path)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create a complete set of AI SDK tools backed by a Workspace instance.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* import { Workspace } from "agents/experimental/workspace";
|
|
42
|
+
* import { createWorkspaceTools } from "@cloudflare/think";
|
|
43
|
+
*
|
|
44
|
+
* class MyAgent extends Agent<Env> {
|
|
45
|
+
* workspace = new Workspace(this);
|
|
46
|
+
*
|
|
47
|
+
* async onChatMessage() {
|
|
48
|
+
* const tools = createWorkspaceTools(this.workspace);
|
|
49
|
+
* const result = streamText({ model, tools, messages });
|
|
50
|
+
* return result.toUIMessageStreamResponse();
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
function createWorkspaceTools(workspace) {
|
|
56
|
+
return {
|
|
57
|
+
read: createReadTool({ ops: workspaceReadOps(workspace) }),
|
|
58
|
+
write: createWriteTool({ ops: workspaceWriteOps(workspace) }),
|
|
59
|
+
edit: createEditTool({ ops: workspaceEditOps(workspace) }),
|
|
60
|
+
list: createListTool({ ops: workspaceListOps(workspace) }),
|
|
61
|
+
find: createFindTool({ ops: workspaceFindOps(workspace) }),
|
|
62
|
+
grep: createGrepTool({ ops: workspaceGrepOps(workspace) }),
|
|
63
|
+
delete: createDeleteTool({ ops: workspaceDeleteOps(workspace) })
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const MAX_LINES = 2e3;
|
|
67
|
+
const MAX_LINE_LENGTH = 2e3;
|
|
68
|
+
function createReadTool(options) {
|
|
69
|
+
const { ops } = options;
|
|
70
|
+
return tool({
|
|
71
|
+
description: "Read the contents of a file. Returns the file content with line numbers. Use offset and limit for large files. Returns null if the file does not exist.",
|
|
72
|
+
inputSchema: z.object({
|
|
73
|
+
path: z.string().describe("Absolute path to the file"),
|
|
74
|
+
offset: z.number().int().min(1).optional().describe("1-indexed line number to start reading from"),
|
|
75
|
+
limit: z.number().int().min(1).optional().describe("Number of lines to read")
|
|
76
|
+
}),
|
|
77
|
+
execute: async ({ path, offset, limit }) => {
|
|
78
|
+
const stat = await ops.stat(path);
|
|
79
|
+
if (!stat) return { error: `File not found: ${path}` };
|
|
80
|
+
if (stat.type === "directory") return { error: `${path} is a directory, not a file` };
|
|
81
|
+
const content = await ops.readFile(path);
|
|
82
|
+
if (content === null) return { error: `Could not read file: ${path}` };
|
|
83
|
+
const allLines = content.split("\n");
|
|
84
|
+
const totalLines = allLines.length;
|
|
85
|
+
const startLine = offset ? offset - 1 : 0;
|
|
86
|
+
const endLine = limit ? startLine + limit : allLines.length;
|
|
87
|
+
const numbered = allLines.slice(startLine, endLine).map((line, i) => {
|
|
88
|
+
return `${startLine + i + 1}\t${line.length > MAX_LINE_LENGTH ? line.slice(0, MAX_LINE_LENGTH) + "... (truncated)" : line}`;
|
|
89
|
+
});
|
|
90
|
+
let output;
|
|
91
|
+
if (numbered.length > MAX_LINES) output = numbered.slice(0, MAX_LINES).join("\n") + `\n... (${numbered.length - MAX_LINES} more lines truncated)`;
|
|
92
|
+
else output = numbered.join("\n");
|
|
93
|
+
const result = {
|
|
94
|
+
path,
|
|
95
|
+
content: output,
|
|
96
|
+
totalLines
|
|
97
|
+
};
|
|
98
|
+
if (offset || limit) {
|
|
99
|
+
result.fromLine = startLine + 1;
|
|
100
|
+
result.toLine = Math.min(endLine, totalLines);
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function createWriteTool(options) {
|
|
107
|
+
const { ops } = options;
|
|
108
|
+
return tool({
|
|
109
|
+
description: "Write content to a file. Creates the file if it does not exist, overwrites if it does. Parent directories are created automatically.",
|
|
110
|
+
inputSchema: z.object({
|
|
111
|
+
path: z.string().describe("Absolute path to the file"),
|
|
112
|
+
content: z.string().describe("Content to write to the file")
|
|
113
|
+
}),
|
|
114
|
+
execute: async ({ path, content }) => {
|
|
115
|
+
const parent = path.replace(/\/[^/]+$/, "");
|
|
116
|
+
if (parent && parent !== "/") await ops.mkdir(parent, { recursive: true });
|
|
117
|
+
await ops.writeFile(path, content);
|
|
118
|
+
const lines = content.split("\n").length;
|
|
119
|
+
return {
|
|
120
|
+
path,
|
|
121
|
+
bytesWritten: new TextEncoder().encode(content).byteLength,
|
|
122
|
+
lines
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function createEditTool(options) {
|
|
128
|
+
const { ops } = options;
|
|
129
|
+
return tool({
|
|
130
|
+
description: "Make a targeted edit to a file by replacing an exact string match. Provide the old_string to find and new_string to replace it with. The old_string must match exactly (including whitespace and indentation). Use an empty old_string with new_string to create a new file.",
|
|
131
|
+
inputSchema: z.object({
|
|
132
|
+
path: z.string().describe("Absolute path to the file"),
|
|
133
|
+
old_string: z.string().describe("Exact text to find and replace. Empty string to create a new file."),
|
|
134
|
+
new_string: z.string().describe("Replacement text")
|
|
135
|
+
}),
|
|
136
|
+
execute: async ({ path, old_string, new_string }) => {
|
|
137
|
+
if (old_string === "") {
|
|
138
|
+
if (await ops.readFile(path) !== null) return { error: "File already exists. Provide old_string to edit, or use the write tool to overwrite." };
|
|
139
|
+
await ops.writeFile(path, new_string);
|
|
140
|
+
return {
|
|
141
|
+
path,
|
|
142
|
+
created: true,
|
|
143
|
+
lines: new_string.split("\n").length
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const content = await ops.readFile(path);
|
|
147
|
+
if (content === null) return { error: `File not found: ${path}` };
|
|
148
|
+
const occurrences = countOccurrences(content, old_string);
|
|
149
|
+
if (occurrences === 0) {
|
|
150
|
+
const fuzzyResult = fuzzyReplace(content, old_string, new_string);
|
|
151
|
+
if (fuzzyResult === "ambiguous") return { error: "old_string matches multiple locations after whitespace normalization. Include more surrounding context to make the match unique." };
|
|
152
|
+
if (fuzzyResult !== null) {
|
|
153
|
+
await ops.writeFile(path, fuzzyResult);
|
|
154
|
+
return {
|
|
155
|
+
path,
|
|
156
|
+
replaced: true,
|
|
157
|
+
fuzzyMatch: true,
|
|
158
|
+
lines: fuzzyResult.split("\n").length
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return { error: "old_string not found in file. Make sure it matches exactly, including whitespace and indentation. Read the file first to verify." };
|
|
162
|
+
}
|
|
163
|
+
if (occurrences > 1) return { error: `old_string appears ${occurrences} times in the file. Include more surrounding context to make the match unique.` };
|
|
164
|
+
const newContent = content.replace(old_string, new_string);
|
|
165
|
+
await ops.writeFile(path, newContent);
|
|
166
|
+
return {
|
|
167
|
+
path,
|
|
168
|
+
replaced: true,
|
|
169
|
+
lines: newContent.split("\n").length
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function countOccurrences(text, search) {
|
|
175
|
+
let count = 0;
|
|
176
|
+
let pos = 0;
|
|
177
|
+
while (true) {
|
|
178
|
+
const idx = text.indexOf(search, pos);
|
|
179
|
+
if (idx === -1) break;
|
|
180
|
+
count++;
|
|
181
|
+
pos = idx + 1;
|
|
182
|
+
}
|
|
183
|
+
return count;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Fuzzy replacement: normalize whitespace in both the file content
|
|
187
|
+
* and the search string, find the match, then replace the corresponding
|
|
188
|
+
* region in the original content.
|
|
189
|
+
*/
|
|
190
|
+
function fuzzyReplace(content, oldStr, newStr) {
|
|
191
|
+
const normalizedContent = normalizeWhitespace(content);
|
|
192
|
+
const normalizedSearch = normalizeWhitespace(oldStr);
|
|
193
|
+
if (!normalizedSearch) return null;
|
|
194
|
+
const idx = normalizedContent.indexOf(normalizedSearch);
|
|
195
|
+
if (idx === -1) return null;
|
|
196
|
+
if (normalizedContent.indexOf(normalizedSearch, idx + normalizedSearch.length) !== -1) return "ambiguous";
|
|
197
|
+
const originalStart = mapToOriginal(content, idx);
|
|
198
|
+
const originalEnd = mapToOriginal(content, idx + normalizedSearch.length);
|
|
199
|
+
return content.slice(0, originalStart) + newStr + content.slice(originalEnd);
|
|
200
|
+
}
|
|
201
|
+
function normalizeWhitespace(s) {
|
|
202
|
+
return s.replace(/[ \t]+/g, " ").replace(/\r\n/g, "\n");
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Map a position in the normalized string back to the original string.
|
|
206
|
+
* Walks both strings char-by-char, skipping extra whitespace in the original.
|
|
207
|
+
*/
|
|
208
|
+
function mapToOriginal(original, normalizedPos) {
|
|
209
|
+
let ni = 0;
|
|
210
|
+
let oi = 0;
|
|
211
|
+
while (ni < normalizedPos && oi < original.length) {
|
|
212
|
+
const oc = original[oi];
|
|
213
|
+
if (oc === "\r" && original[oi + 1] === "\n") {
|
|
214
|
+
oi += 2;
|
|
215
|
+
ni += 1;
|
|
216
|
+
} else if (oc === " " || oc === " ") {
|
|
217
|
+
oi++;
|
|
218
|
+
while (oi < original.length && (original[oi] === " " || original[oi] === " ")) oi++;
|
|
219
|
+
ni++;
|
|
220
|
+
} else {
|
|
221
|
+
oi++;
|
|
222
|
+
ni++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return oi;
|
|
226
|
+
}
|
|
227
|
+
function createListTool(options) {
|
|
228
|
+
const { ops } = options;
|
|
229
|
+
return tool({
|
|
230
|
+
description: "List files and directories in a given path. Returns names, types, and sizes for each entry.",
|
|
231
|
+
inputSchema: z.object({
|
|
232
|
+
path: z.string().default("/").describe("Absolute path to the directory to list"),
|
|
233
|
+
limit: z.number().int().min(1).max(1e3).optional().describe("Maximum number of entries to return (default: 200)"),
|
|
234
|
+
offset: z.number().int().min(0).optional().describe("Number of entries to skip (for pagination)")
|
|
235
|
+
}),
|
|
236
|
+
execute: async ({ path, limit, offset }) => {
|
|
237
|
+
const maxEntries = limit ?? 200;
|
|
238
|
+
const entries = await ops.readDir(path, {
|
|
239
|
+
limit: maxEntries,
|
|
240
|
+
offset: offset ?? 0
|
|
241
|
+
});
|
|
242
|
+
const formatted = entries.map((entry) => {
|
|
243
|
+
const suffix = entry.type === "directory" ? "/" : "";
|
|
244
|
+
const sizeStr = entry.type === "file" ? ` (${formatSize(entry.size)})` : "";
|
|
245
|
+
return `${entry.name}${suffix}${sizeStr}`;
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
path,
|
|
249
|
+
count: entries.length,
|
|
250
|
+
entries: formatted
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
function formatSize(bytes) {
|
|
256
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
257
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
258
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
259
|
+
}
|
|
260
|
+
function createFindTool(options) {
|
|
261
|
+
const { ops } = options;
|
|
262
|
+
return tool({
|
|
263
|
+
description: "Find files matching a glob pattern. Supports standard glob syntax: * matches any file, ** matches directories recursively, ? matches a single character. Returns matching file paths with types and sizes.",
|
|
264
|
+
inputSchema: z.object({ pattern: z.string().describe("Glob pattern to match (e.g. \"**/*.ts\", \"src/**/*.test.ts\", \"*.md\")") }),
|
|
265
|
+
execute: async ({ pattern }) => {
|
|
266
|
+
const matches = await ops.glob(pattern);
|
|
267
|
+
const MAX_RESULTS = 200;
|
|
268
|
+
const truncated = matches.length > MAX_RESULTS;
|
|
269
|
+
const formatted = matches.slice(0, MAX_RESULTS).map((entry) => {
|
|
270
|
+
const suffix = entry.type === "directory" ? "/" : "";
|
|
271
|
+
return `${entry.path}${suffix}`;
|
|
272
|
+
});
|
|
273
|
+
const result = {
|
|
274
|
+
pattern,
|
|
275
|
+
count: matches.length,
|
|
276
|
+
files: formatted
|
|
277
|
+
};
|
|
278
|
+
if (truncated) {
|
|
279
|
+
result.truncated = true;
|
|
280
|
+
result.showing = MAX_RESULTS;
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const MAX_MATCHES = 200;
|
|
287
|
+
const MAX_FILE_SIZE = 1048576;
|
|
288
|
+
function createGrepTool(options) {
|
|
289
|
+
const { ops } = options;
|
|
290
|
+
return tool({
|
|
291
|
+
description: "Search file contents using a regular expression or fixed string. Returns matching lines with file paths and line numbers. Searches all files matching the include glob, or all files if not specified.",
|
|
292
|
+
inputSchema: z.object({
|
|
293
|
+
query: z.string().describe("Search pattern (regex or fixed string)"),
|
|
294
|
+
include: z.string().optional().describe("Glob pattern to filter files (e.g. \"**/*.ts\"). Defaults to \"**/*\""),
|
|
295
|
+
fixedString: z.boolean().optional().describe("If true, treat query as a literal string instead of regex"),
|
|
296
|
+
caseSensitive: z.boolean().optional().describe("If true, search is case-sensitive (default: false)"),
|
|
297
|
+
contextLines: z.number().int().min(0).max(10).optional().describe("Number of context lines around each match (default: 0)")
|
|
298
|
+
}),
|
|
299
|
+
execute: async ({ query, include, fixedString, caseSensitive, contextLines }) => {
|
|
300
|
+
const pattern = include ?? "**/*";
|
|
301
|
+
const files = (await ops.glob(pattern)).filter((f) => f.type === "file");
|
|
302
|
+
let regex;
|
|
303
|
+
try {
|
|
304
|
+
const escaped = fixedString ? escapeRegex(query) : query;
|
|
305
|
+
regex = new RegExp(escaped, caseSensitive ? "g" : "gi");
|
|
306
|
+
} catch {
|
|
307
|
+
return { error: `Invalid regex: ${query}` };
|
|
308
|
+
}
|
|
309
|
+
const ctx = contextLines ?? 0;
|
|
310
|
+
const matches = [];
|
|
311
|
+
let totalMatches = 0;
|
|
312
|
+
let filesSearched = 0;
|
|
313
|
+
let filesWithMatches = 0;
|
|
314
|
+
let filesSkipped = 0;
|
|
315
|
+
for (const file of files) {
|
|
316
|
+
if (totalMatches >= MAX_MATCHES) break;
|
|
317
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
318
|
+
filesSkipped++;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const content = await ops.readFile(file.path);
|
|
322
|
+
if (content === null) continue;
|
|
323
|
+
filesSearched++;
|
|
324
|
+
const lines = content.split("\n");
|
|
325
|
+
let fileHasMatch = false;
|
|
326
|
+
for (let i = 0; i < lines.length; i++) {
|
|
327
|
+
if (totalMatches >= MAX_MATCHES) break;
|
|
328
|
+
regex.lastIndex = 0;
|
|
329
|
+
if (regex.test(lines[i])) {
|
|
330
|
+
if (!fileHasMatch) {
|
|
331
|
+
fileHasMatch = true;
|
|
332
|
+
filesWithMatches++;
|
|
333
|
+
}
|
|
334
|
+
totalMatches++;
|
|
335
|
+
const match = {
|
|
336
|
+
file: file.path,
|
|
337
|
+
line: i + 1,
|
|
338
|
+
text: lines[i]
|
|
339
|
+
};
|
|
340
|
+
if (ctx > 0) {
|
|
341
|
+
const start = Math.max(0, i - ctx);
|
|
342
|
+
const end = Math.min(lines.length, i + ctx + 1);
|
|
343
|
+
match.context = lines.slice(start, end).map((l, j) => {
|
|
344
|
+
const lineNum = start + j + 1;
|
|
345
|
+
return `${lineNum === i + 1 ? ">" : " "} ${lineNum}\t${l}`;
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
matches.push(match);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const result = {
|
|
353
|
+
query,
|
|
354
|
+
filesSearched,
|
|
355
|
+
filesWithMatches,
|
|
356
|
+
totalMatches,
|
|
357
|
+
matches: matches.map((m) => {
|
|
358
|
+
if (m.context) return {
|
|
359
|
+
file: m.file,
|
|
360
|
+
line: m.line,
|
|
361
|
+
context: m.context.join("\n")
|
|
362
|
+
};
|
|
363
|
+
return `${m.file}:${m.line}: ${m.text}`;
|
|
364
|
+
})
|
|
365
|
+
};
|
|
366
|
+
if (totalMatches >= MAX_MATCHES) result.truncated = true;
|
|
367
|
+
if (filesSkipped > 0) {
|
|
368
|
+
result.filesSkipped = filesSkipped;
|
|
369
|
+
result.note = `${filesSkipped} file(s) skipped (larger than 1 MB)`;
|
|
370
|
+
}
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
function escapeRegex(s) {
|
|
376
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
377
|
+
}
|
|
378
|
+
function createDeleteTool(options) {
|
|
379
|
+
const { ops } = options;
|
|
380
|
+
return tool({
|
|
381
|
+
description: "Delete a file or directory. Set recursive to true to remove non-empty directories.",
|
|
382
|
+
inputSchema: z.object({
|
|
383
|
+
path: z.string().describe("Absolute path to the file or directory"),
|
|
384
|
+
recursive: z.boolean().optional().describe("If true, remove directories and their contents recursively")
|
|
385
|
+
}),
|
|
386
|
+
execute: async ({ path, recursive }) => {
|
|
387
|
+
await ops.rm(path, {
|
|
388
|
+
recursive,
|
|
389
|
+
force: true
|
|
390
|
+
});
|
|
391
|
+
return { deleted: path };
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
//#endregion
|
|
396
|
+
export { createDeleteTool, createEditTool, createFindTool, createGrepTool, createListTool, createReadTool, createWorkspaceTools, createWriteTool };
|
|
397
|
+
|
|
398
|
+
//# sourceMappingURL=workspace.js.map
|