@robota-sdk/agent-tools 3.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/node/index.cjs +1352 -0
- package/dist/node/index.d.cts +353 -0
- package/dist/node/index.d.ts +353 -0
- package/dist/node/index.js +1303 -0
- package/package.json +51 -0
|
@@ -0,0 +1,1303 @@
|
|
|
1
|
+
// src/registry/tool-registry.ts
|
|
2
|
+
import { ValidationError } from "@robota-sdk/agent-core";
|
|
3
|
+
import { logger } from "@robota-sdk/agent-core";
|
|
4
|
+
var ToolRegistry = class {
|
|
5
|
+
tools = /* @__PURE__ */ new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Register a tool
|
|
8
|
+
*/
|
|
9
|
+
register(tool) {
|
|
10
|
+
if (!tool.schema?.name) {
|
|
11
|
+
throw new ValidationError("Tool must have a valid schema with name");
|
|
12
|
+
}
|
|
13
|
+
const toolName = tool.schema.name;
|
|
14
|
+
this.validateToolSchema(tool.schema);
|
|
15
|
+
if (this.tools.has(toolName)) {
|
|
16
|
+
logger.warn(`Tool "${toolName}" is already registered, overriding`, {
|
|
17
|
+
toolName,
|
|
18
|
+
existingTool: this.tools.get(toolName)?.constructor.name
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
this.tools.set(toolName, tool);
|
|
22
|
+
logger.debug(`Tool "${toolName}" registered successfully`, {
|
|
23
|
+
toolName,
|
|
24
|
+
toolType: tool.constructor.name,
|
|
25
|
+
parameters: Object.keys(tool.schema.parameters?.properties || {})
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Unregister a tool
|
|
30
|
+
*/
|
|
31
|
+
unregister(name) {
|
|
32
|
+
if (!this.tools.has(name)) {
|
|
33
|
+
logger.warn(`Attempted to unregister non-existent tool "${name}"`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.tools.delete(name);
|
|
37
|
+
logger.debug(`Tool "${name}" unregistered successfully`);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get tool by name
|
|
41
|
+
*/
|
|
42
|
+
get(name) {
|
|
43
|
+
return this.tools.get(name);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get all registered tools
|
|
47
|
+
*/
|
|
48
|
+
getAll() {
|
|
49
|
+
return Array.from(this.tools.values());
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get tool schemas
|
|
53
|
+
*/
|
|
54
|
+
getSchemas() {
|
|
55
|
+
const tools = this.getAll();
|
|
56
|
+
logger.debug("[TOOL-FLOW] ToolRegistry.getSchemas() - Tools before schema extraction", {
|
|
57
|
+
count: tools.length,
|
|
58
|
+
tools: tools.map((t) => ({
|
|
59
|
+
name: t.schema?.name ?? "unnamed",
|
|
60
|
+
hasSchema: !!t.schema,
|
|
61
|
+
schemaType: typeof t.schema,
|
|
62
|
+
toolType: t.constructor?.name || "unknown"
|
|
63
|
+
}))
|
|
64
|
+
});
|
|
65
|
+
return this.getAll().map((tool) => tool.schema);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if tool exists
|
|
69
|
+
*/
|
|
70
|
+
has(name) {
|
|
71
|
+
return this.tools.has(name);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Clear all tools
|
|
75
|
+
*/
|
|
76
|
+
clear() {
|
|
77
|
+
const toolCount = this.tools.size;
|
|
78
|
+
this.tools.clear();
|
|
79
|
+
logger.debug(`Cleared ${toolCount} tools from registry`);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get tool names
|
|
83
|
+
*/
|
|
84
|
+
getToolNames() {
|
|
85
|
+
return Array.from(this.tools.keys());
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get tools by pattern
|
|
89
|
+
*/
|
|
90
|
+
getToolsByPattern(pattern) {
|
|
91
|
+
const regex = typeof pattern === "string" ? new RegExp(pattern) : pattern;
|
|
92
|
+
return this.getAll().filter((tool) => regex.test(tool.schema.name));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get tool count
|
|
96
|
+
*/
|
|
97
|
+
size() {
|
|
98
|
+
return this.tools.size;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Validate tool schema
|
|
102
|
+
*/
|
|
103
|
+
validateToolSchema(schema) {
|
|
104
|
+
if (!schema.name || typeof schema.name !== "string") {
|
|
105
|
+
throw new ValidationError("Tool schema must have a valid name");
|
|
106
|
+
}
|
|
107
|
+
if (!schema.description || typeof schema.description !== "string") {
|
|
108
|
+
throw new ValidationError("Tool schema must have a description");
|
|
109
|
+
}
|
|
110
|
+
if (!schema.parameters || typeof schema.parameters !== "object" || schema.parameters === null || Array.isArray(schema.parameters)) {
|
|
111
|
+
throw new ValidationError("Tool schema must have parameters object");
|
|
112
|
+
}
|
|
113
|
+
if (schema.parameters.type !== "object") {
|
|
114
|
+
throw new ValidationError('Tool parameters type must be "object"');
|
|
115
|
+
}
|
|
116
|
+
if (schema.parameters.properties) {
|
|
117
|
+
for (const propName of Object.keys(schema.parameters.properties)) {
|
|
118
|
+
const propSchema = schema.parameters.properties[propName];
|
|
119
|
+
if (!propSchema?.type) {
|
|
120
|
+
throw new ValidationError(`Parameter "${propName}" must have a type`);
|
|
121
|
+
}
|
|
122
|
+
const validTypes = ["string", "number", "boolean", "array", "object"];
|
|
123
|
+
if (!validTypes.includes(propSchema.type)) {
|
|
124
|
+
throw new ValidationError(
|
|
125
|
+
`Parameter "${propName}" has invalid type "${propSchema.type}"`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (schema.parameters.required) {
|
|
131
|
+
const properties = schema.parameters.properties || {};
|
|
132
|
+
for (const requiredField of schema.parameters.required) {
|
|
133
|
+
if (!properties[requiredField]) {
|
|
134
|
+
throw new ValidationError(
|
|
135
|
+
`Required parameter "${requiredField}" is not defined in properties`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// src/implementations/function-tool.ts
|
|
144
|
+
import { ToolExecutionError, ValidationError as ValidationError2 } from "@robota-sdk/agent-core";
|
|
145
|
+
|
|
146
|
+
// src/implementations/function-tool/schema-converter.ts
|
|
147
|
+
function zodToJsonSchema(schema, options = {}) {
|
|
148
|
+
const properties = {};
|
|
149
|
+
const required = [];
|
|
150
|
+
const schemaDef = schema._def;
|
|
151
|
+
if (!schemaDef) {
|
|
152
|
+
throw new Error("Zod schema is missing _def; cannot convert to JSON schema.");
|
|
153
|
+
}
|
|
154
|
+
if (schemaDef.typeName === "ZodObject" && schemaDef.shape) {
|
|
155
|
+
const shape = typeof schemaDef.shape === "function" ? schemaDef.shape() : schemaDef.shape;
|
|
156
|
+
for (const [key, typeObj] of Object.entries(shape)) {
|
|
157
|
+
const property = convertZodTypeToProperty(typeObj);
|
|
158
|
+
properties[key] = property;
|
|
159
|
+
if (isRequiredField(typeObj)) {
|
|
160
|
+
required.push(key);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
type: "object",
|
|
166
|
+
properties,
|
|
167
|
+
required,
|
|
168
|
+
...options.allowAdditionalProperties && { additionalProperties: true }
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function convertZodTypeToProperty(typeObj) {
|
|
172
|
+
const typeDef = typeObj._def;
|
|
173
|
+
if (!typeDef) {
|
|
174
|
+
throw new Error("Zod type is missing _def; cannot convert to JSON schema.");
|
|
175
|
+
}
|
|
176
|
+
const base = {};
|
|
177
|
+
if (typeDef.description) {
|
|
178
|
+
base.description = typeDef.description;
|
|
179
|
+
}
|
|
180
|
+
switch (typeDef.typeName) {
|
|
181
|
+
case "ZodString":
|
|
182
|
+
return { type: "string", ...base };
|
|
183
|
+
case "ZodNumber":
|
|
184
|
+
return { type: "number", ...base };
|
|
185
|
+
case "ZodBoolean":
|
|
186
|
+
return { type: "boolean", ...base };
|
|
187
|
+
case "ZodArray": {
|
|
188
|
+
if (!typeDef.type) {
|
|
189
|
+
throw new Error("ZodArray is missing item type; cannot convert to JSON schema.");
|
|
190
|
+
}
|
|
191
|
+
const arrayItems = convertZodTypeToProperty(typeDef.type);
|
|
192
|
+
return {
|
|
193
|
+
type: "array",
|
|
194
|
+
items: arrayItems,
|
|
195
|
+
...base
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
case "ZodObject":
|
|
199
|
+
return { type: "object", ...base };
|
|
200
|
+
case "ZodEnum": {
|
|
201
|
+
const enumValues = typeDef.values;
|
|
202
|
+
if (!enumValues || !Array.isArray(enumValues)) {
|
|
203
|
+
throw new Error("ZodEnum is missing enum values; cannot convert to JSON schema.");
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
type: "string",
|
|
207
|
+
enum: enumValues,
|
|
208
|
+
...base
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
case "ZodOptional":
|
|
212
|
+
if (typeDef.innerType) {
|
|
213
|
+
const innerProperty = convertZodTypeToProperty(typeDef.innerType);
|
|
214
|
+
return { ...innerProperty, ...base };
|
|
215
|
+
}
|
|
216
|
+
throw new Error("ZodOptional is missing innerType; cannot convert to JSON schema.");
|
|
217
|
+
case "ZodNullable":
|
|
218
|
+
if (typeDef.innerType) {
|
|
219
|
+
const innerProperty = convertZodTypeToProperty(typeDef.innerType);
|
|
220
|
+
return { ...innerProperty, ...base };
|
|
221
|
+
}
|
|
222
|
+
throw new Error("ZodNullable is missing innerType; cannot convert to JSON schema.");
|
|
223
|
+
case "ZodDefault":
|
|
224
|
+
if (typeDef.innerType) {
|
|
225
|
+
const innerProperty = convertZodTypeToProperty(typeDef.innerType);
|
|
226
|
+
return { ...innerProperty, ...base };
|
|
227
|
+
}
|
|
228
|
+
throw new Error("ZodDefault is missing innerType; cannot convert to JSON schema.");
|
|
229
|
+
default:
|
|
230
|
+
throw new Error(`Unsupported Zod type: ${String(typeDef.typeName)}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function isRequiredField(typeObj) {
|
|
234
|
+
const typeDef = typeObj._def;
|
|
235
|
+
if (!typeDef) {
|
|
236
|
+
throw new Error("Zod schema is missing _def; cannot determine required fields.");
|
|
237
|
+
}
|
|
238
|
+
return typeDef.typeName !== "ZodOptional" && typeDef.typeName !== "ZodNullable" && typeDef.typeName !== "ZodDefault";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/implementations/function-tool.ts
|
|
242
|
+
var FunctionTool = class {
|
|
243
|
+
schema;
|
|
244
|
+
fn;
|
|
245
|
+
eventService;
|
|
246
|
+
constructor(schema, fn) {
|
|
247
|
+
this.schema = schema;
|
|
248
|
+
this.fn = fn;
|
|
249
|
+
this.validateConstructorInputs();
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get tool name
|
|
253
|
+
*/
|
|
254
|
+
getName() {
|
|
255
|
+
return this.schema.name;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Set EventService for post-construction injection.
|
|
259
|
+
* Accepts EventService as-is without transformation.
|
|
260
|
+
* Caller is responsible for providing properly configured EventService.
|
|
261
|
+
*/
|
|
262
|
+
setEventService(eventService) {
|
|
263
|
+
this.eventService = eventService;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Execute the function tool
|
|
267
|
+
*/
|
|
268
|
+
async execute(parameters, context) {
|
|
269
|
+
const toolName = this.schema.name;
|
|
270
|
+
if (!this.validate(parameters)) {
|
|
271
|
+
const errors = this.getValidationErrors(parameters);
|
|
272
|
+
throw new ValidationError2(`Invalid parameters for tool "${toolName}": ${errors.join(", ")}`);
|
|
273
|
+
}
|
|
274
|
+
const startTime = Date.now();
|
|
275
|
+
let result;
|
|
276
|
+
try {
|
|
277
|
+
result = await this.fn(parameters, context);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (error instanceof ToolExecutionError || error instanceof ValidationError2) {
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
throw new ToolExecutionError(
|
|
283
|
+
`Function tool execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
284
|
+
toolName,
|
|
285
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
286
|
+
{
|
|
287
|
+
parameterCount: Object.keys(parameters || {}).length,
|
|
288
|
+
hasContext: !!context
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
const executionTime = Date.now() - startTime;
|
|
293
|
+
return {
|
|
294
|
+
success: true,
|
|
295
|
+
data: result,
|
|
296
|
+
metadata: {
|
|
297
|
+
executionTime,
|
|
298
|
+
toolName,
|
|
299
|
+
parameters
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Validate parameters (simple boolean result)
|
|
305
|
+
*/
|
|
306
|
+
validate(parameters) {
|
|
307
|
+
return this.getValidationErrors(parameters).length === 0;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Validate tool parameters with detailed result
|
|
311
|
+
*/
|
|
312
|
+
validateParameters(parameters) {
|
|
313
|
+
const errors = this.getValidationErrors(parameters);
|
|
314
|
+
return {
|
|
315
|
+
isValid: errors.length === 0,
|
|
316
|
+
errors
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Get tool description
|
|
321
|
+
*/
|
|
322
|
+
getDescription() {
|
|
323
|
+
return this.schema.description;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Get detailed validation errors
|
|
327
|
+
*/
|
|
328
|
+
getValidationErrors(parameters) {
|
|
329
|
+
const errors = [];
|
|
330
|
+
const required = this.schema.parameters.required || [];
|
|
331
|
+
const properties = this.schema.parameters.properties || {};
|
|
332
|
+
for (const field of required) {
|
|
333
|
+
if (!(field in parameters)) {
|
|
334
|
+
errors.push(`Missing required parameter: ${field}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
338
|
+
const paramSchema = properties[key];
|
|
339
|
+
if (!paramSchema) {
|
|
340
|
+
errors.push(`Unknown parameter: ${key}`);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const typeError = this.validateParameterType(key, value, paramSchema);
|
|
344
|
+
if (typeError) {
|
|
345
|
+
errors.push(typeError);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return errors;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Validate individual parameter type
|
|
352
|
+
*/
|
|
353
|
+
validateParameterType(key, value, schema) {
|
|
354
|
+
const expectedType = schema["type"];
|
|
355
|
+
switch (expectedType) {
|
|
356
|
+
case "string":
|
|
357
|
+
if (typeof value !== "string") {
|
|
358
|
+
return `Parameter "${key}" must be a string, got ${typeof value}`;
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
361
|
+
case "number":
|
|
362
|
+
if (typeof value !== "number" || isNaN(value)) {
|
|
363
|
+
return `Parameter "${key}" must be a number, got ${typeof value}`;
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
366
|
+
case "boolean":
|
|
367
|
+
if (typeof value !== "boolean") {
|
|
368
|
+
return `Parameter "${key}" must be a boolean, got ${typeof value}`;
|
|
369
|
+
}
|
|
370
|
+
break;
|
|
371
|
+
case "array":
|
|
372
|
+
if (!Array.isArray(value)) {
|
|
373
|
+
return `Parameter "${key}" must be an array, got ${typeof value}`;
|
|
374
|
+
}
|
|
375
|
+
if (schema.items) {
|
|
376
|
+
for (let i = 0; i < value.length; i++) {
|
|
377
|
+
const itemError = this.validateParameterType(`${key}[${i}]`, value[i], schema.items);
|
|
378
|
+
if (itemError) {
|
|
379
|
+
return itemError;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
break;
|
|
384
|
+
case "object":
|
|
385
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
386
|
+
return `Parameter "${key}" must be an object, got ${typeof value}`;
|
|
387
|
+
}
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
if (schema.enum && schema.enum.length > 0) {
|
|
391
|
+
const enumValues = schema.enum;
|
|
392
|
+
let isValidEnum = false;
|
|
393
|
+
for (const enumValue of enumValues) {
|
|
394
|
+
if (value === enumValue) {
|
|
395
|
+
isValidEnum = true;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (!isValidEnum) {
|
|
400
|
+
return `Parameter "${key}" must be one of: ${enumValues.join(", ")}, got ${value}`;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return void 0;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Validate constructor inputs
|
|
407
|
+
*/
|
|
408
|
+
validateConstructorInputs() {
|
|
409
|
+
if (!this.schema) {
|
|
410
|
+
throw new ValidationError2("Tool schema is required");
|
|
411
|
+
}
|
|
412
|
+
if (!this.fn || typeof this.fn !== "function") {
|
|
413
|
+
throw new ValidationError2("Tool function is required and must be a function");
|
|
414
|
+
}
|
|
415
|
+
if (!this.schema.name) {
|
|
416
|
+
throw new ValidationError2("Tool schema must have a name");
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
function createFunctionTool(name, description, parameters, fn) {
|
|
421
|
+
const schema = {
|
|
422
|
+
name,
|
|
423
|
+
description,
|
|
424
|
+
parameters
|
|
425
|
+
};
|
|
426
|
+
return new FunctionTool(schema, fn);
|
|
427
|
+
}
|
|
428
|
+
function createZodFunctionTool(name, description, zodSchema, fn) {
|
|
429
|
+
const parameters = zodToJsonSchema(zodSchema);
|
|
430
|
+
const schema = {
|
|
431
|
+
name,
|
|
432
|
+
description,
|
|
433
|
+
parameters
|
|
434
|
+
};
|
|
435
|
+
const wrappedFn = async (parameters2, context) => {
|
|
436
|
+
const parseResult = zodSchema.safeParse(parameters2);
|
|
437
|
+
if (!parseResult.success) {
|
|
438
|
+
throw new ValidationError2(`Zod validation failed: ${parseResult.error}`);
|
|
439
|
+
}
|
|
440
|
+
const result = await fn(parseResult.data || parameters2, context);
|
|
441
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
442
|
+
};
|
|
443
|
+
return new FunctionTool(schema, wrappedFn);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/implementations/openapi-tool.ts
|
|
447
|
+
import { ToolExecutionError as ToolExecutionError2, ValidationError as ValidationError3 } from "@robota-sdk/agent-core";
|
|
448
|
+
var OpenAPITool = class {
|
|
449
|
+
schema;
|
|
450
|
+
apiSpec;
|
|
451
|
+
operationId;
|
|
452
|
+
baseURL;
|
|
453
|
+
config;
|
|
454
|
+
eventService;
|
|
455
|
+
constructor(config) {
|
|
456
|
+
this.config = config;
|
|
457
|
+
if (typeof config.spec !== "object" || config.spec === null || typeof config.spec.openapi !== "string" || typeof config.spec.paths !== "object") {
|
|
458
|
+
throw new Error(
|
|
459
|
+
'Invalid OpenAPI spec: must contain "openapi" (string) and "paths" (object) fields'
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
this.apiSpec = config.spec;
|
|
463
|
+
this.operationId = config.operationId;
|
|
464
|
+
this.baseURL = config.baseURL;
|
|
465
|
+
this.schema = this.createSchemaFromOpenAPI();
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Execute the OpenAPI tool
|
|
469
|
+
*/
|
|
470
|
+
async execute(parameters, context) {
|
|
471
|
+
const toolName = this.schema.name;
|
|
472
|
+
const validation = this.validateParameters(parameters);
|
|
473
|
+
if (!validation.isValid) {
|
|
474
|
+
throw new ValidationError3(
|
|
475
|
+
`Invalid parameters for OpenAPI tool "${toolName}": ${validation.errors.join(", ")}`
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
const startTime = Date.now();
|
|
480
|
+
const result = await this.executeAPICall(parameters, context);
|
|
481
|
+
const executionTime = Date.now() - startTime;
|
|
482
|
+
return {
|
|
483
|
+
success: true,
|
|
484
|
+
data: result,
|
|
485
|
+
metadata: {
|
|
486
|
+
executionTime,
|
|
487
|
+
toolName,
|
|
488
|
+
operationId: this.operationId,
|
|
489
|
+
baseURL: this.baseURL
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
} catch (error) {
|
|
493
|
+
if (error instanceof ToolExecutionError2 || error instanceof ValidationError3) {
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
const safeError = error instanceof Error ? error : new Error(String(error));
|
|
497
|
+
throw new ToolExecutionError2(
|
|
498
|
+
`OpenAPI tool execution failed: ${safeError.message}`,
|
|
499
|
+
toolName,
|
|
500
|
+
safeError,
|
|
501
|
+
{
|
|
502
|
+
operationId: this.operationId,
|
|
503
|
+
baseURL: this.baseURL,
|
|
504
|
+
parametersCount: Object.keys(parameters).length
|
|
505
|
+
}
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Validate tool parameters
|
|
511
|
+
*/
|
|
512
|
+
validate(parameters) {
|
|
513
|
+
return this.validateParameters(parameters).isValid;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Validate tool parameters with detailed result
|
|
517
|
+
*/
|
|
518
|
+
validateParameters(parameters) {
|
|
519
|
+
const required = this.schema.parameters.required || [];
|
|
520
|
+
const errors = [];
|
|
521
|
+
for (const field of required) {
|
|
522
|
+
if (!(field in parameters)) {
|
|
523
|
+
errors.push(`Missing required parameter: ${field}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
isValid: errors.length === 0,
|
|
528
|
+
errors
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Get tool name
|
|
533
|
+
*/
|
|
534
|
+
getName() {
|
|
535
|
+
return this.schema.name;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Set EventService for post-construction injection.
|
|
539
|
+
*/
|
|
540
|
+
setEventService(eventService) {
|
|
541
|
+
this.eventService = eventService;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Get tool description
|
|
545
|
+
*/
|
|
546
|
+
getDescription() {
|
|
547
|
+
return this.schema.description;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Execute the actual API call
|
|
551
|
+
* @private
|
|
552
|
+
*/
|
|
553
|
+
async executeAPICall(parameters, _context) {
|
|
554
|
+
const operation = this.findOperation();
|
|
555
|
+
if (!operation) {
|
|
556
|
+
throw new Error(`Operation ${this.operationId} not found in OpenAPI spec`);
|
|
557
|
+
}
|
|
558
|
+
const requestConfig = this.buildRequestConfig(operation, parameters);
|
|
559
|
+
throw new Error("Not implemented: actual API execution is not yet available");
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Find the operation in the OpenAPI specification
|
|
563
|
+
*/
|
|
564
|
+
findOperation() {
|
|
565
|
+
for (const [path, pathItem] of Object.entries(this.apiSpec.paths || {})) {
|
|
566
|
+
if (!pathItem) continue;
|
|
567
|
+
for (const method of [
|
|
568
|
+
"get",
|
|
569
|
+
"post",
|
|
570
|
+
"put",
|
|
571
|
+
"delete",
|
|
572
|
+
"patch",
|
|
573
|
+
"head",
|
|
574
|
+
"options"
|
|
575
|
+
]) {
|
|
576
|
+
const operation = pathItem[method];
|
|
577
|
+
if (operation?.operationId === this.operationId) {
|
|
578
|
+
return { method, path, operation };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return void 0;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Build HTTP request configuration from OpenAPI operation and parameters
|
|
586
|
+
*/
|
|
587
|
+
buildRequestConfig(opInfo, parameters) {
|
|
588
|
+
const { method, path, operation } = opInfo;
|
|
589
|
+
let url = this.baseURL + path;
|
|
590
|
+
const headers = {};
|
|
591
|
+
let body;
|
|
592
|
+
const params = operation.parameters || [];
|
|
593
|
+
for (const param of params) {
|
|
594
|
+
const value = parameters[param.name];
|
|
595
|
+
if (value === void 0 && param.required) {
|
|
596
|
+
throw new Error(`Required parameter ${param.name} is missing`);
|
|
597
|
+
}
|
|
598
|
+
if (value !== void 0) {
|
|
599
|
+
switch (param.in) {
|
|
600
|
+
case "path":
|
|
601
|
+
url = url.replace(`{${param.name}}`, encodeURIComponent(String(value)));
|
|
602
|
+
break;
|
|
603
|
+
case "query": {
|
|
604
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
605
|
+
url += `${separator}${param.name}=${encodeURIComponent(String(value))}`;
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
case "header":
|
|
609
|
+
headers[param.name] = String(value);
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (["post", "put", "patch"].includes(method) && operation.requestBody) {
|
|
615
|
+
const requestBody = operation.requestBody;
|
|
616
|
+
const jsonContent = requestBody.content?.["application/json"];
|
|
617
|
+
if (jsonContent) {
|
|
618
|
+
headers["Content-Type"] = "application/json";
|
|
619
|
+
const bodyParams = {};
|
|
620
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
621
|
+
const isParamUsed = params.some((p) => p.name === key);
|
|
622
|
+
if (!isParamUsed) {
|
|
623
|
+
bodyParams[key] = value;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
body = JSON.stringify(bodyParams);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (this.config.auth) {
|
|
630
|
+
switch (this.config.auth.type) {
|
|
631
|
+
case "bearer":
|
|
632
|
+
headers["Authorization"] = `Bearer ${this.config.auth.token}`;
|
|
633
|
+
break;
|
|
634
|
+
case "apiKey": {
|
|
635
|
+
const headerName = this.config.auth.header || "X-API-Key";
|
|
636
|
+
headers[headerName] = this.config.auth.apiKey || "";
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const result = {
|
|
642
|
+
method,
|
|
643
|
+
url,
|
|
644
|
+
headers
|
|
645
|
+
};
|
|
646
|
+
if (body !== void 0) {
|
|
647
|
+
result.body = body;
|
|
648
|
+
}
|
|
649
|
+
return result;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Create tool schema from OpenAPI operation specification
|
|
653
|
+
*/
|
|
654
|
+
createSchemaFromOpenAPI() {
|
|
655
|
+
const operation = this.findOperation();
|
|
656
|
+
if (!operation) {
|
|
657
|
+
throw new Error(
|
|
658
|
+
`[STRICT-POLICY][EMITTER-CONTRACT] OpenAPI operation not found: ${this.operationId}. Emitter contract must provide a valid operationId present in the OpenAPI document.`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
const { operation: opSpec } = operation;
|
|
662
|
+
const properties = {};
|
|
663
|
+
const required = [];
|
|
664
|
+
const params = opSpec.parameters || [];
|
|
665
|
+
for (const param of params) {
|
|
666
|
+
properties[param.name] = this.convertOpenAPIParamToSchema(param);
|
|
667
|
+
if (param.required) {
|
|
668
|
+
required.push(param.name);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (opSpec.requestBody) {
|
|
672
|
+
const requestBody = opSpec.requestBody;
|
|
673
|
+
const jsonContent = requestBody.content?.["application/json"];
|
|
674
|
+
if (jsonContent?.schema) {
|
|
675
|
+
const bodySchema = this.convertOpenAPISchemaToParameterSchema(jsonContent.schema);
|
|
676
|
+
if (bodySchema.type === "object" && bodySchema.properties) {
|
|
677
|
+
Object.assign(properties, bodySchema.properties);
|
|
678
|
+
const schemaWithRequired = bodySchema;
|
|
679
|
+
if (schemaWithRequired.required) {
|
|
680
|
+
required.push(...schemaWithRequired.required);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const schemaParams = {
|
|
686
|
+
type: "object",
|
|
687
|
+
properties
|
|
688
|
+
};
|
|
689
|
+
if (required.length > 0) {
|
|
690
|
+
schemaParams.required = required;
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
name: this.operationId,
|
|
694
|
+
description: opSpec.summary || opSpec.description || `OpenAPI operation: ${this.operationId}`,
|
|
695
|
+
parameters: schemaParams
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Convert OpenAPI parameter to tool parameter schema
|
|
700
|
+
*/
|
|
701
|
+
convertOpenAPIParamToSchema(param) {
|
|
702
|
+
const schema = param.schema;
|
|
703
|
+
return this.convertOpenAPISchemaToParameterSchema(schema);
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Convert OpenAPI schema to parameter schema
|
|
707
|
+
*/
|
|
708
|
+
convertOpenAPISchemaToParameterSchema(schema) {
|
|
709
|
+
if ("$ref" in schema) {
|
|
710
|
+
return { type: "object" };
|
|
711
|
+
}
|
|
712
|
+
const result = {
|
|
713
|
+
type: this.mapOpenAPIType(schema.type)
|
|
714
|
+
};
|
|
715
|
+
if (schema.description) {
|
|
716
|
+
result.description = schema.description;
|
|
717
|
+
}
|
|
718
|
+
if (schema.enum) {
|
|
719
|
+
result.enum = schema.enum;
|
|
720
|
+
}
|
|
721
|
+
if (schema.minimum !== void 0) {
|
|
722
|
+
result.minimum = schema.minimum;
|
|
723
|
+
}
|
|
724
|
+
if (schema.maximum !== void 0) {
|
|
725
|
+
result.maximum = schema.maximum;
|
|
726
|
+
}
|
|
727
|
+
if (schema.pattern) {
|
|
728
|
+
result.pattern = schema.pattern;
|
|
729
|
+
}
|
|
730
|
+
if (schema.format) {
|
|
731
|
+
result.format = schema.format;
|
|
732
|
+
}
|
|
733
|
+
if (schema.default !== void 0) {
|
|
734
|
+
result.default = schema.default;
|
|
735
|
+
}
|
|
736
|
+
if (schema.type === "array" && schema.items) {
|
|
737
|
+
result.items = this.convertOpenAPISchemaToParameterSchema(schema.items);
|
|
738
|
+
}
|
|
739
|
+
if (schema.type === "object" && schema.properties) {
|
|
740
|
+
result.properties = {};
|
|
741
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
742
|
+
result.properties[propName] = this.convertOpenAPISchemaToParameterSchema(propSchema);
|
|
743
|
+
}
|
|
744
|
+
if (schema.required && schema.required.length > 0) {
|
|
745
|
+
result.required = schema.required;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return result;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Map OpenAPI type to JSON schema type
|
|
752
|
+
*/
|
|
753
|
+
mapOpenAPIType(type) {
|
|
754
|
+
switch (type) {
|
|
755
|
+
case "string":
|
|
756
|
+
return "string";
|
|
757
|
+
case "number":
|
|
758
|
+
return "number";
|
|
759
|
+
case "integer":
|
|
760
|
+
return "integer";
|
|
761
|
+
case "boolean":
|
|
762
|
+
return "boolean";
|
|
763
|
+
case "array":
|
|
764
|
+
return "array";
|
|
765
|
+
case "object":
|
|
766
|
+
return "object";
|
|
767
|
+
default:
|
|
768
|
+
return "string";
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
function createOpenAPITool(config) {
|
|
773
|
+
return new OpenAPITool(config);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/builtins/bash-tool.ts
|
|
777
|
+
import { spawn } from "child_process";
|
|
778
|
+
import { z } from "zod";
|
|
779
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
780
|
+
var BashSchema = z.object({
|
|
781
|
+
command: z.string().describe("The bash command to execute"),
|
|
782
|
+
timeout: z.number().optional().describe("Optional timeout in milliseconds (max 600000). Default is 120000 (2 minutes)"),
|
|
783
|
+
workingDirectory: z.string().optional().describe("Working directory for the command. Defaults to the current working directory")
|
|
784
|
+
});
|
|
785
|
+
async function runBash(args) {
|
|
786
|
+
const { command, timeout = DEFAULT_TIMEOUT_MS, workingDirectory } = args;
|
|
787
|
+
return new Promise((resolve3) => {
|
|
788
|
+
const stdoutChunks = [];
|
|
789
|
+
const stderrChunks = [];
|
|
790
|
+
let timedOut = false;
|
|
791
|
+
let settled = false;
|
|
792
|
+
const child = spawn("sh", ["-c", command], {
|
|
793
|
+
cwd: workingDirectory ?? process.cwd(),
|
|
794
|
+
env: process.env,
|
|
795
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
796
|
+
});
|
|
797
|
+
child.stdout.on("data", (chunk) => {
|
|
798
|
+
stdoutChunks.push(chunk);
|
|
799
|
+
});
|
|
800
|
+
child.stderr.on("data", (chunk) => {
|
|
801
|
+
stderrChunks.push(chunk);
|
|
802
|
+
});
|
|
803
|
+
const timer = setTimeout(() => {
|
|
804
|
+
timedOut = true;
|
|
805
|
+
child.kill("SIGTERM");
|
|
806
|
+
}, timeout);
|
|
807
|
+
function settle(result) {
|
|
808
|
+
if (settled) return;
|
|
809
|
+
settled = true;
|
|
810
|
+
clearTimeout(timer);
|
|
811
|
+
resolve3(JSON.stringify(result));
|
|
812
|
+
}
|
|
813
|
+
child.on("error", (err) => {
|
|
814
|
+
settle({
|
|
815
|
+
success: false,
|
|
816
|
+
output: "",
|
|
817
|
+
error: err.message
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
child.on("close", (code) => {
|
|
821
|
+
if (timedOut) {
|
|
822
|
+
settle({
|
|
823
|
+
success: false,
|
|
824
|
+
output: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
825
|
+
error: `Command timed out after ${timeout}ms`,
|
|
826
|
+
exitCode: code ?? void 0
|
|
827
|
+
});
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
831
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
832
|
+
const exitCode = code ?? 0;
|
|
833
|
+
const output = stderr ? `${stdout}
|
|
834
|
+
stderr:
|
|
835
|
+
${stderr}` : stdout;
|
|
836
|
+
settle({
|
|
837
|
+
success: true,
|
|
838
|
+
output,
|
|
839
|
+
exitCode
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
var bashTool = createZodFunctionTool(
|
|
845
|
+
"Bash",
|
|
846
|
+
"Executes a given bash command and returns its output.\n\nThe working directory persists between commands, but shell state does not.\n\nIMPORTANT: Avoid using this tool to run `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or `echo` commands. Instead, use the appropriate dedicated tool:\n - File search: Use Glob (NOT find or ls)\n - Content search: Use Grep (NOT grep or rg)\n - Read files: Use Read (NOT cat/head/tail)\n - Edit files: Use Edit (NOT sed/awk)\n\nFor simple commands, keep the description brief (5-10 words). For complex commands, include enough context to clarify what the command does.\n\nOutput is limited to 30,000 characters. Longer output will be middle-truncated.",
|
|
847
|
+
BashSchema,
|
|
848
|
+
async (params) => {
|
|
849
|
+
return runBash(params);
|
|
850
|
+
}
|
|
851
|
+
);
|
|
852
|
+
|
|
853
|
+
// src/builtins/read-tool.ts
|
|
854
|
+
import { readFile, stat } from "fs/promises";
|
|
855
|
+
import { z as z2 } from "zod";
|
|
856
|
+
var DEFAULT_LIMIT = 2e3;
|
|
857
|
+
var ReadSchema = z2.object({
|
|
858
|
+
filePath: z2.string().describe("The absolute path to the file to read"),
|
|
859
|
+
offset: z2.number().optional().describe(
|
|
860
|
+
"The line number to start reading from (1-based). Only provide if the file is too large to read at once"
|
|
861
|
+
),
|
|
862
|
+
limit: z2.number().optional().describe(
|
|
863
|
+
`The number of lines to read (default: ${DEFAULT_LIMIT}). Only provide if the file is too large to read at once`
|
|
864
|
+
)
|
|
865
|
+
});
|
|
866
|
+
function isBinary(buffer) {
|
|
867
|
+
const checkLength = Math.min(buffer.length, 8192);
|
|
868
|
+
for (let i = 0; i < checkLength; i++) {
|
|
869
|
+
if (buffer[i] === 0) return true;
|
|
870
|
+
}
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
function formatWithLineNumbers(lines, startLine) {
|
|
874
|
+
const lastLineNum = startLine + lines.length - 1;
|
|
875
|
+
const width = String(lastLineNum).length;
|
|
876
|
+
return lines.map((line, idx) => {
|
|
877
|
+
const lineNum = String(startLine + idx).padStart(width, " ");
|
|
878
|
+
return `${lineNum} ${line}`;
|
|
879
|
+
}).join("\n");
|
|
880
|
+
}
|
|
881
|
+
async function readFileTool(args) {
|
|
882
|
+
const { filePath, offset, limit = DEFAULT_LIMIT } = args;
|
|
883
|
+
const startLine = offset !== void 0 && offset > 0 ? offset : 1;
|
|
884
|
+
let fileStats;
|
|
885
|
+
try {
|
|
886
|
+
fileStats = await stat(filePath);
|
|
887
|
+
} catch (err) {
|
|
888
|
+
const result2 = {
|
|
889
|
+
success: false,
|
|
890
|
+
output: "",
|
|
891
|
+
error: `File not found: ${filePath}`
|
|
892
|
+
};
|
|
893
|
+
return JSON.stringify(result2);
|
|
894
|
+
}
|
|
895
|
+
if (!fileStats.isFile()) {
|
|
896
|
+
const result2 = {
|
|
897
|
+
success: false,
|
|
898
|
+
output: "",
|
|
899
|
+
error: `Path is not a file: ${filePath}`
|
|
900
|
+
};
|
|
901
|
+
return JSON.stringify(result2);
|
|
902
|
+
}
|
|
903
|
+
let buffer;
|
|
904
|
+
try {
|
|
905
|
+
buffer = await readFile(filePath);
|
|
906
|
+
} catch (err) {
|
|
907
|
+
const result2 = {
|
|
908
|
+
success: false,
|
|
909
|
+
output: "",
|
|
910
|
+
error: err instanceof Error ? err.message : String(err)
|
|
911
|
+
};
|
|
912
|
+
return JSON.stringify(result2);
|
|
913
|
+
}
|
|
914
|
+
if (isBinary(buffer)) {
|
|
915
|
+
const result2 = {
|
|
916
|
+
success: false,
|
|
917
|
+
output: "",
|
|
918
|
+
error: `Binary file not supported: ${filePath}`
|
|
919
|
+
};
|
|
920
|
+
return JSON.stringify(result2);
|
|
921
|
+
}
|
|
922
|
+
const content = buffer.toString("utf8");
|
|
923
|
+
const allLines = content.split("\n");
|
|
924
|
+
if (allLines[allLines.length - 1] === "") {
|
|
925
|
+
allLines.pop();
|
|
926
|
+
}
|
|
927
|
+
const zeroBasedStart = startLine - 1;
|
|
928
|
+
const selectedLines = allLines.slice(zeroBasedStart, zeroBasedStart + limit);
|
|
929
|
+
const output = formatWithLineNumbers(selectedLines, startLine);
|
|
930
|
+
const totalLines = allLines.length;
|
|
931
|
+
const returnedLines = selectedLines.length;
|
|
932
|
+
const header = returnedLines < totalLines ? `[File: ${filePath} (lines ${startLine}-${startLine + returnedLines - 1} of ${totalLines})]
|
|
933
|
+
` : `[File: ${filePath} (${totalLines} lines)]
|
|
934
|
+
`;
|
|
935
|
+
const result = {
|
|
936
|
+
success: true,
|
|
937
|
+
output: header + output
|
|
938
|
+
};
|
|
939
|
+
return JSON.stringify(result);
|
|
940
|
+
}
|
|
941
|
+
var readTool = createZodFunctionTool(
|
|
942
|
+
"Read",
|
|
943
|
+
"Reads a file from the local filesystem.\n\nBy default, reads up to 2000 lines from the beginning of the file. You can optionally specify offset and limit for partial reads.\n\nResults are returned using cat -n format, with line numbers starting at 1.\n\nThe file_path parameter must be an absolute path, not a relative path.",
|
|
944
|
+
ReadSchema,
|
|
945
|
+
async (params) => {
|
|
946
|
+
return readFileTool(params);
|
|
947
|
+
}
|
|
948
|
+
);
|
|
949
|
+
|
|
950
|
+
// src/builtins/write-tool.ts
|
|
951
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
952
|
+
import { dirname } from "path";
|
|
953
|
+
import { z as z3 } from "zod";
|
|
954
|
+
var WriteSchema = z3.object({
|
|
955
|
+
filePath: z3.string().describe("The absolute path to the file to write"),
|
|
956
|
+
content: z3.string().describe("The content to write to the file")
|
|
957
|
+
});
|
|
958
|
+
async function writeFileTool(args) {
|
|
959
|
+
const { filePath, content } = args;
|
|
960
|
+
try {
|
|
961
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
962
|
+
await writeFile(filePath, content, "utf8");
|
|
963
|
+
const result = {
|
|
964
|
+
success: true,
|
|
965
|
+
output: `Written ${content.length} bytes to ${filePath}`
|
|
966
|
+
};
|
|
967
|
+
return JSON.stringify(result);
|
|
968
|
+
} catch (err) {
|
|
969
|
+
const result = {
|
|
970
|
+
success: false,
|
|
971
|
+
output: "",
|
|
972
|
+
error: err instanceof Error ? err.message : String(err)
|
|
973
|
+
};
|
|
974
|
+
return JSON.stringify(result);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
var writeTool = createZodFunctionTool(
|
|
978
|
+
"Write",
|
|
979
|
+
"Writes a file to the local filesystem. This will overwrite an existing file if one exists.\n\nALWAYS prefer the Edit tool for modifying existing files \u2014 it only sends the diff. Only use this tool to create new files or for complete rewrites.\n\nNEVER create documentation files (*.md) or README files unless explicitly requested by the user.",
|
|
980
|
+
WriteSchema,
|
|
981
|
+
async (params) => {
|
|
982
|
+
return writeFileTool(params);
|
|
983
|
+
}
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
// src/builtins/edit-tool.ts
|
|
987
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
988
|
+
import { z as z4 } from "zod";
|
|
989
|
+
var EditSchema = z4.object({
|
|
990
|
+
filePath: z4.string().describe("The absolute path to the file to modify"),
|
|
991
|
+
oldString: z4.string().describe("The text to replace (must be an exact match of existing content)"),
|
|
992
|
+
newString: z4.string().describe("The text to replace it with (must be different from old_string)"),
|
|
993
|
+
replaceAll: z4.boolean().optional().describe(
|
|
994
|
+
"Replace all occurrences of old_string (default: false). Useful for renaming variables"
|
|
995
|
+
)
|
|
996
|
+
});
|
|
997
|
+
async function editFileTool(args) {
|
|
998
|
+
const { filePath, oldString, newString, replaceAll = false } = args;
|
|
999
|
+
let content;
|
|
1000
|
+
try {
|
|
1001
|
+
content = await readFile2(filePath, "utf8");
|
|
1002
|
+
} catch (err) {
|
|
1003
|
+
const result2 = {
|
|
1004
|
+
success: false,
|
|
1005
|
+
output: "",
|
|
1006
|
+
error: `File not found: ${filePath}`
|
|
1007
|
+
};
|
|
1008
|
+
return JSON.stringify(result2);
|
|
1009
|
+
}
|
|
1010
|
+
if (!content.includes(oldString)) {
|
|
1011
|
+
const result2 = {
|
|
1012
|
+
success: false,
|
|
1013
|
+
output: "",
|
|
1014
|
+
error: `oldString not found in file: ${filePath}`
|
|
1015
|
+
};
|
|
1016
|
+
return JSON.stringify(result2);
|
|
1017
|
+
}
|
|
1018
|
+
if (!replaceAll) {
|
|
1019
|
+
const firstIdx = content.indexOf(oldString);
|
|
1020
|
+
const lastIdx = content.lastIndexOf(oldString);
|
|
1021
|
+
if (firstIdx !== lastIdx) {
|
|
1022
|
+
const occurrences = content.split(oldString).length - 1;
|
|
1023
|
+
const result2 = {
|
|
1024
|
+
success: false,
|
|
1025
|
+
output: "",
|
|
1026
|
+
error: `oldString is not unique in file (found ${occurrences} occurrences). Provide more context to make it unique, or use replaceAll:true.`
|
|
1027
|
+
};
|
|
1028
|
+
return JSON.stringify(result2);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const updated = replaceAll ? content.split(oldString).join(newString) : content.slice(0, content.indexOf(oldString)) + newString + content.slice(content.indexOf(oldString) + oldString.length);
|
|
1032
|
+
try {
|
|
1033
|
+
await writeFile2(filePath, updated, "utf8");
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
const result2 = {
|
|
1036
|
+
success: false,
|
|
1037
|
+
output: "",
|
|
1038
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1039
|
+
};
|
|
1040
|
+
return JSON.stringify(result2);
|
|
1041
|
+
}
|
|
1042
|
+
const count = replaceAll ? content.split(oldString).length - 1 : 1;
|
|
1043
|
+
const result = {
|
|
1044
|
+
success: true,
|
|
1045
|
+
output: `Replaced ${count} occurrence(s) in ${filePath}`
|
|
1046
|
+
};
|
|
1047
|
+
return JSON.stringify(result);
|
|
1048
|
+
}
|
|
1049
|
+
var editTool = createZodFunctionTool(
|
|
1050
|
+
"Edit",
|
|
1051
|
+
"Performs exact string replacements in files.\n\nYou must use the Read tool at least once before editing. When editing text from Read output, preserve the exact indentation.\n\nThe edit will FAIL if old_string is not unique in the file. Either provide more surrounding context to make it unique, or use replace_all to change every instance.\n\nALWAYS prefer editing existing files over creating new ones.",
|
|
1052
|
+
EditSchema,
|
|
1053
|
+
async (params) => {
|
|
1054
|
+
return editFileTool(params);
|
|
1055
|
+
}
|
|
1056
|
+
);
|
|
1057
|
+
|
|
1058
|
+
// src/builtins/glob-tool.ts
|
|
1059
|
+
import { stat as stat2 } from "fs/promises";
|
|
1060
|
+
import { resolve } from "path";
|
|
1061
|
+
import fg from "fast-glob";
|
|
1062
|
+
import { z as z5 } from "zod";
|
|
1063
|
+
var DEFAULT_MAX_RESULTS = 1e3;
|
|
1064
|
+
var GlobSchema = z5.object({
|
|
1065
|
+
pattern: z5.string().describe('The glob pattern to match files against (e.g. "**/*.ts", "src/**/*.tsx")'),
|
|
1066
|
+
path: z5.string().optional().describe(
|
|
1067
|
+
"The directory to search in. Defaults to the current working directory. Must be a valid directory path if provided"
|
|
1068
|
+
),
|
|
1069
|
+
limit: z5.number().optional().describe(
|
|
1070
|
+
"Maximum number of results to return (default: 1000). Use a smaller limit to save context space"
|
|
1071
|
+
)
|
|
1072
|
+
});
|
|
1073
|
+
async function globFileTool(args) {
|
|
1074
|
+
const { pattern, path: basePath } = args;
|
|
1075
|
+
const cwd = basePath ? resolve(basePath) : process.cwd();
|
|
1076
|
+
let matches;
|
|
1077
|
+
try {
|
|
1078
|
+
matches = await fg(pattern, {
|
|
1079
|
+
cwd,
|
|
1080
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
1081
|
+
dot: true,
|
|
1082
|
+
absolute: false
|
|
1083
|
+
});
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
const result2 = {
|
|
1086
|
+
success: false,
|
|
1087
|
+
output: "",
|
|
1088
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1089
|
+
};
|
|
1090
|
+
return JSON.stringify(result2);
|
|
1091
|
+
}
|
|
1092
|
+
const withMtime = await Promise.all(
|
|
1093
|
+
matches.map(async (p) => {
|
|
1094
|
+
const absPath = resolve(cwd, p);
|
|
1095
|
+
try {
|
|
1096
|
+
const s = await stat2(absPath);
|
|
1097
|
+
return { path: p, mtime: s.mtimeMs };
|
|
1098
|
+
} catch {
|
|
1099
|
+
return { path: p, mtime: 0 };
|
|
1100
|
+
}
|
|
1101
|
+
})
|
|
1102
|
+
);
|
|
1103
|
+
withMtime.sort((a, b) => b.mtime - a.mtime);
|
|
1104
|
+
const maxResults = args.limit ?? DEFAULT_MAX_RESULTS;
|
|
1105
|
+
const totalMatches = withMtime.length;
|
|
1106
|
+
const truncated = totalMatches > maxResults;
|
|
1107
|
+
const limited = truncated ? withMtime.slice(0, maxResults) : withMtime;
|
|
1108
|
+
const sorted = limited.map((f) => f.path);
|
|
1109
|
+
let output = sorted.length > 0 ? sorted.join("\n") : "(no matches)";
|
|
1110
|
+
if (truncated) {
|
|
1111
|
+
output += `
|
|
1112
|
+
|
|
1113
|
+
[Showing ${maxResults} of ${totalMatches} matches. Use limit parameter to see more.]`;
|
|
1114
|
+
}
|
|
1115
|
+
const result = {
|
|
1116
|
+
success: true,
|
|
1117
|
+
output
|
|
1118
|
+
};
|
|
1119
|
+
return JSON.stringify(result);
|
|
1120
|
+
}
|
|
1121
|
+
var globTool = createZodFunctionTool(
|
|
1122
|
+
"Glob",
|
|
1123
|
+
"Fast file pattern matching tool that works with any codebase size.\n\nSupports glob patterns like '**/*.js' or 'src/**/*.ts'. Returns matching file paths sorted by modification time.\n\nUse this tool when you need to find files by name patterns. When doing an open-ended search that may require multiple rounds, use the Agent tool instead.\n\nDefault limit is 1000 results. Use the limit parameter if you need fewer results to save context space.",
|
|
1124
|
+
GlobSchema,
|
|
1125
|
+
async (params) => {
|
|
1126
|
+
return globFileTool(params);
|
|
1127
|
+
}
|
|
1128
|
+
);
|
|
1129
|
+
|
|
1130
|
+
// src/builtins/grep-tool.ts
|
|
1131
|
+
import { readFile as readFile3, readdir, stat as stat3 } from "fs/promises";
|
|
1132
|
+
import { join, resolve as resolve2 } from "path";
|
|
1133
|
+
import { z as z6 } from "zod";
|
|
1134
|
+
var GrepSchema = z6.object({
|
|
1135
|
+
pattern: z6.string().describe("The regular expression pattern to search for in file contents"),
|
|
1136
|
+
path: z6.string().optional().describe("File or directory to search in. Defaults to the current working directory"),
|
|
1137
|
+
glob: z6.string().optional().describe(
|
|
1138
|
+
'Glob pattern to filter files (e.g. "*.ts", "*.{ts,tsx}"). Only files matching this pattern will be searched'
|
|
1139
|
+
),
|
|
1140
|
+
contextLines: z6.number().optional().describe(
|
|
1141
|
+
'Number of context lines to show before and after each match. Only applies when outputMode is "content". Default: 0'
|
|
1142
|
+
),
|
|
1143
|
+
outputMode: z6.enum(["files_with_matches", "content"]).optional().describe(
|
|
1144
|
+
'Output mode: "files_with_matches" shows only file paths (default), "content" shows matching lines with context'
|
|
1145
|
+
)
|
|
1146
|
+
});
|
|
1147
|
+
function globToRegex(glob) {
|
|
1148
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".+").replace(/\*/g, "[^/]*");
|
|
1149
|
+
return new RegExp(`^${escaped}$`);
|
|
1150
|
+
}
|
|
1151
|
+
function matchesGlob(filename, glob) {
|
|
1152
|
+
if (glob === void 0) return true;
|
|
1153
|
+
return globToRegex(glob).test(filename);
|
|
1154
|
+
}
|
|
1155
|
+
async function collectFiles(dirPath, glob) {
|
|
1156
|
+
const results = [];
|
|
1157
|
+
async function walk(current) {
|
|
1158
|
+
let entryNames;
|
|
1159
|
+
try {
|
|
1160
|
+
entryNames = await readdir(current);
|
|
1161
|
+
} catch {
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
for (const name of entryNames) {
|
|
1165
|
+
if (name === "node_modules" || name === ".git") continue;
|
|
1166
|
+
const fullPath = join(current, name);
|
|
1167
|
+
let fileStat;
|
|
1168
|
+
try {
|
|
1169
|
+
fileStat = await stat3(fullPath);
|
|
1170
|
+
} catch {
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
if (fileStat.isDirectory()) {
|
|
1174
|
+
await walk(fullPath);
|
|
1175
|
+
} else if (fileStat.isFile()) {
|
|
1176
|
+
if (matchesGlob(name, glob)) {
|
|
1177
|
+
results.push(fullPath);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
await walk(dirPath);
|
|
1183
|
+
return results;
|
|
1184
|
+
}
|
|
1185
|
+
function searchFile(content, filePath, regex, contextLines, outputMode) {
|
|
1186
|
+
const lines = content.split("\n");
|
|
1187
|
+
const matchingIndices = [];
|
|
1188
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1189
|
+
if (regex.test(lines[i])) {
|
|
1190
|
+
matchingIndices.push(i);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (matchingIndices.length === 0) return [];
|
|
1194
|
+
if (outputMode === "files_with_matches") {
|
|
1195
|
+
return [filePath];
|
|
1196
|
+
}
|
|
1197
|
+
const includedIndices = /* @__PURE__ */ new Set();
|
|
1198
|
+
for (const idx of matchingIndices) {
|
|
1199
|
+
for (let c = Math.max(0, idx - contextLines); c <= Math.min(lines.length - 1, idx + contextLines); c++) {
|
|
1200
|
+
includedIndices.add(c);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
const outputLines = [];
|
|
1204
|
+
const sortedIndices = Array.from(includedIndices).sort((a, b) => a - b);
|
|
1205
|
+
let prevIdx;
|
|
1206
|
+
for (const idx of sortedIndices) {
|
|
1207
|
+
if (prevIdx !== void 0 && idx > prevIdx + 1) {
|
|
1208
|
+
outputLines.push("--");
|
|
1209
|
+
}
|
|
1210
|
+
const lineNum = idx + 1;
|
|
1211
|
+
const marker = matchingIndices.includes(idx) ? ":" : "-";
|
|
1212
|
+
outputLines.push(`${filePath}:${lineNum}${marker}${lines[idx]}`);
|
|
1213
|
+
prevIdx = idx;
|
|
1214
|
+
}
|
|
1215
|
+
return outputLines;
|
|
1216
|
+
}
|
|
1217
|
+
async function grepFileTool(args) {
|
|
1218
|
+
const {
|
|
1219
|
+
pattern,
|
|
1220
|
+
path: searchPath,
|
|
1221
|
+
glob,
|
|
1222
|
+
contextLines = 0,
|
|
1223
|
+
outputMode = "files_with_matches"
|
|
1224
|
+
} = args;
|
|
1225
|
+
const targetPath = searchPath ? resolve2(searchPath) : process.cwd();
|
|
1226
|
+
let regex;
|
|
1227
|
+
try {
|
|
1228
|
+
regex = new RegExp(pattern);
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
const result2 = {
|
|
1231
|
+
success: false,
|
|
1232
|
+
output: "",
|
|
1233
|
+
error: `Invalid regex pattern: ${pattern}`
|
|
1234
|
+
};
|
|
1235
|
+
return JSON.stringify(result2);
|
|
1236
|
+
}
|
|
1237
|
+
let targetStat;
|
|
1238
|
+
try {
|
|
1239
|
+
targetStat = await stat3(targetPath);
|
|
1240
|
+
} catch {
|
|
1241
|
+
const result2 = {
|
|
1242
|
+
success: false,
|
|
1243
|
+
output: "",
|
|
1244
|
+
error: `Path not found: ${targetPath}`
|
|
1245
|
+
};
|
|
1246
|
+
return JSON.stringify(result2);
|
|
1247
|
+
}
|
|
1248
|
+
let files;
|
|
1249
|
+
if (targetStat.isFile()) {
|
|
1250
|
+
files = [targetPath];
|
|
1251
|
+
} else {
|
|
1252
|
+
files = await collectFiles(targetPath, glob);
|
|
1253
|
+
}
|
|
1254
|
+
const allOutputLines = [];
|
|
1255
|
+
for (const filePath of files) {
|
|
1256
|
+
let content;
|
|
1257
|
+
try {
|
|
1258
|
+
const buffer = await readFile3(filePath);
|
|
1259
|
+
const checkLen = Math.min(buffer.length, 8192);
|
|
1260
|
+
let hasBinary = false;
|
|
1261
|
+
for (let i = 0; i < checkLen; i++) {
|
|
1262
|
+
if (buffer[i] === 0) {
|
|
1263
|
+
hasBinary = true;
|
|
1264
|
+
break;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
if (hasBinary) continue;
|
|
1268
|
+
content = buffer.toString("utf8");
|
|
1269
|
+
} catch {
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
const fileMatches = searchFile(content, filePath, regex, contextLines, outputMode);
|
|
1273
|
+
allOutputLines.push(...fileMatches);
|
|
1274
|
+
}
|
|
1275
|
+
const result = {
|
|
1276
|
+
success: true,
|
|
1277
|
+
output: allOutputLines.length > 0 ? allOutputLines.join("\n") : "(no matches)"
|
|
1278
|
+
};
|
|
1279
|
+
return JSON.stringify(result);
|
|
1280
|
+
}
|
|
1281
|
+
var grepTool = createZodFunctionTool(
|
|
1282
|
+
"Grep",
|
|
1283
|
+
"A powerful search tool built on regex matching.\n\nSupports full regex syntax (e.g., 'log.*Error', 'function\\\\s+\\\\w+'). Filter files with glob parameter (e.g., '*.js', '**/*.tsx').\n\nOutput modes: 'content' shows matching lines with context, 'files_with_matches' shows only file paths (default), 'count' shows match counts.\n\nUse this tool for ALL search tasks. NEVER invoke grep or rg as a Bash command.\n\nUse head_limit to control result size and save context space.",
|
|
1284
|
+
GrepSchema,
|
|
1285
|
+
async (params) => {
|
|
1286
|
+
return grepFileTool(params);
|
|
1287
|
+
}
|
|
1288
|
+
);
|
|
1289
|
+
export {
|
|
1290
|
+
FunctionTool,
|
|
1291
|
+
OpenAPITool,
|
|
1292
|
+
ToolRegistry,
|
|
1293
|
+
bashTool,
|
|
1294
|
+
createFunctionTool,
|
|
1295
|
+
createOpenAPITool,
|
|
1296
|
+
createZodFunctionTool,
|
|
1297
|
+
editTool,
|
|
1298
|
+
globTool,
|
|
1299
|
+
grepTool,
|
|
1300
|
+
readTool,
|
|
1301
|
+
writeTool,
|
|
1302
|
+
zodToJsonSchema
|
|
1303
|
+
};
|