mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Custom tool calling (#10083)
This commit is contained in:
parent
78dc34498b
commit
5ae4d4d635
76 changed files with 3514 additions and 37 deletions
22
.roo/tools/__tests__/system-time.spec.ts
Normal file
22
.roo/tools/__tests__/system-time.spec.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { CustomToolContext, TaskLike } from "@roo-code/types"
|
||||
|
||||
import systemTime from "../system-time.js"
|
||||
|
||||
const mockContext: CustomToolContext = {
|
||||
mode: "code",
|
||||
task: { taskId: "test-task-id" } as unknown as TaskLike,
|
||||
}
|
||||
|
||||
describe("system-time tool", () => {
|
||||
describe("execute", () => {
|
||||
it("should return a formatted date/time string", async () => {
|
||||
const result = await systemTime.execute({}, mockContext)
|
||||
expect(result).toMatch(/^The current date and time is:/)
|
||||
expect(result).toMatch(/(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)/)
|
||||
expect(result).toMatch(
|
||||
/(January|February|March|April|May|June|July|August|September|October|November|December)/,
|
||||
)
|
||||
expect(result).toMatch(/\d{1,2}:\d{2}:\d{2}/)
|
||||
})
|
||||
})
|
||||
})
|
||||
4
.roo/tools/eslint.config.mjs
Normal file
4
.roo/tools/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { config } from "@roo-code/config-eslint/base"
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default [...config]
|
||||
18
.roo/tools/package.json
Normal file
18
.roo/tools/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "@roo-code/custom-tools",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Custom tools for the Roo Code project itself",
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext=ts --max-warnings=0",
|
||||
"check-types": "tsc --noEmit",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@roo-code/config-eslint": "workspace:^",
|
||||
"@roo-code/config-typescript": "workspace:^",
|
||||
"@roo-code/types": "workspace:^",
|
||||
"vitest": "^3.2.3"
|
||||
}
|
||||
}
|
||||
22
.roo/tools/system-time.ts
Normal file
22
.roo/tools/system-time.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { parametersSchema, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
export default defineCustomTool({
|
||||
name: "system_time",
|
||||
description: "Returns the current system date and time in a friendly, human-readable format.",
|
||||
parameters: parametersSchema.object({}),
|
||||
async execute() {
|
||||
const systemTime = new Date().toLocaleString("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
timeZoneName: "short",
|
||||
timeZone: "America/Los_Angeles",
|
||||
})
|
||||
|
||||
return `The current date and time is: ${systemTime}`
|
||||
},
|
||||
})
|
||||
9
.roo/tools/tsconfig.json
Normal file
9
.roo/tools/tsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "@roo-code/config-typescript/base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"types": ["vitest/globals"]
|
||||
},
|
||||
"include": ["*.ts", "__tests__/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
9
.roo/tools/vitest.config.ts
Normal file
9
.roo/tools/vitest.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { defineConfig } from "vitest/config"
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
watch: false,
|
||||
},
|
||||
})
|
||||
|
|
@ -158,6 +158,58 @@ export function copyWasms(srcDir: string, distDir: string): void {
|
|||
})
|
||||
|
||||
console.log(`[copyWasms] Copied ${wasmFiles.length} tree-sitter language wasms to ${distDir}`)
|
||||
|
||||
// Copy esbuild-wasm files for custom tool transpilation (cross-platform).
|
||||
copyEsbuildWasmFiles(nodeModulesDir, distDir)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy esbuild-wasm files to the dist/bin directory.
|
||||
*
|
||||
* This function copies the esbuild-wasm CLI and WASM binary, which provides
|
||||
* a cross-platform esbuild implementation that works on all platforms.
|
||||
*
|
||||
* Files copied:
|
||||
* - bin/esbuild (Node.js CLI script)
|
||||
* - esbuild.wasm (WASM binary)
|
||||
* - wasm_exec_node.js (Go WASM runtime for Node.js)
|
||||
* - wasm_exec.js (Go WASM runtime dependency)
|
||||
*/
|
||||
function copyEsbuildWasmFiles(nodeModulesDir: string, distDir: string): void {
|
||||
const esbuildWasmDir = path.join(nodeModulesDir, "esbuild-wasm")
|
||||
|
||||
if (!fs.existsSync(esbuildWasmDir)) {
|
||||
throw new Error(`Directory does not exist: ${esbuildWasmDir}`)
|
||||
}
|
||||
|
||||
// Create bin directory in dist.
|
||||
const binDir = path.join(distDir, "bin")
|
||||
fs.mkdirSync(binDir, { recursive: true })
|
||||
|
||||
// Files to copy - the esbuild CLI script expects wasm_exec_node.js and esbuild.wasm
|
||||
// to be one directory level up from the bin directory (i.e., in distDir directly).
|
||||
// wasm_exec_node.js requires wasm_exec.js, so we need to copy that too.
|
||||
const filesToCopy = [
|
||||
{ src: path.join(esbuildWasmDir, "bin", "esbuild"), dest: path.join(binDir, "esbuild") },
|
||||
{ src: path.join(esbuildWasmDir, "esbuild.wasm"), dest: path.join(distDir, "esbuild.wasm") },
|
||||
{ src: path.join(esbuildWasmDir, "wasm_exec_node.js"), dest: path.join(distDir, "wasm_exec_node.js") },
|
||||
{ src: path.join(esbuildWasmDir, "wasm_exec.js"), dest: path.join(distDir, "wasm_exec.js") },
|
||||
]
|
||||
|
||||
for (const { src, dest } of filesToCopy) {
|
||||
fs.copyFileSync(src, dest)
|
||||
|
||||
// Make CLI executable.
|
||||
if (src.endsWith("esbuild")) {
|
||||
try {
|
||||
fs.chmodSync(dest, 0o755)
|
||||
} catch {
|
||||
// Ignore chmod errors on Windows.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[copyWasms] Copied ${filesToCopy.length} esbuild-wasm files to ${distDir}`)
|
||||
}
|
||||
|
||||
export function copyLocales(srcDir: string, distDir: string): void {
|
||||
|
|
|
|||
4
packages/core/eslint.config.mjs
Normal file
4
packages/core/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { config } from "@roo-code/config-eslint/base"
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default [...config]
|
||||
26
packages/core/package.json
Normal file
26
packages/core/package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "@roo-code/core",
|
||||
"description": "Platform agnostic core functionality for Roo Code.",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"exports": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint src --ext=ts --max-warnings=0",
|
||||
"check-types": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"clean": "rimraf .turbo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@roo-code/types": "workspace:^",
|
||||
"esbuild": "^0.25.0",
|
||||
"execa": "^9.5.2",
|
||||
"openai": "^5.12.2",
|
||||
"zod": "^3.25.61"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@roo-code/config-eslint": "workspace:^",
|
||||
"@roo-code/config-typescript": "workspace:^",
|
||||
"@types/node": "^24.1.0",
|
||||
"vitest": "^3.2.3"
|
||||
}
|
||||
}
|
||||
231
packages/core/src/custom-tools/__tests__/__snapshots__/format-native.spec.ts.snap
generated
Normal file
231
packages/core/src/custom-tools/__tests__/__snapshots__/format-native.spec.ts.snap
generated
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Native Protocol snapshots > should generate correct native definition for cached tool 1`] = `
|
||||
{
|
||||
"function": {
|
||||
"description": "Cached tool",
|
||||
"name": "cached",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Native Protocol snapshots > should generate correct native definition for legacy tool (using args) 1`] = `
|
||||
{
|
||||
"function": {
|
||||
"description": "Legacy tool using args",
|
||||
"name": "legacy",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"input": {
|
||||
"description": "The input string",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Native Protocol snapshots > should generate correct native definition for mixed export tool 1`] = `
|
||||
{
|
||||
"function": {
|
||||
"description": "Valid",
|
||||
"name": "mixed_validTool",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Native Protocol snapshots > should generate correct native definition for simple tool 1`] = `
|
||||
{
|
||||
"function": {
|
||||
"description": "Simple tool",
|
||||
"name": "simple",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": {
|
||||
"description": "The input value",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"value",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Native Protocol snapshots > should generate correct native definitions for all fixtures combined 1`] = `
|
||||
[
|
||||
{
|
||||
"function": {
|
||||
"description": "Simple tool",
|
||||
"name": "simple",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": {
|
||||
"description": "The input value",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"value",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"description": "Cached tool",
|
||||
"name": "cached",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"description": "Legacy tool using args",
|
||||
"name": "legacy",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"input": {
|
||||
"description": "The input string",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"description": "Tool A",
|
||||
"name": "multi_toolA",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"description": "Tool B",
|
||||
"name": "multi_toolB",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"description": "Valid",
|
||||
"name": "mixed_validTool",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Native Protocol snapshots > should generate correct native definitions for multi export tools 1`] = `
|
||||
[
|
||||
{
|
||||
"function": {
|
||||
"description": "Tool A",
|
||||
"name": "multi_toolA",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"description": "Tool B",
|
||||
"name": "multi_toolB",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
},
|
||||
]
|
||||
`;
|
||||
129
packages/core/src/custom-tools/__tests__/__snapshots__/format-xml.spec.ts.snap
generated
Normal file
129
packages/core/src/custom-tools/__tests__/__snapshots__/format-xml.spec.ts.snap
generated
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for all fixtures combined 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## simple
|
||||
Description: Simple tool
|
||||
Parameters:
|
||||
- value: (required) The input value (type: string)
|
||||
Usage:
|
||||
<simple>
|
||||
<value>value value here</value>
|
||||
</simple>
|
||||
|
||||
## cached
|
||||
Description: Cached tool
|
||||
Parameters:
|
||||
Usage:
|
||||
<cached>
|
||||
</cached>
|
||||
|
||||
## legacy
|
||||
Description: Legacy tool using args
|
||||
Parameters:
|
||||
- input: (required) The input string (type: string)
|
||||
Usage:
|
||||
<legacy>
|
||||
<input>input value here</input>
|
||||
</legacy>
|
||||
|
||||
## multi_toolA
|
||||
Description: Tool A
|
||||
Parameters:
|
||||
Usage:
|
||||
<multi_toolA>
|
||||
</multi_toolA>
|
||||
|
||||
## multi_toolB
|
||||
Description: Tool B
|
||||
Parameters:
|
||||
Usage:
|
||||
<multi_toolB>
|
||||
</multi_toolB>
|
||||
|
||||
## mixed_validTool
|
||||
Description: Valid
|
||||
Parameters:
|
||||
Usage:
|
||||
<mixed_validTool>
|
||||
</mixed_validTool>"
|
||||
`;
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for cached tool 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## cached
|
||||
Description: Cached tool
|
||||
Parameters:
|
||||
Usage:
|
||||
<cached>
|
||||
</cached>"
|
||||
`;
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for legacy tool (using args) 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## legacy
|
||||
Description: Legacy tool using args
|
||||
Parameters:
|
||||
- input: (required) The input string (type: string)
|
||||
Usage:
|
||||
<legacy>
|
||||
<input>input value here</input>
|
||||
</legacy>"
|
||||
`;
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for mixed export tool 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## mixed_validTool
|
||||
Description: Valid
|
||||
Parameters:
|
||||
Usage:
|
||||
<mixed_validTool>
|
||||
</mixed_validTool>"
|
||||
`;
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for multi export tools 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## multi_toolA
|
||||
Description: Tool A
|
||||
Parameters:
|
||||
Usage:
|
||||
<multi_toolA>
|
||||
</multi_toolA>
|
||||
|
||||
## multi_toolB
|
||||
Description: Tool B
|
||||
Parameters:
|
||||
Usage:
|
||||
<multi_toolB>
|
||||
</multi_toolB>"
|
||||
`;
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for simple tool 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## simple
|
||||
Description: Simple tool
|
||||
Parameters:
|
||||
- value: (required) The input value (type: string)
|
||||
Usage:
|
||||
<simple>
|
||||
<value>value value here</value>
|
||||
</simple>"
|
||||
`;
|
||||
146
packages/core/src/custom-tools/__tests__/__snapshots__/serialize.spec.ts.snap
generated
Normal file
146
packages/core/src/custom-tools/__tests__/__snapshots__/serialize.spec.ts.snap
generated
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Serialization snapshots > should correctly serialize all fixtures 1`] = `
|
||||
[
|
||||
{
|
||||
"description": "Simple tool",
|
||||
"name": "simple",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": {
|
||||
"description": "The input value",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"value",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
},
|
||||
{
|
||||
"description": "Cached tool",
|
||||
"name": "cached",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
},
|
||||
{
|
||||
"description": "Legacy tool using args",
|
||||
"name": "legacy",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"input": {
|
||||
"description": "The input string",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
},
|
||||
{
|
||||
"description": "Tool A",
|
||||
"name": "multi_toolA",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
},
|
||||
{
|
||||
"description": "Tool B",
|
||||
"name": "multi_toolB",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
},
|
||||
{
|
||||
"description": "Valid",
|
||||
"name": "mixed_validTool",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Serialization snapshots > should correctly serialize cached tool 1`] = `
|
||||
{
|
||||
"description": "Cached tool",
|
||||
"name": "cached",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Serialization snapshots > should correctly serialize legacy tool (using args) 1`] = `
|
||||
{
|
||||
"description": "Legacy tool using args",
|
||||
"name": "legacy",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"input": {
|
||||
"description": "The input string",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Serialization snapshots > should correctly serialize simple tool 1`] = `
|
||||
{
|
||||
"description": "Simple tool",
|
||||
"name": "simple",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": {
|
||||
"description": "The input value",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"value",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
}
|
||||
`;
|
||||
|
|
@ -0,0 +1,381 @@
|
|||
// pnpm --filter @roo-code/core test src/custom-tools/__tests__/custom-tool-registry.spec.ts
|
||||
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
import { type CustomToolDefinition, parametersSchema as z } from "@roo-code/types"
|
||||
|
||||
import { CustomToolRegistry } from "../custom-tool-registry.js"
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const TEST_FIXTURES_DIR = path.join(__dirname, "fixtures")
|
||||
const TEST_FIXTURES_OVERRIDE_DIR = path.join(__dirname, "fixtures-override")
|
||||
|
||||
describe("CustomToolRegistry", () => {
|
||||
let registry: CustomToolRegistry
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new CustomToolRegistry()
|
||||
})
|
||||
|
||||
describe("validation", () => {
|
||||
it("should accept a valid tool definition", () => {
|
||||
const validTool = {
|
||||
name: "valid_tool",
|
||||
description: "A valid tool",
|
||||
parameters: z.object({ name: z.string() }),
|
||||
execute: async () => "result",
|
||||
}
|
||||
|
||||
expect(() => registry.register(validTool)).not.toThrow()
|
||||
expect(registry.has("valid_tool")).toBe(true)
|
||||
})
|
||||
|
||||
it("should reject empty description", () => {
|
||||
const invalidTool = {
|
||||
name: "invalid_tool",
|
||||
description: "",
|
||||
parameters: z.object({}),
|
||||
execute: async () => "result",
|
||||
}
|
||||
|
||||
expect(() => registry.register(invalidTool as CustomToolDefinition)).toThrow(/Invalid tool definition/)
|
||||
})
|
||||
|
||||
it("should reject non-Zod parameters", () => {
|
||||
const invalidTool = {
|
||||
name: "bad_params_tool",
|
||||
description: "Tool with bad params",
|
||||
parameters: { foo: "bar" },
|
||||
execute: async () => "result",
|
||||
}
|
||||
|
||||
expect(() => registry.register(invalidTool as unknown as CustomToolDefinition)).toThrow(
|
||||
/Invalid tool definition/,
|
||||
)
|
||||
})
|
||||
|
||||
it("should allow missing parameters", () => {
|
||||
const toolWithoutParams = {
|
||||
name: "no_params_tool",
|
||||
description: "Tool without parameters",
|
||||
execute: async () => "result",
|
||||
}
|
||||
|
||||
expect(() => registry.register(toolWithoutParams)).not.toThrow()
|
||||
expect(registry.has("no_params_tool")).toBe(true)
|
||||
})
|
||||
|
||||
it("should reject empty name", () => {
|
||||
const invalidTool = {
|
||||
name: "",
|
||||
description: "Tool with empty name",
|
||||
execute: async () => "result",
|
||||
}
|
||||
|
||||
expect(() => registry.register(invalidTool as CustomToolDefinition)).toThrow(/Invalid tool definition/)
|
||||
})
|
||||
|
||||
it("should reject missing name", () => {
|
||||
const invalidTool = {
|
||||
description: "Tool without name",
|
||||
execute: async () => "result",
|
||||
}
|
||||
|
||||
expect(() => registry.register(invalidTool as unknown as CustomToolDefinition)).toThrow(
|
||||
/Invalid tool definition/,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("register", () => {
|
||||
it("should register a valid tool", () => {
|
||||
const tool: CustomToolDefinition = {
|
||||
name: "test_tool",
|
||||
description: "Test tool",
|
||||
parameters: z.object({ input: z.string() }),
|
||||
execute: async (args: { input: string }) => `Processed: ${args.input}`,
|
||||
}
|
||||
|
||||
registry.register(tool)
|
||||
|
||||
expect(registry.has("test_tool")).toBe(true)
|
||||
expect(registry.size).toBe(1)
|
||||
})
|
||||
|
||||
it("should throw for invalid tool definition", () => {
|
||||
const invalidTool = {
|
||||
name: "bad_tool",
|
||||
description: "",
|
||||
execute: async () => "result",
|
||||
}
|
||||
|
||||
expect(() => registry.register(invalidTool as CustomToolDefinition)).toThrow(/Invalid tool definition/)
|
||||
})
|
||||
|
||||
it("should overwrite existing tool with same id", () => {
|
||||
const tool1: CustomToolDefinition = {
|
||||
name: "tool",
|
||||
description: "First version",
|
||||
execute: async () => "v1",
|
||||
}
|
||||
|
||||
const tool2: CustomToolDefinition = {
|
||||
name: "tool",
|
||||
description: "Second version",
|
||||
execute: async () => "v2",
|
||||
}
|
||||
|
||||
registry.register(tool1)
|
||||
registry.register(tool2)
|
||||
|
||||
expect(registry.size).toBe(1)
|
||||
expect(registry.get("tool")?.description).toBe("Second version")
|
||||
})
|
||||
})
|
||||
|
||||
describe("unregister", () => {
|
||||
it("should remove a registered tool", () => {
|
||||
registry.register({
|
||||
name: "tool",
|
||||
description: "Test",
|
||||
execute: async () => "result",
|
||||
})
|
||||
|
||||
const result = registry.unregister("tool")
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(registry.has("tool")).toBe(false)
|
||||
})
|
||||
|
||||
it("should return false for non-existent tool", () => {
|
||||
const result = registry.unregister("nonexistent")
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("get", () => {
|
||||
it("should return registered tool", () => {
|
||||
registry.register({
|
||||
name: "my_tool",
|
||||
description: "My tool",
|
||||
execute: async () => "result",
|
||||
})
|
||||
|
||||
const tool = registry.get("my_tool")
|
||||
|
||||
expect(tool).toBeDefined()
|
||||
expect(tool?.name).toBe("my_tool")
|
||||
expect(tool?.description).toBe("My tool")
|
||||
})
|
||||
|
||||
it("should return undefined for non-existent tool", () => {
|
||||
expect(registry.get("nonexistent")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("list", () => {
|
||||
it("should return all tool IDs", () => {
|
||||
registry.register({ name: "tool_a", description: "A", execute: async () => "a" })
|
||||
registry.register({ name: "tool_b", description: "B", execute: async () => "b" })
|
||||
registry.register({ name: "tool_c", description: "C", execute: async () => "c" })
|
||||
|
||||
const ids = registry.list()
|
||||
|
||||
expect(ids).toHaveLength(3)
|
||||
expect(ids).toContain("tool_a")
|
||||
expect(ids).toContain("tool_b")
|
||||
expect(ids).toContain("tool_c")
|
||||
})
|
||||
|
||||
it("should return empty array when no tools registered", () => {
|
||||
expect(registry.list()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("getAll", () => {
|
||||
it("should return all tools as array", () => {
|
||||
registry.register({ name: "tool1", description: "Tool 1", execute: async () => "1" })
|
||||
registry.register({ name: "tool2", description: "Tool 2", execute: async () => "2" })
|
||||
|
||||
const all = registry.getAll()
|
||||
|
||||
expect(all).toHaveLength(2)
|
||||
expect(all.find((t) => t.name === "tool1")?.description).toBe("Tool 1")
|
||||
expect(all.find((t) => t.name === "tool2")?.description).toBe("Tool 2")
|
||||
})
|
||||
})
|
||||
|
||||
describe("clear", () => {
|
||||
it("should remove all registered tools", () => {
|
||||
registry.register({ name: "tool1", description: "1", execute: async () => "1" })
|
||||
registry.register({ name: "tool2", description: "2", execute: async () => "2" })
|
||||
|
||||
expect(registry.size).toBe(2)
|
||||
|
||||
registry.clear()
|
||||
|
||||
expect(registry.size).toBe(0)
|
||||
expect(registry.list()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe.sequential("loadFromDirectory", () => {
|
||||
it("should load tools from TypeScript files", async () => {
|
||||
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)
|
||||
|
||||
expect(result.loaded).toContain("simple")
|
||||
expect(registry.has("simple")).toBe(true)
|
||||
}, 60000)
|
||||
|
||||
it("should handle named exports", async () => {
|
||||
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)
|
||||
|
||||
expect(result.loaded).toContain("multi_toolA")
|
||||
expect(result.loaded).toContain("multi_toolB")
|
||||
}, 30000)
|
||||
|
||||
it("should report validation failures", async () => {
|
||||
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)
|
||||
|
||||
const invalidFailure = result.failed.find((f) => f.file === "invalid.ts")
|
||||
expect(invalidFailure).toBeDefined()
|
||||
expect(invalidFailure?.error).toContain("Invalid tool definition")
|
||||
}, 30000)
|
||||
|
||||
it("should return empty results for non-existent directory", async () => {
|
||||
const result = await registry.loadFromDirectory("/nonexistent/path")
|
||||
|
||||
expect(result.loaded).toHaveLength(0)
|
||||
expect(result.failed).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should skip non-tool exports silently", async () => {
|
||||
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)
|
||||
|
||||
expect(result.loaded).toContain("mixed_validTool")
|
||||
// The non-tool exports should not appear in loaded or failed.
|
||||
expect(result.loaded).not.toContain("mixed_someString")
|
||||
expect(result.loaded).not.toContain("mixed_someNumber")
|
||||
expect(result.loaded).not.toContain("mixed_someObject")
|
||||
}, 30000)
|
||||
|
||||
it("should support args as alias for parameters", async () => {
|
||||
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)
|
||||
|
||||
expect(result.loaded).toContain("legacy")
|
||||
|
||||
const tool = registry.get("legacy")
|
||||
expect(tool?.parameters).toBeDefined()
|
||||
}, 30000)
|
||||
})
|
||||
|
||||
describe.sequential("clearCache", () => {
|
||||
it("should clear the TypeScript compilation cache", async () => {
|
||||
await registry.loadFromDirectory(TEST_FIXTURES_DIR)
|
||||
registry.clearCache()
|
||||
|
||||
// Should be able to load again without issues.
|
||||
registry.clear()
|
||||
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)
|
||||
|
||||
expect(result.loaded).toContain("cached")
|
||||
}, 30000)
|
||||
})
|
||||
|
||||
describe.sequential("loadFromDirectories", () => {
|
||||
it("should load tools from multiple directories", async () => {
|
||||
const result = await registry.loadFromDirectories([TEST_FIXTURES_DIR, TEST_FIXTURES_OVERRIDE_DIR])
|
||||
|
||||
// Should load tools from both directories.
|
||||
expect(result.loaded).toContain("simple") // From both directories (override wins).
|
||||
expect(result.loaded).toContain("unique_override") // Only in override directory.
|
||||
expect(result.loaded).toContain("multi_toolA") // Only in fixtures directory.
|
||||
}, 60000)
|
||||
|
||||
it("should allow later directories to override earlier ones", async () => {
|
||||
await registry.loadFromDirectories([TEST_FIXTURES_DIR, TEST_FIXTURES_OVERRIDE_DIR])
|
||||
|
||||
// The simple tool should have the overridden description.
|
||||
const simpleTool = registry.get("simple")
|
||||
expect(simpleTool).toBeDefined()
|
||||
expect(simpleTool?.description).toBe("Simple tool - OVERRIDDEN")
|
||||
}, 60000)
|
||||
|
||||
it("should preserve order: first directory loaded first, second overrides", async () => {
|
||||
// Load in reverse order: override first, then fixtures.
|
||||
await registry.loadFromDirectories([TEST_FIXTURES_OVERRIDE_DIR, TEST_FIXTURES_DIR])
|
||||
|
||||
// Now the original fixtures directory should win.
|
||||
const simpleTool = registry.get("simple")
|
||||
expect(simpleTool).toBeDefined()
|
||||
expect(simpleTool?.description).toBe("Simple tool") // Original wins when loaded second.
|
||||
}, 60000)
|
||||
|
||||
it("should handle non-existent directories in the array", async () => {
|
||||
const result = await registry.loadFromDirectories([
|
||||
"/nonexistent/path",
|
||||
TEST_FIXTURES_DIR,
|
||||
"/another/nonexistent",
|
||||
])
|
||||
|
||||
// Should still load from the existing directory.
|
||||
expect(result.loaded).toContain("simple")
|
||||
expect(result.failed).toHaveLength(1) // Only the invalid.ts from fixtures.
|
||||
}, 60000)
|
||||
|
||||
it("should handle empty array", async () => {
|
||||
const result = await registry.loadFromDirectories([])
|
||||
|
||||
expect(result.loaded).toHaveLength(0)
|
||||
expect(result.failed).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should combine results from all directories", async () => {
|
||||
const result = await registry.loadFromDirectories([TEST_FIXTURES_DIR, TEST_FIXTURES_OVERRIDE_DIR])
|
||||
|
||||
// Loaded should include tools from both (with duplicates since simple is loaded twice).
|
||||
// The "simple" tool is loaded from both directories.
|
||||
const simpleCount = result.loaded.filter((name) => name === "simple").length
|
||||
expect(simpleCount).toBe(2) // Listed twice in loaded results.
|
||||
}, 60000)
|
||||
})
|
||||
|
||||
describe.sequential("loadFromDirectoriesIfStale", () => {
|
||||
it("should load tools from multiple directories when stale", async () => {
|
||||
const result = await registry.loadFromDirectoriesIfStale([TEST_FIXTURES_DIR, TEST_FIXTURES_OVERRIDE_DIR])
|
||||
|
||||
expect(result.loaded).toContain("simple")
|
||||
expect(result.loaded).toContain("unique_override")
|
||||
}, 60000)
|
||||
|
||||
it("should not reload if directories are not stale", async () => {
|
||||
// First load.
|
||||
await registry.loadFromDirectoriesIfStale([TEST_FIXTURES_DIR])
|
||||
|
||||
// Clear tools but keep staleness tracking.
|
||||
// (firstLoadSize is captured to document that tools were loaded, then cleared).
|
||||
const _firstLoadSize = registry.size
|
||||
registry.clear()
|
||||
|
||||
// Second load - should return cached tool names without reloading.
|
||||
const result = await registry.loadFromDirectoriesIfStale([TEST_FIXTURES_DIR])
|
||||
|
||||
// Registry was cleared, not stale so no reload.
|
||||
expect(result.loaded).toEqual([])
|
||||
}, 30000)
|
||||
|
||||
it("should handle mixed stale and non-stale directories", async () => {
|
||||
// Load from fixtures first.
|
||||
await registry.loadFromDirectoriesIfStale([TEST_FIXTURES_DIR])
|
||||
|
||||
// Load from both - fixtures is not stale, override is new (stale).
|
||||
const result = await registry.loadFromDirectoriesIfStale([TEST_FIXTURES_DIR, TEST_FIXTURES_OVERRIDE_DIR])
|
||||
|
||||
// Override directory tools should be loaded (it's stale/new).
|
||||
expect(result.loaded).toContain("simple") // From override (stale).
|
||||
expect(result.loaded).toContain("unique_override") // From override (stale).
|
||||
}, 60000)
|
||||
})
|
||||
})
|
||||
156
packages/core/src/custom-tools/__tests__/esbuild-runner.spec.ts
Normal file
156
packages/core/src/custom-tools/__tests__/esbuild-runner.spec.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import fs from "fs"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
|
||||
import { getEsbuildScriptPath, runEsbuild } from "../esbuild-runner.js"
|
||||
|
||||
describe("getEsbuildScriptPath", () => {
|
||||
it("should find esbuild-wasm script in node_modules in development", () => {
|
||||
const scriptPath = getEsbuildScriptPath()
|
||||
|
||||
// Should find the script.
|
||||
expect(typeof scriptPath).toBe("string")
|
||||
expect(scriptPath.length).toBeGreaterThan(0)
|
||||
|
||||
// The script should exist.
|
||||
expect(fs.existsSync(scriptPath)).toBe(true)
|
||||
|
||||
// Should be the esbuild script (not a binary).
|
||||
expect(scriptPath).toMatch(/esbuild$/)
|
||||
})
|
||||
|
||||
it("should prefer production path when extensionPath is provided and script exists", () => {
|
||||
// Create a temporary directory with a fake script.
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "esbuild-runner-test-"))
|
||||
const binDir = path.join(tempDir, "dist", "bin")
|
||||
fs.mkdirSync(binDir, { recursive: true })
|
||||
|
||||
const fakeScriptPath = path.join(binDir, "esbuild")
|
||||
fs.writeFileSync(fakeScriptPath, "#!/usr/bin/env node\nconsole.log('fake esbuild')")
|
||||
|
||||
try {
|
||||
const result = getEsbuildScriptPath(tempDir)
|
||||
expect(result).toBe(fakeScriptPath)
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it("should fall back to node_modules when production script does not exist", () => {
|
||||
// Pass a non-existent extension path.
|
||||
const result = getEsbuildScriptPath("/nonexistent/extension/path")
|
||||
|
||||
// Should fall back to development path.
|
||||
expect(typeof result).toBe("string")
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(fs.existsSync(result)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("runEsbuild", () => {
|
||||
let tempDir: string
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "esbuild-runner-test-"))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it("should compile a TypeScript file to ESM", async () => {
|
||||
// Create a simple TypeScript file.
|
||||
const inputFile = path.join(tempDir, "input.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
fs.writeFileSync(
|
||||
inputFile,
|
||||
`
|
||||
export const greeting = "Hello, World!"
|
||||
export function add(a: number, b: number): number {
|
||||
return a + b
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
await runEsbuild({
|
||||
entryPoint: inputFile,
|
||||
outfile: outputFile,
|
||||
format: "esm",
|
||||
platform: "node",
|
||||
target: "node18",
|
||||
bundle: true,
|
||||
})
|
||||
|
||||
// Verify output file exists.
|
||||
expect(fs.existsSync(outputFile)).toBe(true)
|
||||
|
||||
// Verify output content is valid JavaScript.
|
||||
const outputContent = fs.readFileSync(outputFile, "utf-8")
|
||||
expect(outputContent).toContain("Hello, World!")
|
||||
expect(outputContent).toContain("add")
|
||||
}, 30000)
|
||||
|
||||
it("should generate inline source maps when specified", async () => {
|
||||
const inputFile = path.join(tempDir, "input.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
fs.writeFileSync(inputFile, `export const value = 42`)
|
||||
|
||||
await runEsbuild({ entryPoint: inputFile, outfile: outputFile, format: "esm", sourcemap: "inline" })
|
||||
|
||||
const outputContent = fs.readFileSync(outputFile, "utf-8")
|
||||
expect(outputContent).toContain("sourceMappingURL=data:")
|
||||
}, 30000)
|
||||
|
||||
it("should throw an error for invalid TypeScript", async () => {
|
||||
const inputFile = path.join(tempDir, "invalid.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
// Write syntactically invalid TypeScript.
|
||||
fs.writeFileSync(inputFile, `export const value = {{{ invalid syntax`)
|
||||
|
||||
await expect(runEsbuild({ entryPoint: inputFile, outfile: outputFile, format: "esm" })).rejects.toThrow()
|
||||
}, 30000)
|
||||
|
||||
it("should throw an error for non-existent file", async () => {
|
||||
const nonExistentFile = path.join(tempDir, "does-not-exist.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
await expect(runEsbuild({ entryPoint: nonExistentFile, outfile: outputFile, format: "esm" })).rejects.toThrow()
|
||||
}, 30000)
|
||||
|
||||
it("should bundle dependencies when bundle option is true", async () => {
|
||||
// Create two files where one imports the other.
|
||||
const libFile = path.join(tempDir, "lib.ts")
|
||||
const mainFile = path.join(tempDir, "main.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
fs.writeFileSync(libFile, `export const PI = 3.14159`)
|
||||
fs.writeFileSync(
|
||||
mainFile,
|
||||
`
|
||||
import { PI } from "./lib.js"
|
||||
export const circumference = (r: number) => 2 * PI * r
|
||||
`,
|
||||
)
|
||||
|
||||
await runEsbuild({ entryPoint: mainFile, outfile: outputFile, format: "esm", bundle: true })
|
||||
|
||||
const outputContent = fs.readFileSync(outputFile, "utf-8")
|
||||
// The PI constant should be bundled inline.
|
||||
expect(outputContent).toContain("3.14159")
|
||||
}, 30000)
|
||||
|
||||
it("should respect platform option", async () => {
|
||||
const inputFile = path.join(tempDir, "input.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
fs.writeFileSync(inputFile, `export const value = process.env.NODE_ENV`)
|
||||
|
||||
await runEsbuild({ entryPoint: inputFile, outfile: outputFile, format: "esm", platform: "node" })
|
||||
|
||||
// File should be created successfully.
|
||||
expect(fs.existsSync(outputFile)).toBe(true)
|
||||
}, 30000)
|
||||
})
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { parametersSchema, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
// This tool has the same name as the one in fixtures/ to test override behavior.
|
||||
export default defineCustomTool({
|
||||
name: "simple",
|
||||
description: "Simple tool - OVERRIDDEN",
|
||||
parameters: parametersSchema.object({ value: parametersSchema.string().describe("The input value") }),
|
||||
async execute(args: { value: string }) {
|
||||
return "Overridden Result: " + args.value
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { parametersSchema, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
// This tool only exists in fixtures-override/ to test combined loading.
|
||||
export default defineCustomTool({
|
||||
name: "unique_override",
|
||||
description: "A unique tool only in override directory",
|
||||
parameters: parametersSchema.object({ input: parametersSchema.string().describe("The input") }),
|
||||
async execute(args: { input: string }) {
|
||||
return "Unique: " + args.input
|
||||
},
|
||||
})
|
||||
10
packages/core/src/custom-tools/__tests__/fixtures/cached.ts
Normal file
10
packages/core/src/custom-tools/__tests__/fixtures/cached.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { parametersSchema, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
export default defineCustomTool({
|
||||
name: "cached",
|
||||
description: "Cached tool",
|
||||
parameters: parametersSchema.object({}),
|
||||
async execute() {
|
||||
return "cached"
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
description: "", // Invalid: empty description.
|
||||
execute: async () => "result",
|
||||
}
|
||||
10
packages/core/src/custom-tools/__tests__/fixtures/legacy.ts
Normal file
10
packages/core/src/custom-tools/__tests__/fixtures/legacy.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { parametersSchema, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
export default defineCustomTool({
|
||||
name: "legacy",
|
||||
description: "Legacy tool using args",
|
||||
parameters: parametersSchema.object({ input: parametersSchema.string().describe("The input string") }),
|
||||
async execute(args: { input: string }) {
|
||||
return args.input
|
||||
},
|
||||
})
|
||||
16
packages/core/src/custom-tools/__tests__/fixtures/mixed.ts
Normal file
16
packages/core/src/custom-tools/__tests__/fixtures/mixed.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { parametersSchema, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
// This is a valid tool.
|
||||
export const validTool = defineCustomTool({
|
||||
name: "mixed_validTool",
|
||||
description: "Valid",
|
||||
parameters: parametersSchema.object({}),
|
||||
async execute() {
|
||||
return "valid"
|
||||
},
|
||||
})
|
||||
|
||||
// These should be silently skipped.
|
||||
export const someString = "not a tool"
|
||||
export const someNumber = 42
|
||||
export const someObject = { foo: "bar" }
|
||||
19
packages/core/src/custom-tools/__tests__/fixtures/multi.ts
Normal file
19
packages/core/src/custom-tools/__tests__/fixtures/multi.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { parametersSchema, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
export const toolA = defineCustomTool({
|
||||
name: "multi_toolA",
|
||||
description: "Tool A",
|
||||
parameters: parametersSchema.object({}),
|
||||
async execute() {
|
||||
return "A"
|
||||
},
|
||||
})
|
||||
|
||||
export const toolB = defineCustomTool({
|
||||
name: "multi_toolB",
|
||||
description: "Tool B",
|
||||
parameters: parametersSchema.object({}),
|
||||
async execute() {
|
||||
return "B"
|
||||
},
|
||||
})
|
||||
10
packages/core/src/custom-tools/__tests__/fixtures/simple.ts
Normal file
10
packages/core/src/custom-tools/__tests__/fixtures/simple.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { parametersSchema, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
export default defineCustomTool({
|
||||
name: "simple",
|
||||
description: "Simple tool",
|
||||
parameters: parametersSchema.object({ value: parametersSchema.string().describe("The input value") }),
|
||||
async execute(args: { value: string }) {
|
||||
return "Result: " + args.value
|
||||
},
|
||||
})
|
||||
250
packages/core/src/custom-tools/__tests__/format-native.spec.ts
Normal file
250
packages/core/src/custom-tools/__tests__/format-native.spec.ts
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
// pnpm --filter @roo-code/core test src/custom-tools/__tests__/format-native.spec.ts
|
||||
|
||||
import { type SerializedCustomToolDefinition, parametersSchema as z, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
import { serializeCustomTool, serializeCustomTools } from "../serialize.js"
|
||||
import { formatNative } from "../format-native.js"
|
||||
|
||||
import simpleTool from "./fixtures/simple.js"
|
||||
import cachedTool from "./fixtures/cached.js"
|
||||
import legacyTool from "./fixtures/legacy.js"
|
||||
import { toolA, toolB } from "./fixtures/multi.js"
|
||||
import { validTool as mixedValidTool } from "./fixtures/mixed.js"
|
||||
|
||||
const fixtureTools = {
|
||||
simple: simpleTool,
|
||||
cached: cachedTool,
|
||||
legacy: legacyTool,
|
||||
multi_toolA: toolA,
|
||||
multi_toolB: toolB,
|
||||
mixed_validTool: mixedValidTool,
|
||||
}
|
||||
|
||||
describe("formatNative", () => {
|
||||
it("should convert a tool without args", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "simple_tool",
|
||||
description: "A simple tool",
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatNative(serialized)
|
||||
|
||||
expect(result).toEqual({
|
||||
type: "function",
|
||||
function: {
|
||||
name: "simple_tool",
|
||||
description: "A simple tool",
|
||||
parameters: undefined,
|
||||
strict: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should convert a tool with required args", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "greeter",
|
||||
description: "Greets a person",
|
||||
parameters: z.object({
|
||||
name: z.string().describe("Person's name"),
|
||||
}),
|
||||
async execute({ name }) {
|
||||
return `Hello, ${name}!`
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatNative(serialized)
|
||||
|
||||
expect(result.type).toBe("function")
|
||||
expect(result.function.name).toBe("greeter")
|
||||
expect(result.function.description).toBe("Greets a person")
|
||||
expect(result.function.parameters?.properties).toEqual({
|
||||
name: {
|
||||
type: "string",
|
||||
description: "Person's name",
|
||||
},
|
||||
})
|
||||
expect(result.function.parameters?.required).toEqual(["name"])
|
||||
expect(result.function.parameters?.additionalProperties).toBe(false)
|
||||
})
|
||||
|
||||
it("should convert a tool with optional args", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "optional_tool",
|
||||
description: "Tool with optional args",
|
||||
parameters: z.object({
|
||||
format: z.string().optional().describe("Output format"),
|
||||
}),
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatNative(serialized)
|
||||
|
||||
expect(result.function.parameters?.required).toEqual([])
|
||||
expect(result.function.parameters?.properties).toEqual({
|
||||
format: {
|
||||
type: "string",
|
||||
description: "Output format",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should convert a tool with mixed required and optional args", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "mixed_tool",
|
||||
description: "Tool with mixed args",
|
||||
parameters: z.object({
|
||||
input: z.string().describe("Required input"),
|
||||
options: z.object({}).optional().describe("Optional config"),
|
||||
count: z.number().describe("Also required"),
|
||||
}),
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatNative(serialized)
|
||||
|
||||
expect(result.function.parameters?.required).toEqual(["input", "count"])
|
||||
expect(result.function.parameters?.properties).toEqual({
|
||||
input: {
|
||||
type: "string",
|
||||
description: "Required input",
|
||||
},
|
||||
options: {
|
||||
additionalProperties: false,
|
||||
properties: {},
|
||||
type: "object",
|
||||
description: "Optional config",
|
||||
},
|
||||
count: {
|
||||
type: "number",
|
||||
description: "Also required",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should map type strings to JSON Schema types", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "typed_tool",
|
||||
description: "Tool with various types",
|
||||
parameters: z.object({
|
||||
str: z.string().describe("A string"),
|
||||
num: z.number().describe("A number"),
|
||||
bool: z.boolean().describe("A boolean"),
|
||||
obj: z.object({}).describe("An object"),
|
||||
arr: z.array(z.string()).describe("An array"),
|
||||
}),
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatNative(serialized)
|
||||
const props = result.function.parameters?.properties as
|
||||
| Record<string, { type: string; description?: string }>
|
||||
| undefined
|
||||
|
||||
expect(props?.str?.type).toBe("string")
|
||||
expect(props?.num?.type).toBe("number")
|
||||
expect(props?.bool?.type).toBe("boolean")
|
||||
expect(props?.obj?.type).toBe("object")
|
||||
expect(props?.arr?.type).toBe("array")
|
||||
})
|
||||
|
||||
it("should pass through raw parameters as-is", () => {
|
||||
// formatNative is a simple wrapper that passes through parameters unchanged
|
||||
const serialized = {
|
||||
name: "test_tool",
|
||||
description: "Tool with specific type",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
data: { type: "integer", description: "Integer type" },
|
||||
},
|
||||
},
|
||||
} as SerializedCustomToolDefinition
|
||||
|
||||
const result = formatNative(serialized)
|
||||
|
||||
expect(result.type).toBe("function")
|
||||
expect(result.function.name).toBe("test_tool")
|
||||
const props = result.function.parameters?.properties as Record<string, { type: string }> | undefined
|
||||
expect(props?.data?.type).toBe("integer")
|
||||
})
|
||||
|
||||
it("should convert multiple tools", () => {
|
||||
const tools = [
|
||||
defineCustomTool({
|
||||
name: "tool_a",
|
||||
description: "First tool",
|
||||
async execute() {
|
||||
return "a"
|
||||
},
|
||||
}),
|
||||
defineCustomTool({
|
||||
name: "tool_b",
|
||||
description: "Second tool",
|
||||
async execute() {
|
||||
return "b"
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
const serialized = serializeCustomTools(tools)
|
||||
const result = serialized.map(formatNative)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]?.function.name).toBe("tool_a")
|
||||
expect(result[1]?.function.name).toBe("tool_b")
|
||||
expect(result.every((t) => t.type === "function")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Native Protocol snapshots", () => {
|
||||
it("should generate correct native definition for simple tool", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.simple)
|
||||
const result = formatNative(serialized)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct native definition for cached tool", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.cached)
|
||||
const result = formatNative(serialized)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct native definition for legacy tool (using args)", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.legacy)
|
||||
const result = formatNative(serialized)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct native definitions for multi export tools", () => {
|
||||
const serializedA = serializeCustomTool(fixtureTools.multi_toolA)
|
||||
const serializedB = serializeCustomTool(fixtureTools.multi_toolB)
|
||||
const result = [serializedA, serializedB].map(formatNative)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct native definition for mixed export tool", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.mixed_validTool)
|
||||
const result = formatNative(serialized)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct native definitions for all fixtures combined", () => {
|
||||
const allSerialized = Object.values(fixtureTools).map(serializeCustomTool)
|
||||
const result = allSerialized.map(formatNative)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
192
packages/core/src/custom-tools/__tests__/format-xml.spec.ts
Normal file
192
packages/core/src/custom-tools/__tests__/format-xml.spec.ts
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
// pnpm --filter @roo-code/core test src/custom-tools/__tests__/format-xml.spec.ts
|
||||
|
||||
import { type SerializedCustomToolDefinition, parametersSchema as z, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
import { serializeCustomTool, serializeCustomTools } from "../serialize.js"
|
||||
import { formatXml } from "../format-xml.js"
|
||||
|
||||
import simpleTool from "./fixtures/simple.js"
|
||||
import cachedTool from "./fixtures/cached.js"
|
||||
import legacyTool from "./fixtures/legacy.js"
|
||||
import { toolA, toolB } from "./fixtures/multi.js"
|
||||
import { validTool as mixedValidTool } from "./fixtures/mixed.js"
|
||||
|
||||
const fixtureTools = {
|
||||
simple: simpleTool,
|
||||
cached: cachedTool,
|
||||
legacy: legacyTool,
|
||||
multi_toolA: toolA,
|
||||
multi_toolB: toolB,
|
||||
mixed_validTool: mixedValidTool,
|
||||
}
|
||||
|
||||
describe("formatXml", () => {
|
||||
it("should return empty string for empty tools array", () => {
|
||||
expect(formatXml([])).toBe("")
|
||||
})
|
||||
|
||||
it("should throw for undefined tools", () => {
|
||||
expect(() => formatXml(undefined as unknown as SerializedCustomToolDefinition[])).toThrow()
|
||||
})
|
||||
|
||||
it("should generate description for a single tool without args", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "my_tool",
|
||||
description: "A simple tool that does something",
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatXml([serialized])
|
||||
|
||||
expect(result).toContain("# Custom Tools")
|
||||
expect(result).toContain("## my_tool")
|
||||
expect(result).toContain("Description: A simple tool that does something")
|
||||
expect(result).toContain("Parameters: None")
|
||||
expect(result).toContain("<my_tool>")
|
||||
expect(result).toContain("</my_tool>")
|
||||
})
|
||||
|
||||
it("should generate description for a tool with required args", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "greeter",
|
||||
description: "Greets a person by name",
|
||||
parameters: z.object({
|
||||
name: z.string().describe("The name of the person to greet"),
|
||||
}),
|
||||
async execute({ name }) {
|
||||
return `Hello, ${name}!`
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatXml([serialized])
|
||||
|
||||
expect(result).toContain("## greeter")
|
||||
expect(result).toContain("Description: Greets a person by name")
|
||||
expect(result).toContain("Parameters:")
|
||||
expect(result).toContain("- name: (required) The name of the person to greet (type: string)")
|
||||
expect(result).toContain("<greeter>")
|
||||
expect(result).toContain("<name>name value here</name>")
|
||||
expect(result).toContain("</greeter>")
|
||||
})
|
||||
|
||||
it("should generate description for a tool with optional args", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "configurable_tool",
|
||||
description: "A tool with optional configuration",
|
||||
parameters: z.object({
|
||||
input: z.string().describe("The input to process"),
|
||||
format: z.string().optional().describe("Output format"),
|
||||
}),
|
||||
async execute({ input, format }) {
|
||||
return format ? `${input} (${format})` : input
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatXml([serialized])
|
||||
|
||||
expect(result).toContain("- input: (required) The input to process (type: string)")
|
||||
expect(result).toContain("- format: (optional) Output format (type: string)")
|
||||
expect(result).toContain("<input>input value here</input>")
|
||||
expect(result).toContain("<format>optional format value</format>")
|
||||
})
|
||||
|
||||
it("should generate descriptions for multiple tools", () => {
|
||||
const tools = [
|
||||
defineCustomTool({
|
||||
name: "tool_a",
|
||||
description: "First tool",
|
||||
async execute() {
|
||||
return "a"
|
||||
},
|
||||
}),
|
||||
defineCustomTool({
|
||||
name: "tool_b",
|
||||
description: "Second tool",
|
||||
parameters: z.object({
|
||||
value: z.number().describe("A numeric value"),
|
||||
}),
|
||||
async execute() {
|
||||
return "b"
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
const serialized = serializeCustomTools(tools)
|
||||
const result = formatXml(serialized)
|
||||
|
||||
expect(result).toContain("## tool_a")
|
||||
expect(result).toContain("Description: First tool")
|
||||
expect(result).toContain("## tool_b")
|
||||
expect(result).toContain("Description: Second tool")
|
||||
expect(result).toContain("- value: (required) A numeric value (type: number)")
|
||||
})
|
||||
|
||||
it("should treat args in required array as required", () => {
|
||||
// Using a raw SerializedToolDefinition to test the required behavior.
|
||||
const tools: SerializedCustomToolDefinition[] = [
|
||||
{
|
||||
name: "test_tool",
|
||||
description: "Test tool",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
data: {
|
||||
type: "object",
|
||||
description: "Some data",
|
||||
},
|
||||
},
|
||||
required: ["data"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const result = formatXml(tools)
|
||||
|
||||
expect(result).toContain("- data: (required) Some data (type: object)")
|
||||
expect(result).toContain("<data>data value here</data>")
|
||||
})
|
||||
})
|
||||
|
||||
describe("XML Protocol snapshots", () => {
|
||||
it("should generate correct XML description for simple tool", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.simple)
|
||||
const result = formatXml([serialized])
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct XML description for cached tool", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.cached)
|
||||
const result = formatXml([serialized])
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct XML description for legacy tool (using args)", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.legacy)
|
||||
const result = formatXml([serialized])
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct XML description for multi export tools", () => {
|
||||
const serializedA = serializeCustomTool(fixtureTools.multi_toolA)
|
||||
const serializedB = serializeCustomTool(fixtureTools.multi_toolB)
|
||||
const result = formatXml([serializedA, serializedB])
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct XML description for mixed export tool", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.mixed_validTool)
|
||||
const result = formatXml([serialized])
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct XML description for all fixtures combined", () => {
|
||||
const allSerialized = Object.values(fixtureTools).map(serializeCustomTool)
|
||||
const result = formatXml(allSerialized)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
225
packages/core/src/custom-tools/__tests__/serialize.spec.ts
Normal file
225
packages/core/src/custom-tools/__tests__/serialize.spec.ts
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
// pnpm --filter @roo-code/core test src/custom-tools/__tests__/serialize.spec.ts
|
||||
|
||||
import { parametersSchema as z, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
import { serializeCustomTool, serializeCustomTools } from "../serialize.js"
|
||||
|
||||
import simpleTool from "./fixtures/simple.js"
|
||||
import cachedTool from "./fixtures/cached.js"
|
||||
import legacyTool from "./fixtures/legacy.js"
|
||||
import { toolA, toolB } from "./fixtures/multi.js"
|
||||
import { validTool as mixedValidTool } from "./fixtures/mixed.js"
|
||||
|
||||
const fixtureTools = {
|
||||
simple: simpleTool,
|
||||
cached: cachedTool,
|
||||
legacy: legacyTool,
|
||||
multi_toolA: toolA,
|
||||
multi_toolB: toolB,
|
||||
mixed_validTool: mixedValidTool,
|
||||
}
|
||||
|
||||
describe("serializeCustomTool", () => {
|
||||
it("should serialize a tool without parameters", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "simple_tool",
|
||||
description: "A simple tool that does something",
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const result = serializeCustomTool(tool)
|
||||
|
||||
expect(result).toEqual({
|
||||
name: "simple_tool",
|
||||
description: "A simple tool that does something",
|
||||
})
|
||||
})
|
||||
|
||||
it("should serialize a tool with required string parameter", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "greeter",
|
||||
description: "Greets a person by name",
|
||||
parameters: z.object({
|
||||
name: z.string().describe("The name of the person to greet"),
|
||||
}),
|
||||
async execute({ name }) {
|
||||
return `Hello, ${name}!`
|
||||
},
|
||||
})
|
||||
|
||||
const result = serializeCustomTool(tool)
|
||||
|
||||
expect(result.name).toBe("greeter")
|
||||
expect(result.description).toBe("Greets a person by name")
|
||||
expect(result.parameters?.properties?.name).toEqual({
|
||||
type: "string",
|
||||
description: "The name of the person to greet",
|
||||
})
|
||||
expect(result.parameters?.required).toEqual(["name"])
|
||||
})
|
||||
|
||||
it("should serialize a tool with optional parameter", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "configurable_tool",
|
||||
description: "A tool with optional configuration",
|
||||
parameters: z.object({
|
||||
input: z.string().describe("The input to process"),
|
||||
format: z.string().optional().describe("Output format"),
|
||||
}),
|
||||
async execute({ input, format }) {
|
||||
return format ? `${input} (${format})` : input
|
||||
},
|
||||
})
|
||||
|
||||
const result = serializeCustomTool(tool)
|
||||
|
||||
expect(result.parameters?.properties?.input).toEqual({
|
||||
type: "string",
|
||||
description: "The input to process",
|
||||
})
|
||||
|
||||
expect(result.parameters?.properties?.format).toEqual({
|
||||
type: "string",
|
||||
description: "Output format",
|
||||
})
|
||||
|
||||
// Only required params should be in the required array
|
||||
expect(result.parameters?.required).toEqual(["input"])
|
||||
})
|
||||
|
||||
it("should serialize a tool with various types", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "typed_tool",
|
||||
description: "Tool with various types",
|
||||
parameters: z.object({
|
||||
str: z.string().describe("A string"),
|
||||
num: z.number().describe("A number"),
|
||||
bool: z.boolean().describe("A boolean"),
|
||||
obj: z.object({}).describe("An object"),
|
||||
arr: z.array(z.string()).describe("An array"),
|
||||
}),
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const result = serializeCustomTool(tool)
|
||||
|
||||
expect(result.parameters?.properties?.str).toEqual({
|
||||
description: "A string",
|
||||
type: "string",
|
||||
})
|
||||
expect(result.parameters?.properties?.num).toEqual({
|
||||
description: "A number",
|
||||
type: "number",
|
||||
})
|
||||
expect(result.parameters?.properties?.bool).toEqual({
|
||||
description: "A boolean",
|
||||
type: "boolean",
|
||||
})
|
||||
expect(result.parameters?.properties?.obj).toEqual({
|
||||
additionalProperties: false,
|
||||
description: "An object",
|
||||
properties: {},
|
||||
type: "object",
|
||||
})
|
||||
expect(result.parameters?.properties?.arr).toEqual({
|
||||
description: "An array",
|
||||
items: { type: "string" },
|
||||
type: "array",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle nullable parameters as optional", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "nullable_tool",
|
||||
description: "Tool with nullable param",
|
||||
parameters: z.object({
|
||||
value: z.string().nullable().describe("A nullable value"),
|
||||
}),
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const result = serializeCustomTool(tool)
|
||||
|
||||
expect(result.parameters?.required).toEqual(["value"])
|
||||
})
|
||||
|
||||
it("should handle default values as optional", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "default_tool",
|
||||
description: "Tool with default param",
|
||||
parameters: z.object({
|
||||
count: z.number().default(10).describe("A count with default"),
|
||||
}),
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const result = serializeCustomTool(tool)
|
||||
|
||||
expect(result.parameters?.required).toEqual(["count"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("serializeCustomTools", () => {
|
||||
it("should return empty array for empty tools array", () => {
|
||||
expect(serializeCustomTools([])).toEqual([])
|
||||
})
|
||||
|
||||
it("should serialize multiple tools", () => {
|
||||
const tools = [
|
||||
defineCustomTool({
|
||||
name: "tool_a",
|
||||
description: "First tool",
|
||||
async execute() {
|
||||
return "a"
|
||||
},
|
||||
}),
|
||||
defineCustomTool({
|
||||
name: "tool_b",
|
||||
description: "Second tool",
|
||||
parameters: z.object({
|
||||
value: z.number().describe("A numeric value"),
|
||||
}),
|
||||
async execute() {
|
||||
return "b"
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
const result = serializeCustomTools(tools)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]?.name).toBe("tool_a")
|
||||
expect(result[1]?.name).toBe("tool_b")
|
||||
expect(result[1]?.parameters?.properties?.value).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Serialization snapshots", () => {
|
||||
it("should correctly serialize simple tool", () => {
|
||||
const result = serializeCustomTool(fixtureTools.simple)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should correctly serialize cached tool", () => {
|
||||
const result = serializeCustomTool(fixtureTools.cached)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should correctly serialize legacy tool (using args)", () => {
|
||||
const result = serializeCustomTool(fixtureTools.legacy)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should correctly serialize all fixtures", () => {
|
||||
const result = Object.values(fixtureTools).map(serializeCustomTool)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
370
packages/core/src/custom-tools/custom-tool-registry.ts
Normal file
370
packages/core/src/custom-tools/custom-tool-registry.ts
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
/**
|
||||
* CustomToolRegistry - A reusable class for dynamically loading and managing TypeScript tools.
|
||||
*
|
||||
* Features:
|
||||
* - Dynamic TypeScript/JavaScript tool loading with esbuild transpilation.
|
||||
* - Runtime validation of tool definitions.
|
||||
* - Tool execution with context.
|
||||
* - JSON Schema generation for LLM integration.
|
||||
*/
|
||||
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { createHash } from "crypto"
|
||||
import os from "os"
|
||||
|
||||
import type { CustomToolDefinition, SerializedCustomToolDefinition, CustomToolParametersSchema } from "@roo-code/types"
|
||||
|
||||
import type { StoredCustomTool, LoadResult } from "./types.js"
|
||||
import { serializeCustomTool } from "./serialize.js"
|
||||
import { runEsbuild } from "./esbuild-runner.js"
|
||||
|
||||
export interface RegistryOptions {
|
||||
/** Directory for caching compiled TypeScript files. */
|
||||
cacheDir?: string
|
||||
/** Additional paths for resolving node modules (useful for tools outside node_modules). */
|
||||
nodePaths?: string[]
|
||||
/** Path to the extension root directory (for finding bundled esbuild binary in production). */
|
||||
extensionPath?: string
|
||||
}
|
||||
|
||||
export class CustomToolRegistry {
|
||||
private tools = new Map<string, StoredCustomTool>()
|
||||
private tsCache = new Map<string, string>()
|
||||
private cacheDir: string
|
||||
private nodePaths: string[]
|
||||
private extensionPath?: string
|
||||
private lastLoaded: Map<string, number> = new Map()
|
||||
|
||||
constructor(options?: RegistryOptions) {
|
||||
this.cacheDir = options?.cacheDir ?? path.join(os.tmpdir(), "dynamic-tools-cache")
|
||||
// Default to current working directory's node_modules.
|
||||
this.nodePaths = options?.nodePaths ?? [path.join(process.cwd(), "node_modules")]
|
||||
this.extensionPath = options?.extensionPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all tools from a directory.
|
||||
* Supports both .ts and .js files.
|
||||
*
|
||||
* @param toolDir - Absolute path to the tools directory
|
||||
* @returns LoadResult with lists of loaded and failed tools
|
||||
*/
|
||||
async loadFromDirectory(toolDir: string): Promise<LoadResult> {
|
||||
const result: LoadResult = { loaded: [], failed: [] }
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(toolDir)) {
|
||||
return result
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(toolDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js"))
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(toolDir, file)
|
||||
|
||||
try {
|
||||
console.log(`[CustomToolRegistry] importing tool from ${filePath}`)
|
||||
const mod = await this.import(filePath)
|
||||
|
||||
for (const [exportName, value] of Object.entries(mod)) {
|
||||
const def = this.validate(exportName, value)
|
||||
|
||||
if (!def) {
|
||||
continue
|
||||
}
|
||||
|
||||
this.tools.set(def.name, { ...def, source: filePath })
|
||||
console.log(`[CustomToolRegistry] loaded tool ${def.name} from ${filePath}`)
|
||||
result.loaded.push(def.name)
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`[CustomToolRegistry] import(${filePath}) failed: ${message}`)
|
||||
result.failed.push({ file, error: message })
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`[CustomToolRegistry] loadFromDirectory(${toolDir}) failed: ${message}`)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async loadFromDirectoryIfStale(toolDir: string): Promise<LoadResult> {
|
||||
if (!fs.existsSync(toolDir)) {
|
||||
return { loaded: [], failed: [] }
|
||||
}
|
||||
|
||||
const lastLoaded = this.lastLoaded.get(toolDir)
|
||||
const stat = fs.statSync(toolDir)
|
||||
const isStale = lastLoaded ? stat.mtimeMs > lastLoaded : true
|
||||
|
||||
if (isStale) {
|
||||
this.lastLoaded.set(toolDir, stat.mtimeMs)
|
||||
return this.loadFromDirectory(toolDir)
|
||||
}
|
||||
|
||||
return { loaded: this.list(), failed: [] }
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all tools from multiple directories.
|
||||
* Directories are processed in order, so later directories can override tools from earlier ones.
|
||||
* Supports both .ts and .js files.
|
||||
*
|
||||
* @param toolDirs - Array of absolute paths to tools directories
|
||||
* @returns LoadResult with lists of loaded and failed tools from all directories
|
||||
*/
|
||||
async loadFromDirectories(toolDirs: string[]): Promise<LoadResult> {
|
||||
const result: LoadResult = { loaded: [], failed: [] }
|
||||
|
||||
for (const toolDir of toolDirs) {
|
||||
const dirResult = await this.loadFromDirectory(toolDir)
|
||||
result.loaded.push(...dirResult.loaded)
|
||||
result.failed.push(...dirResult.failed)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all tools from multiple directories if any has become stale.
|
||||
* Directories are processed in order, so later directories can override tools from earlier ones.
|
||||
*
|
||||
* @param toolDirs - Array of absolute paths to tools directories
|
||||
* @returns LoadResult with lists of loaded and failed tools
|
||||
*/
|
||||
async loadFromDirectoriesIfStale(toolDirs: string[]): Promise<LoadResult> {
|
||||
const result: LoadResult = { loaded: [], failed: [] }
|
||||
|
||||
for (const toolDir of toolDirs) {
|
||||
const dirResult = await this.loadFromDirectoryIfStale(toolDir)
|
||||
result.loaded.push(...dirResult.loaded)
|
||||
result.failed.push(...dirResult.failed)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a tool directly (without loading from file).
|
||||
*/
|
||||
register(definition: CustomToolDefinition, source?: string): void {
|
||||
const { name: id } = definition
|
||||
const validated = this.validate(id, definition)
|
||||
|
||||
if (!validated) {
|
||||
throw new Error(`Invalid tool definition for '${id}'`)
|
||||
}
|
||||
|
||||
const storedTool: StoredCustomTool = source ? { ...validated, source } : validated
|
||||
this.tools.set(id, storedTool)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a tool by ID.
|
||||
*/
|
||||
unregister(id: string): boolean {
|
||||
return this.tools.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a tool by ID.
|
||||
*/
|
||||
get(id: string): CustomToolDefinition | undefined {
|
||||
return this.tools.get(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool exists.
|
||||
*/
|
||||
has(id: string): boolean {
|
||||
return this.tools.has(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered tool IDs.
|
||||
*/
|
||||
list(): string[] {
|
||||
return Array.from(this.tools.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered tools.
|
||||
*/
|
||||
getAll(): CustomToolDefinition[] {
|
||||
return Array.from(this.tools.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered tools in the serialized format.
|
||||
*/
|
||||
getAllSerialized(): SerializedCustomToolDefinition[] {
|
||||
return this.getAll().map(serializeCustomTool)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of registered tools.
|
||||
*/
|
||||
get size(): number {
|
||||
return this.tools.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registered tools.
|
||||
*/
|
||||
clear(): void {
|
||||
this.tools.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the extension path for finding bundled esbuild binary.
|
||||
* This should be called with context.extensionPath when the extension activates.
|
||||
*/
|
||||
setExtensionPath(extensionPath: string): void {
|
||||
this.extensionPath = extensionPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current extension path.
|
||||
*/
|
||||
getExtensionPath(): string | undefined {
|
||||
return this.extensionPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the TypeScript compilation cache (both in-memory and on disk).
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.tsCache.clear()
|
||||
|
||||
if (fs.existsSync(this.cacheDir)) {
|
||||
try {
|
||||
const files = fs.readdirSync(this.cacheDir)
|
||||
for (const file of files) {
|
||||
if (file.endsWith(".mjs")) {
|
||||
fs.unlinkSync(path.join(this.cacheDir, file))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[CustomToolRegistry] clearCache failed to clean disk cache: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically import a TypeScript or JavaScript file.
|
||||
* TypeScript files are transpiled on-the-fly using esbuild.
|
||||
*/
|
||||
private async import(filePath: string): Promise<Record<string, CustomToolDefinition>> {
|
||||
const absolutePath = path.resolve(filePath)
|
||||
const ext = path.extname(absolutePath)
|
||||
|
||||
if (ext === ".js" || ext === ".mjs") {
|
||||
return import(`file://${absolutePath}`)
|
||||
}
|
||||
|
||||
const stat = fs.statSync(absolutePath)
|
||||
const cacheKey = `${absolutePath}:${stat.mtimeMs}`
|
||||
|
||||
// Check if we have a cached version in memory.
|
||||
if (this.tsCache.has(cacheKey)) {
|
||||
const cachedPath = this.tsCache.get(cacheKey)!
|
||||
return import(`file://${cachedPath}`)
|
||||
}
|
||||
|
||||
// Ensure cache directory exists.
|
||||
fs.mkdirSync(this.cacheDir, { recursive: true })
|
||||
|
||||
const hash = createHash("sha256").update(cacheKey).digest("hex").slice(0, 16)
|
||||
const tempFile = path.join(this.cacheDir, `${hash}.mjs`)
|
||||
|
||||
// Check if we have a cached version on disk (from a previous run/instance).
|
||||
if (fs.existsSync(tempFile)) {
|
||||
this.tsCache.set(cacheKey, tempFile)
|
||||
return import(`file://${tempFile}`)
|
||||
}
|
||||
|
||||
// Bundle the TypeScript file with dependencies using esbuild CLI.
|
||||
await runEsbuild(
|
||||
{
|
||||
entryPoint: absolutePath,
|
||||
outfile: tempFile,
|
||||
format: "esm",
|
||||
platform: "node",
|
||||
target: "node18",
|
||||
bundle: true,
|
||||
sourcemap: "inline",
|
||||
packages: "bundle",
|
||||
nodePaths: this.nodePaths,
|
||||
},
|
||||
this.extensionPath,
|
||||
)
|
||||
|
||||
this.tsCache.set(cacheKey, tempFile)
|
||||
return import(`file://${tempFile}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is a Zod schema by looking for the _def property
|
||||
* which is present on all Zod types.
|
||||
*/
|
||||
private isParametersSchema(value: unknown): value is CustomToolParametersSchema {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === "object" &&
|
||||
"_def" in value &&
|
||||
typeof (value as Record<string, unknown>)._def === "object"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a tool definition and return a typed result.
|
||||
* Returns null for non-tool exports, throws for invalid tools.
|
||||
*/
|
||||
private validate(exportName: string, value: unknown): CustomToolDefinition | null {
|
||||
// Quick pre-check to filter out non-objects.
|
||||
if (!value || typeof value !== "object") {
|
||||
return null
|
||||
}
|
||||
|
||||
// Check if it looks like a tool (has execute function).
|
||||
if (!("execute" in value) || typeof (value as Record<string, unknown>).execute !== "function") {
|
||||
return null
|
||||
}
|
||||
|
||||
const obj = value as Record<string, unknown>
|
||||
const errors: string[] = []
|
||||
|
||||
// Validate name.
|
||||
if (typeof obj.name !== "string") {
|
||||
errors.push("name: Expected string")
|
||||
} else if (obj.name.length === 0) {
|
||||
errors.push("name: Tool must have a non-empty name")
|
||||
}
|
||||
|
||||
// Validate description.
|
||||
if (typeof obj.description !== "string") {
|
||||
errors.push("description: Expected string")
|
||||
} else if (obj.description.length === 0) {
|
||||
errors.push("description: Tool must have a non-empty description")
|
||||
}
|
||||
|
||||
// Validate parameters (optional).
|
||||
if (obj.parameters !== undefined && !this.isParametersSchema(obj.parameters)) {
|
||||
errors.push("parameters: parameters must be a Zod schema")
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Invalid tool definition for '${exportName}': ${errors.join(", ")}`)
|
||||
}
|
||||
|
||||
return value as CustomToolDefinition
|
||||
}
|
||||
}
|
||||
|
||||
export const customToolRegistry = new CustomToolRegistry()
|
||||
175
packages/core/src/custom-tools/esbuild-runner.ts
Normal file
175
packages/core/src/custom-tools/esbuild-runner.ts
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
/**
|
||||
* esbuild-runner - Runs esbuild-wasm CLI to transpile TypeScript files.
|
||||
*
|
||||
* This module provides a way to run esbuild as a CLI process instead of using
|
||||
* the JavaScript API. This uses esbuild-wasm which is cross-platform and works
|
||||
* on all operating systems without needing native binaries.
|
||||
*
|
||||
* In production, the esbuild-wasm CLI script is bundled in dist/bin/.
|
||||
* In development, it falls back to using esbuild-wasm from node_modules.
|
||||
*/
|
||||
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
import { fileURLToPath } from "url"
|
||||
import { execa } from "execa"
|
||||
|
||||
// Get the directory where this module is located.
|
||||
function getModuleDir(): string | undefined {
|
||||
try {
|
||||
// In ESM context, import.meta.url is available.
|
||||
// In bundled CJS, this will throw or be undefined.
|
||||
if (typeof import.meta !== "undefined" && import.meta.url) {
|
||||
return path.dirname(fileURLToPath(import.meta.url))
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors, fall through to undefined.
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const moduleDir = getModuleDir()
|
||||
|
||||
export interface EsbuildOptions {
|
||||
/** Entry point file path (absolute) */
|
||||
entryPoint: string
|
||||
/** Output file path (absolute) */
|
||||
outfile: string
|
||||
/** Output format */
|
||||
format?: "esm" | "cjs" | "iife"
|
||||
/** Target platform */
|
||||
platform?: "node" | "browser" | "neutral"
|
||||
/** Target environment (e.g., "node18") */
|
||||
target?: string
|
||||
/** Bundle dependencies */
|
||||
bundle?: boolean
|
||||
/** Generate source maps */
|
||||
sourcemap?: boolean | "inline" | "external"
|
||||
/** How to handle packages: "bundle" includes them, "external" leaves them */
|
||||
packages?: "bundle" | "external"
|
||||
/** Additional paths for module resolution */
|
||||
nodePaths?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the esbuild-wasm CLI script by walking up the directory tree.
|
||||
* In pnpm monorepos, node_modules/esbuild-wasm is a symlink to the actual package,
|
||||
* so we don't need special pnpm handling.
|
||||
*/
|
||||
function findEsbuildWasmScript(startDir: string): string | null {
|
||||
const maxDepth = 10
|
||||
let currentDir = path.resolve(startDir)
|
||||
const root = path.parse(currentDir).root
|
||||
|
||||
for (let i = 0; i < maxDepth && currentDir !== root; i++) {
|
||||
// Check node_modules/esbuild-wasm/bin/esbuild at this level.
|
||||
const scriptPath = path.join(currentDir, "node_modules", "esbuild-wasm", "bin", "esbuild")
|
||||
|
||||
if (fs.existsSync(scriptPath)) {
|
||||
return scriptPath
|
||||
}
|
||||
|
||||
// Also check src/node_modules for monorepo where src is a workspace.
|
||||
const srcScriptPath = path.join(currentDir, "src", "node_modules", "esbuild-wasm", "bin", "esbuild")
|
||||
|
||||
if (fs.existsSync(srcScriptPath)) {
|
||||
return srcScriptPath
|
||||
}
|
||||
|
||||
currentDir = path.dirname(currentDir)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the esbuild CLI script.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. Production: Look in extension's dist/bin directory for bundled script.
|
||||
* 2. Development: Use esbuild-wasm from node_modules (relative to this module).
|
||||
* 3. Fallback: Try process.cwd() as last resort.
|
||||
*
|
||||
* @param extensionPath - Path to the extension's root directory (production)
|
||||
* @returns Path to the esbuild CLI script
|
||||
*/
|
||||
export function getEsbuildScriptPath(extensionPath?: string): string {
|
||||
// Production: look in extension's dist/bin directory.
|
||||
if (extensionPath) {
|
||||
const prodPath = path.join(extensionPath, "dist", "bin", "esbuild")
|
||||
|
||||
if (fs.existsSync(prodPath)) {
|
||||
return prodPath
|
||||
}
|
||||
}
|
||||
|
||||
// Development: use esbuild-wasm from node_modules relative to this module.
|
||||
// This works when running the extension in debug mode (if moduleDir is available).
|
||||
if (moduleDir) {
|
||||
const devPath = findEsbuildWasmScript(moduleDir)
|
||||
|
||||
if (devPath) {
|
||||
return devPath
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try from cwd (for tests and other contexts).
|
||||
const cwdPath = findEsbuildWasmScript(process.cwd())
|
||||
|
||||
if (cwdPath) {
|
||||
return cwdPath
|
||||
}
|
||||
|
||||
throw new Error("esbuild-wasm CLI not found. Ensure esbuild-wasm is installed.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Run esbuild CLI to bundle a TypeScript file.
|
||||
*
|
||||
* Uses esbuild-wasm which is cross-platform and runs via Node.js.
|
||||
*
|
||||
* @param options - Build options
|
||||
* @param extensionPath - Path to extension root (for finding bundled script)
|
||||
* @returns Promise that resolves when build completes
|
||||
* @throws Error if the build fails
|
||||
*/
|
||||
export async function runEsbuild(options: EsbuildOptions, extensionPath?: string): Promise<void> {
|
||||
const scriptPath = getEsbuildScriptPath(extensionPath)
|
||||
|
||||
const args: string[] = [
|
||||
scriptPath,
|
||||
options.entryPoint,
|
||||
`--outfile=${options.outfile}`,
|
||||
`--format=${options.format ?? "esm"}`,
|
||||
`--platform=${options.platform ?? "node"}`,
|
||||
`--target=${options.target ?? "node18"}`,
|
||||
]
|
||||
|
||||
if (options.bundle !== false) {
|
||||
args.push("--bundle")
|
||||
}
|
||||
|
||||
if (options.sourcemap) {
|
||||
args.push(options.sourcemap === true ? "--sourcemap" : `--sourcemap=${options.sourcemap}`)
|
||||
}
|
||||
|
||||
if (options.packages) {
|
||||
args.push(`--packages=${options.packages}`)
|
||||
}
|
||||
|
||||
// Build environment with NODE_PATH for module resolution.
|
||||
const env: NodeJS.ProcessEnv = { ...process.env }
|
||||
|
||||
if (options.nodePaths && options.nodePaths.length > 0) {
|
||||
env.NODE_PATH = options.nodePaths.join(path.delimiter)
|
||||
}
|
||||
|
||||
try {
|
||||
await execa(process.execPath, args, { env, stdin: "ignore" })
|
||||
} catch (error) {
|
||||
const execaError = error as { stderr?: string; stdout?: string; exitCode?: number; message: string }
|
||||
const errorMessage = execaError.stderr || execaError.stdout || `esbuild exited with code ${execaError.exitCode}`
|
||||
throw new Error(`esbuild failed: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
23
packages/core/src/custom-tools/format-native.ts
Normal file
23
packages/core/src/custom-tools/format-native.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { OpenAI } from "openai"
|
||||
|
||||
import type { SerializedCustomToolDefinition } from "@roo-code/types"
|
||||
|
||||
export function formatNative(tool: SerializedCustomToolDefinition): OpenAI.Chat.ChatCompletionFunctionTool {
|
||||
// Create a shallow copy to avoid mutating the input object
|
||||
let parameters = tool.parameters
|
||||
|
||||
if (parameters) {
|
||||
// Create a new object with the modifications instead of mutating the original
|
||||
parameters = { ...parameters }
|
||||
|
||||
// We don't need the $schema property; none of the other tools specify it.
|
||||
delete parameters["$schema"]
|
||||
|
||||
// https://community.openai.com/t/on-the-function-calling-what-about-if-i-have-no-parameter-to-call/516876
|
||||
if (!parameters.required) {
|
||||
parameters.required = []
|
||||
}
|
||||
}
|
||||
|
||||
return { type: "function", function: { ...tool, strict: true, parameters } }
|
||||
}
|
||||
89
packages/core/src/custom-tools/format-xml.ts
Normal file
89
packages/core/src/custom-tools/format-xml.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import type { SerializedCustomToolDefinition, SerializedCustomToolParameters } from "@roo-code/types"
|
||||
|
||||
/**
|
||||
* Extract the type string from a parameter schema.
|
||||
* Handles both direct `type` property and `anyOf` schemas (used for nullable types).
|
||||
*/
|
||||
function getParameterType(parameter: SerializedCustomToolParameters): string {
|
||||
// Direct type property
|
||||
if (parameter.type) {
|
||||
return String(parameter.type)
|
||||
}
|
||||
|
||||
// Handle anyOf schema (used for nullable types like `string | null`)
|
||||
if (parameter.anyOf && Array.isArray(parameter.anyOf)) {
|
||||
const types = parameter.anyOf
|
||||
.map((schema) => (typeof schema === "object" && schema.type ? String(schema.type) : null))
|
||||
.filter((t): t is string => t !== null && t !== "null")
|
||||
|
||||
if (types.length > 0) {
|
||||
return types.join(" | ")
|
||||
}
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
function getParameterDescription(name: string, parameter: SerializedCustomToolParameters, required: string[]): string {
|
||||
const requiredText = required.includes(name) ? "(required)" : "(optional)"
|
||||
const typeText = getParameterType(parameter)
|
||||
return `- ${name}: ${requiredText} ${parameter.description ?? ""} (type: ${typeText})`
|
||||
}
|
||||
|
||||
function getUsage(tool: SerializedCustomToolDefinition): string {
|
||||
const lines: string[] = [`<${tool.name}>`]
|
||||
|
||||
if (tool.parameters) {
|
||||
const required = tool.parameters.required ?? []
|
||||
|
||||
for (const [argName, _argType] of Object.entries(tool.parameters.properties ?? {})) {
|
||||
const placeholder = required.includes(argName) ? `${argName} value here` : `optional ${argName} value`
|
||||
lines.push(`<${argName}>${placeholder}</${argName}>`)
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(`</${tool.name}>`)
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
function getDescription(tool: SerializedCustomToolDefinition): string {
|
||||
const parts: string[] = []
|
||||
|
||||
parts.push(`## ${tool.name}`)
|
||||
parts.push(`Description: ${tool.description}`)
|
||||
|
||||
if (tool.parameters?.properties) {
|
||||
const required = tool.parameters?.required ?? []
|
||||
parts.push("Parameters:")
|
||||
|
||||
for (const [name, parameter] of Object.entries(tool.parameters.properties)) {
|
||||
// What should we do with `boolean` values for `parameter`?
|
||||
if (typeof parameter !== "object") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts.push(getParameterDescription(name, parameter, required))
|
||||
}
|
||||
} else {
|
||||
parts.push("Parameters: None")
|
||||
}
|
||||
|
||||
parts.push("Usage:")
|
||||
parts.push(getUsage(tool))
|
||||
|
||||
return parts.join("\n")
|
||||
}
|
||||
|
||||
export function formatXml(tools: SerializedCustomToolDefinition[]): string {
|
||||
if (tools.length === 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const descriptions = tools.map((tool) => getDescription(tool))
|
||||
|
||||
return `# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
${descriptions.join("\n\n")}`
|
||||
}
|
||||
4
packages/core/src/custom-tools/index.ts
Normal file
4
packages/core/src/custom-tools/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export * from "./custom-tool-registry.js"
|
||||
export * from "./serialize.js"
|
||||
export * from "./format-xml.js"
|
||||
export * from "./format-native.js"
|
||||
21
packages/core/src/custom-tools/serialize.ts
Normal file
21
packages/core/src/custom-tools/serialize.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { type SerializedCustomToolDefinition, parametersSchema } from "@roo-code/types"
|
||||
|
||||
import type { StoredCustomTool } from "./types.js"
|
||||
|
||||
export function serializeCustomTool({
|
||||
name,
|
||||
description,
|
||||
parameters,
|
||||
source,
|
||||
}: StoredCustomTool): SerializedCustomToolDefinition {
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
parameters: parameters ? parametersSchema.toJSONSchema(parameters) : undefined,
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeCustomTools(tools: StoredCustomTool[]): SerializedCustomToolDefinition[] {
|
||||
return tools.map(serializeCustomTool)
|
||||
}
|
||||
8
packages/core/src/custom-tools/types.ts
Normal file
8
packages/core/src/custom-tools/types.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { type CustomToolDefinition } from "@roo-code/types"
|
||||
|
||||
export type StoredCustomTool = CustomToolDefinition & { source?: string }
|
||||
|
||||
export interface LoadResult {
|
||||
loaded: string[]
|
||||
failed: Array<{ file: string; error: string }>
|
||||
}
|
||||
1
packages/core/src/index.ts
Normal file
1
packages/core/src/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./custom-tools/index.js"
|
||||
9
packages/core/tsconfig.json
Normal file
9
packages/core/tsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "@roo-code/config-typescript/base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals"],
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src", "scripts", "*.config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
9
packages/core/vitest.config.ts
Normal file
9
packages/core/vitest.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { defineConfig } from "vitest/config"
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
watch: false,
|
||||
},
|
||||
})
|
||||
89
packages/types/src/__tests__/custom-tool.spec.ts
Normal file
89
packages/types/src/__tests__/custom-tool.spec.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import {
|
||||
type CustomToolDefinition,
|
||||
type CustomToolContext,
|
||||
defineCustomTool,
|
||||
parametersSchema as z,
|
||||
} from "../custom-tool.js"
|
||||
import type { TaskLike } from "../task.js"
|
||||
|
||||
describe("custom-tool utilities", () => {
|
||||
describe("z (Zod re-export)", () => {
|
||||
it("should export z from zod", () => {
|
||||
expect(z).toBeDefined()
|
||||
expect(z.string).toBeInstanceOf(Function)
|
||||
expect(z.object).toBeInstanceOf(Function)
|
||||
expect(z.number).toBeInstanceOf(Function)
|
||||
})
|
||||
|
||||
it("should allow creating schemas", () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
count: z.number().optional(),
|
||||
})
|
||||
|
||||
const result = schema.parse({ name: "test" })
|
||||
expect(result).toEqual({ name: "test" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("defineCustomTool", () => {
|
||||
it("should return the same definition object", () => {
|
||||
const definition = {
|
||||
name: "test-tool",
|
||||
description: "Test tool",
|
||||
parameters: z.object({ input: z.string() }),
|
||||
execute: async (args: { input: string }) => `Result: ${args.input}`,
|
||||
}
|
||||
|
||||
const result = defineCustomTool(definition)
|
||||
expect(result).toBe(definition)
|
||||
})
|
||||
|
||||
it("should work without parameters", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "no-params-tool",
|
||||
description: "No params tool",
|
||||
execute: async () => "done",
|
||||
})
|
||||
|
||||
expect(tool.description).toBe("No params tool")
|
||||
expect(tool.parameters).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should preserve type inference for execute args", async () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "typed-tool",
|
||||
description: "Typed tool",
|
||||
parameters: z.object({
|
||||
name: z.string(),
|
||||
count: z.number(),
|
||||
}),
|
||||
execute: async (args) => {
|
||||
// TypeScript should infer args as { name: string, count: number }.
|
||||
return `Hello ${args.name}, count is ${args.count}`
|
||||
},
|
||||
})
|
||||
|
||||
const context: CustomToolContext = {
|
||||
mode: "code",
|
||||
task: { taskId: "test-task-id" } as unknown as TaskLike,
|
||||
}
|
||||
|
||||
const result = await tool.execute({ name: "World", count: 42 }, context)
|
||||
expect(result).toBe("Hello World, count is 42")
|
||||
})
|
||||
})
|
||||
|
||||
describe("CustomToolDefinition type", () => {
|
||||
it("should accept valid definitions", () => {
|
||||
const def: CustomToolDefinition = {
|
||||
name: "valid-tool",
|
||||
description: "A valid tool",
|
||||
parameters: z.object({}),
|
||||
execute: async () => "result",
|
||||
}
|
||||
|
||||
expect(def.description).toBe("A valid tool")
|
||||
})
|
||||
})
|
||||
})
|
||||
104
packages/types/src/custom-tool.ts
Normal file
104
packages/types/src/custom-tool.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import type { ZodType, z } from "zod/v4"
|
||||
|
||||
import { TaskLike } from "./task.js"
|
||||
|
||||
// Re-export from Zod for convenience.
|
||||
|
||||
export { z as parametersSchema } from "zod/v4"
|
||||
|
||||
export type CustomToolParametersSchema = ZodType
|
||||
|
||||
export type SerializedCustomToolParameters = z.core.JSONSchema.JSONSchema
|
||||
|
||||
/**
|
||||
* Context provided to tool execute functions.
|
||||
*/
|
||||
export interface CustomToolContext {
|
||||
mode: string
|
||||
task: TaskLike
|
||||
}
|
||||
|
||||
/**
|
||||
* Definition structure for a custom tool.
|
||||
*
|
||||
* Note: This interface uses simple types to avoid TypeScript performance issues
|
||||
* with Zod's complex type inference. For type-safe parameter inference, use
|
||||
* the `defineCustomTool` helper function instead of annotating with this interface.
|
||||
*/
|
||||
export interface CustomToolDefinition {
|
||||
/**
|
||||
* The name of the tool.
|
||||
* This is used to identify the tool in the prompt and in the tool registry.
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* A description of what the tool does.
|
||||
* This is shown to the AI model to help it decide when to use the tool.
|
||||
*/
|
||||
description: string
|
||||
|
||||
/**
|
||||
* Optional Zod schema defining the tool's parameters.
|
||||
* Use `z.object({})` to define the shape of arguments.
|
||||
*/
|
||||
parameters?: CustomToolParametersSchema
|
||||
|
||||
/**
|
||||
* The function that executes the tool.
|
||||
*
|
||||
* @param args - The validated arguments
|
||||
* @param context - Execution context with session and message info
|
||||
* @returns A string result to return to the AI
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
execute: (args: any, context: CustomToolContext) => Promise<string>
|
||||
}
|
||||
|
||||
export interface SerializedCustomToolDefinition {
|
||||
name: string
|
||||
description: string
|
||||
parameters?: SerializedCustomToolParameters
|
||||
source?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe definition structure for a custom tool with inferred parameter types.
|
||||
* Use this with `defineCustomTool` for full type inference.
|
||||
*
|
||||
* @template T - The Zod schema type for parameters
|
||||
*/
|
||||
export interface TypedCustomToolDefinition<T extends CustomToolParametersSchema>
|
||||
extends Omit<CustomToolDefinition, "execute" | "parameters"> {
|
||||
parameters?: T
|
||||
execute: (args: z.infer<T>, context: CustomToolContext) => Promise<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to define a custom tool with proper type inference.
|
||||
*
|
||||
* This is optional - you can also just export a plain object that matches
|
||||
* the CustomToolDefinition interface.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { z, defineCustomTool } from "@roo-code/types"
|
||||
*
|
||||
* export default defineCustomTool({
|
||||
* name: "add_numbers",
|
||||
* description: "Add two numbers",
|
||||
* parameters: z.object({
|
||||
* a: z.number().describe("First number"),
|
||||
* b: z.number().describe("Second number"),
|
||||
* }),
|
||||
* async execute({ a, b }) {
|
||||
* return `The sum is ${a + b}`
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function defineCustomTool<T extends CustomToolParametersSchema>(
|
||||
definition: TypedCustomToolDefinition<T>,
|
||||
): TypedCustomToolDefinition<T> {
|
||||
return definition
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ export const experimentIds = [
|
|||
"imageGeneration",
|
||||
"runSlashCommand",
|
||||
"multipleNativeToolCalls",
|
||||
"customTools",
|
||||
] as const
|
||||
|
||||
export const experimentIdsSchema = z.enum(experimentIds)
|
||||
|
|
@ -30,6 +31,7 @@ export const experimentsSchema = z.object({
|
|||
imageGeneration: z.boolean().optional(),
|
||||
runSlashCommand: z.boolean().optional(),
|
||||
multipleNativeToolCalls: z.boolean().optional(),
|
||||
customTools: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type Experiments = z.infer<typeof experimentsSchema>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export * from "./cloud.js"
|
|||
export * from "./codebase-index.js"
|
||||
export * from "./context-management.js"
|
||||
export * from "./cookie-consent.js"
|
||||
export * from "./custom-tool.js"
|
||||
export * from "./events.js"
|
||||
export * from "./experiment.js"
|
||||
export * from "./followup.js"
|
||||
|
|
|
|||
67
pnpm-lock.yaml
generated
67
pnpm-lock.yaml
generated
|
|
@ -78,6 +78,21 @@ importers:
|
|||
specifier: ^5.4.5
|
||||
version: 5.8.3
|
||||
|
||||
.roo/tools:
|
||||
devDependencies:
|
||||
'@roo-code/config-eslint':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/config-eslint
|
||||
'@roo-code/config-typescript':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/config-typescript
|
||||
'@roo-code/types':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/types
|
||||
vitest:
|
||||
specifier: ^3.2.3
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
|
||||
|
||||
apps/vscode-e2e:
|
||||
devDependencies:
|
||||
'@roo-code/config-eslint':
|
||||
|
|
@ -474,6 +489,37 @@ importers:
|
|||
|
||||
packages/config-typescript: {}
|
||||
|
||||
packages/core:
|
||||
dependencies:
|
||||
'@roo-code/types':
|
||||
specifier: workspace:^
|
||||
version: link:../types
|
||||
esbuild:
|
||||
specifier: '>=0.25.0'
|
||||
version: 0.25.9
|
||||
execa:
|
||||
specifier: ^9.5.2
|
||||
version: 9.6.0
|
||||
openai:
|
||||
specifier: ^5.12.2
|
||||
version: 5.12.2(ws@8.18.3)(zod@3.25.76)
|
||||
zod:
|
||||
specifier: ^3.25.61
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@roo-code/config-eslint':
|
||||
specifier: workspace:^
|
||||
version: link:../config-eslint
|
||||
'@roo-code/config-typescript':
|
||||
specifier: workspace:^
|
||||
version: link:../config-typescript
|
||||
'@types/node':
|
||||
specifier: ^24.1.0
|
||||
version: 24.2.1
|
||||
vitest:
|
||||
specifier: ^3.2.3
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
|
||||
|
||||
packages/evals:
|
||||
dependencies:
|
||||
'@roo-code/ipc':
|
||||
|
|
@ -654,6 +700,9 @@ importers:
|
|||
'@roo-code/cloud':
|
||||
specifier: workspace:^
|
||||
version: link:../packages/cloud
|
||||
'@roo-code/core':
|
||||
specifier: workspace:^
|
||||
version: link:../packages/core
|
||||
'@roo-code/ipc':
|
||||
specifier: workspace:^
|
||||
version: link:../packages/ipc
|
||||
|
|
@ -928,9 +977,9 @@ importers:
|
|||
'@vscode/vsce':
|
||||
specifier: 3.3.2
|
||||
version: 3.3.2
|
||||
esbuild:
|
||||
specifier: '>=0.25.0'
|
||||
version: 0.25.9
|
||||
esbuild-wasm:
|
||||
specifier: ^0.25.0
|
||||
version: 0.25.12
|
||||
execa:
|
||||
specifier: ^9.5.2
|
||||
version: 9.5.3
|
||||
|
|
@ -5786,6 +5835,11 @@ packages:
|
|||
peerDependencies:
|
||||
esbuild: '>=0.25.0'
|
||||
|
||||
esbuild-wasm@0.25.12:
|
||||
resolution: {integrity: sha512-rZqkjL3Y6FwLpSHzLnaEy8Ps6veCNo1kZa9EOfJvmWtBq5dJH4iVjfmOO6Mlkv9B0tt9WFPFmb/VxlgJOnueNg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
esbuild@0.25.9:
|
||||
resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -15666,6 +15720,8 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
esbuild-wasm@0.25.12: {}
|
||||
|
||||
esbuild@0.25.9:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.9
|
||||
|
|
@ -18418,6 +18474,11 @@ snapshots:
|
|||
ws: 8.18.3
|
||||
zod: 3.25.61
|
||||
|
||||
openai@5.12.2(ws@8.18.3)(zod@3.25.76):
|
||||
optionalDependencies:
|
||||
ws: 8.18.3
|
||||
zod: 3.25.76
|
||||
|
||||
option@0.2.4: {}
|
||||
|
||||
optionator@0.9.4:
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ packages:
|
|||
- "webview-ui" # Should be apps/vscode-webview
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
- ".roo/tools" # Custom tools for this workspace
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import { parseJSON } from "partial-json"
|
||||
|
||||
import { type ToolName, toolNames, type FileEntry } from "@roo-code/types"
|
||||
import { customToolRegistry } from "@roo-code/core"
|
||||
|
||||
import {
|
||||
type ToolUse,
|
||||
type McpToolUse,
|
||||
type ToolParamName,
|
||||
toolParamNames,
|
||||
type NativeToolArgs,
|
||||
toolParamNames,
|
||||
} from "../../shared/tools"
|
||||
import { resolveToolAlias } from "../prompts/tools/filter-tools-for-mode"
|
||||
import { parseJSON } from "partial-json"
|
||||
import type {
|
||||
ApiStreamToolCallStartChunk,
|
||||
ApiStreamToolCallDeltaChunk,
|
||||
|
|
@ -573,6 +576,7 @@ export class NativeToolCallParser {
|
|||
}): ToolUse<TName> | McpToolUse | null {
|
||||
// Check if this is a dynamic MCP tool (mcp--serverName--toolName)
|
||||
const mcpPrefix = MCP_TOOL_PREFIX + MCP_TOOL_SEPARATOR
|
||||
|
||||
if (typeof toolCall.name === "string" && toolCall.name.startsWith(mcpPrefix)) {
|
||||
return this.parseDynamicMcpTool(toolCall)
|
||||
}
|
||||
|
|
@ -580,8 +584,8 @@ export class NativeToolCallParser {
|
|||
// Resolve tool alias to canonical name
|
||||
const resolvedName = resolveToolAlias(toolCall.name as string) as TName
|
||||
|
||||
// Validate tool name (after alias resolution)
|
||||
if (!toolNames.includes(resolvedName as ToolName)) {
|
||||
// Validate tool name (after alias resolution).
|
||||
if (!toolNames.includes(resolvedName as ToolName) && !customToolRegistry.has(resolvedName)) {
|
||||
console.error(`Invalid tool name: ${toolCall.name} (resolved: ${resolvedName})`)
|
||||
console.error(`Valid tool names:`, toolNames)
|
||||
return null
|
||||
|
|
@ -589,7 +593,7 @@ export class NativeToolCallParser {
|
|||
|
||||
try {
|
||||
// Parse the arguments JSON string
|
||||
const args = JSON.parse(toolCall.arguments)
|
||||
const args = toolCall.arguments === "" ? {} : JSON.parse(toolCall.arguments)
|
||||
|
||||
// Build legacy params object for backward compatibility with XML protocol and UI.
|
||||
// Native execution path uses nativeArgs instead, which has proper typing.
|
||||
|
|
@ -604,7 +608,7 @@ export class NativeToolCallParser {
|
|||
}
|
||||
|
||||
// Validate parameter name
|
||||
if (!toolParamNames.includes(key as ToolParamName)) {
|
||||
if (!toolParamNames.includes(key as ToolParamName) && !customToolRegistry.has(resolvedName)) {
|
||||
console.warn(`Unknown parameter '${key}' for tool '${resolvedName}'`)
|
||||
console.warn(`Valid param names:`, toolParamNames)
|
||||
continue
|
||||
|
|
@ -816,6 +820,12 @@ export class NativeToolCallParser {
|
|||
break
|
||||
|
||||
default:
|
||||
if (customToolRegistry.has(resolvedName)) {
|
||||
nativeArgs = args as NativeArgsFor<TName>
|
||||
} else {
|
||||
console.error(`Unhandled tool: ${resolvedName}`)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
|||
import type { ToolName, ClineAsk, ToolProgressStatus } from "@roo-code/types"
|
||||
import { ConsecutiveMistakeError } from "@roo-code/types"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
import { customToolRegistry } from "@roo-code/core"
|
||||
|
||||
import { t } from "../../i18n"
|
||||
|
||||
|
|
@ -1070,9 +1071,8 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
})
|
||||
break
|
||||
default: {
|
||||
// Handle unknown/invalid tool names
|
||||
// Handle unknown/invalid tool names OR custom tools
|
||||
// This is critical for native protocol where every tool_use MUST have a tool_result
|
||||
// Note: This case should rarely be reached since validateToolUse now checks for unknown tools
|
||||
|
||||
// CRITICAL: Don't process partial blocks for unknown tools - just let them stream in.
|
||||
// If we try to show errors for partial blocks, we'd show the error on every streaming chunk,
|
||||
|
|
@ -1081,6 +1081,45 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
break
|
||||
}
|
||||
|
||||
const customTool = stateExperiments?.customTools ? customToolRegistry.get(block.name) : undefined
|
||||
|
||||
if (customTool) {
|
||||
try {
|
||||
let customToolArgs
|
||||
|
||||
if (customTool.parameters) {
|
||||
try {
|
||||
customToolArgs = customTool.parameters.parse(block.nativeArgs || block.params || {})
|
||||
} catch (parseParamsError) {
|
||||
const message = `Custom tool "${block.name}" argument validation failed: ${parseParamsError.message}`
|
||||
console.error(message)
|
||||
cline.consecutiveMistakeCount++
|
||||
await cline.say("error", message)
|
||||
pushToolResult(formatResponse.toolError(message, toolProtocol))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const result = await customTool.execute(customToolArgs, {
|
||||
mode: mode ?? defaultModeSlug,
|
||||
task: cline,
|
||||
})
|
||||
|
||||
console.log(
|
||||
`${customTool.name}.execute(): ${JSON.stringify(customToolArgs)} -> ${JSON.stringify(result)}`,
|
||||
)
|
||||
|
||||
pushToolResult(result)
|
||||
cline.consecutiveMistakeCount = 0
|
||||
} catch (executionError: any) {
|
||||
cline.consecutiveMistakeCount++
|
||||
await handleError(`executing custom tool "${block.name}"`, executionError)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
// Not a custom tool - handle as unknown tool error
|
||||
const errorMessage = `Unknown tool "${block.name}". This tool does not exist. Please use one of the available tools.`
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolError(block.name as ToolName, errorMessage)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
import * as vscode from "vscode"
|
||||
import * as os from "os"
|
||||
|
||||
import type { ModeConfig, PromptComponent, CustomModePrompts, TodoItem } from "@roo-code/types"
|
||||
|
||||
import type { SystemPromptSettings } from "./types"
|
||||
import {
|
||||
type ModeConfig,
|
||||
type PromptComponent,
|
||||
type CustomModePrompts,
|
||||
type TodoItem,
|
||||
getEffectiveProtocol,
|
||||
isNativeProtocol,
|
||||
} from "@roo-code/types"
|
||||
import { customToolRegistry, formatXml } from "@roo-code/core"
|
||||
|
||||
import { Mode, modes, defaultModeSlug, getModeBySlug, getGroupName, getModeSelection } from "../../shared/modes"
|
||||
import { DiffStrategy } from "../../shared/tools"
|
||||
|
|
@ -15,8 +21,8 @@ import { CodeIndexManager } from "../../services/code-index/manager"
|
|||
|
||||
import { PromptVariables, loadSystemPromptFile } from "./sections/custom-system-prompt"
|
||||
|
||||
import type { SystemPromptSettings } from "./types"
|
||||
import { getToolDescriptionsForMode } from "./tools"
|
||||
import { getEffectiveProtocol, isNativeProtocol } from "@roo-code/types"
|
||||
import {
|
||||
getRulesSection,
|
||||
getSystemInfoSection,
|
||||
|
|
@ -98,7 +104,7 @@ async function generatePrompt(
|
|||
])
|
||||
|
||||
// Build tools catalog section only for XML protocol
|
||||
const toolsCatalog = isNativeProtocol(effectiveProtocol)
|
||||
const builtInToolsCatalog = isNativeProtocol(effectiveProtocol)
|
||||
? ""
|
||||
: `\n\n${getToolDescriptionsForMode(
|
||||
mode,
|
||||
|
|
@ -116,6 +122,18 @@ async function generatePrompt(
|
|||
modelId,
|
||||
)}`
|
||||
|
||||
let customToolsSection = ""
|
||||
|
||||
if (experiments?.customTools && !isNativeProtocol(effectiveProtocol)) {
|
||||
const customTools = customToolRegistry.getAllSerialized()
|
||||
|
||||
if (customTools.length > 0) {
|
||||
customToolsSection = `\n\n${formatXml(customTools)}`
|
||||
}
|
||||
}
|
||||
|
||||
const toolsCatalog = builtInToolsCatalog + customToolsSection
|
||||
|
||||
const basePrompt = `${roleDefinition}
|
||||
|
||||
${markdownFormattingSection()}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import path from "path"
|
||||
|
||||
import type OpenAI from "openai"
|
||||
|
||||
import type { ProviderSettings, ModeConfig, ModelInfo } from "@roo-code/types"
|
||||
import { customToolRegistry, formatNative } from "@roo-code/core"
|
||||
|
||||
import type { ClineProvider } from "../webview/ClineProvider"
|
||||
import { getRooDirectoriesForCwd } from "../../services/roo-config/index.js"
|
||||
|
||||
import { getNativeTools, getMcpServerTools } from "../prompts/tools/native-tools"
|
||||
import { filterNativeToolsForMode, filterMcpToolsForMode } from "../prompts/tools/filter-tools-for-mode"
|
||||
|
||||
|
|
@ -40,11 +47,11 @@ export async function buildNativeToolsArray(options: BuildToolsOptions): Promise
|
|||
|
||||
const mcpHub = provider.getMcpHub()
|
||||
|
||||
// Get CodeIndexManager for feature checking
|
||||
// Get CodeIndexManager for feature checking.
|
||||
const { CodeIndexManager } = await import("../../services/code-index/manager")
|
||||
const codeIndexManager = CodeIndexManager.getInstance(provider.context, cwd)
|
||||
|
||||
// Build settings object for tool filtering
|
||||
// Build settings object for tool filtering.
|
||||
const filterSettings = {
|
||||
todoListEnabled: apiConfiguration?.todoListEnabled ?? true,
|
||||
browserToolEnabled: browserToolEnabled ?? true,
|
||||
|
|
@ -52,13 +59,13 @@ export async function buildNativeToolsArray(options: BuildToolsOptions): Promise
|
|||
diffEnabled,
|
||||
}
|
||||
|
||||
// Determine if partial reads are enabled based on maxReadFileLine setting
|
||||
// Determine if partial reads are enabled based on maxReadFileLine setting.
|
||||
const partialReadsEnabled = maxReadFileLine !== -1
|
||||
|
||||
// Build native tools with dynamic read_file tool based on partialReadsEnabled
|
||||
// Build native tools with dynamic read_file tool based on partialReadsEnabled.
|
||||
const nativeTools = getNativeTools(partialReadsEnabled)
|
||||
|
||||
// Filter native tools based on mode restrictions
|
||||
// Filter native tools based on mode restrictions.
|
||||
const filteredNativeTools = filterNativeToolsForMode(
|
||||
nativeTools,
|
||||
mode,
|
||||
|
|
@ -69,9 +76,22 @@ export async function buildNativeToolsArray(options: BuildToolsOptions): Promise
|
|||
mcpHub,
|
||||
)
|
||||
|
||||
// Filter MCP tools based on mode restrictions
|
||||
// Filter MCP tools based on mode restrictions.
|
||||
const mcpTools = getMcpServerTools(mcpHub)
|
||||
const filteredMcpTools = filterMcpToolsForMode(mcpTools, mode, customModes, experiments)
|
||||
|
||||
return [...filteredNativeTools, ...filteredMcpTools]
|
||||
// Add custom tools if they are available and the experiment is enabled.
|
||||
let nativeCustomTools: OpenAI.Chat.ChatCompletionFunctionTool[] = []
|
||||
|
||||
if (experiments?.customTools) {
|
||||
const toolDirs = getRooDirectoriesForCwd(cwd).map((dir) => path.join(dir, "tools"))
|
||||
await customToolRegistry.loadFromDirectoriesIfStale(toolDirs)
|
||||
const customTools = customToolRegistry.getAllSerialized()
|
||||
|
||||
if (customTools.length > 0) {
|
||||
nativeCustomTools = customTools.map(formatNative)
|
||||
}
|
||||
}
|
||||
|
||||
return [...filteredNativeTools, ...filteredMcpTools, ...nativeCustomTools]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { ToolName, ModeConfig, ExperimentId, GroupOptions, GroupEntry } from "@roo-code/types"
|
||||
import { toolNames as validToolNames } from "@roo-code/types"
|
||||
import { customToolRegistry } from "@roo-code/core"
|
||||
|
||||
import { type Mode, FileRestrictionError, getModeBySlug, getGroupName } from "../../shared/modes"
|
||||
import { EXPERIMENT_IDS } from "../../shared/experiments"
|
||||
|
|
@ -10,12 +11,16 @@ import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } from "../../shared/tools"
|
|||
* Note: This does NOT check if the tool is allowed for a specific mode,
|
||||
* only that the tool actually exists.
|
||||
*/
|
||||
export function isValidToolName(toolName: string): toolName is ToolName {
|
||||
export function isValidToolName(toolName: string, experiments?: Record<string, boolean>): toolName is ToolName {
|
||||
// Check if it's a valid static tool
|
||||
if ((validToolNames as readonly string[]).includes(toolName)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (experiments?.customTools && customToolRegistry.has(toolName)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if it's a dynamic MCP tool (mcp_serverName_toolName format).
|
||||
if (toolName.startsWith("mcp_")) {
|
||||
return true
|
||||
|
|
@ -35,7 +40,7 @@ export function validateToolUse(
|
|||
): void {
|
||||
// First, check if the tool name is actually a valid/known tool
|
||||
// This catches completely invalid tool names like "edit_file" that don't exist
|
||||
if (!isValidToolName(toolName)) {
|
||||
if (!isValidToolName(toolName, experiments)) {
|
||||
throw new Error(
|
||||
`Unknown tool "${toolName}". This tool does not exist. Please use one of the available tools: ${validToolNames.join(", ")}.`,
|
||||
)
|
||||
|
|
@ -87,6 +92,12 @@ export function isToolAllowedForMode(
|
|||
return true
|
||||
}
|
||||
|
||||
// For now, allow all custom tools in any mode.
|
||||
// As a follow-up we should expand the custom tool definition to include mode restrictions.
|
||||
if (experiments?.customTools && customToolRegistry.has(tool)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if this is a dynamic MCP tool (mcp_serverName_toolName)
|
||||
// These should be allowed if the mcp group is allowed for the mode
|
||||
const isDynamicMcpTool = tool.startsWith("mcp_")
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { safeWriteJson } from "../../utils/safeWriteJson"
|
|||
import * as path from "path"
|
||||
import * as os from "os"
|
||||
import * as fs from "fs/promises"
|
||||
import { getRooDirectoriesForCwd } from "../../services/roo-config/index.js"
|
||||
import pWaitFor from "p-wait-for"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
|
|
@ -13,9 +14,9 @@ import {
|
|||
type UserSettingsConfig,
|
||||
TelemetryEventName,
|
||||
RooCodeSettings,
|
||||
Experiments,
|
||||
ExperimentId,
|
||||
} from "@roo-code/types"
|
||||
import { customToolRegistry } from "@roo-code/core"
|
||||
import { CloudService } from "@roo-code/cloud"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
|
||||
|
|
@ -1725,6 +1726,25 @@ export const webviewMessageHandler = async (
|
|||
}
|
||||
break
|
||||
}
|
||||
case "refreshCustomTools": {
|
||||
try {
|
||||
const toolDirs = getRooDirectoriesForCwd(getCurrentCwd()).map((dir) => path.join(dir, "tools"))
|
||||
await customToolRegistry.loadFromDirectories(toolDirs)
|
||||
|
||||
await provider.postMessageToWebview({
|
||||
type: "customToolsResult",
|
||||
tools: customToolRegistry.getAllSerialized(),
|
||||
})
|
||||
} catch (error) {
|
||||
await provider.postMessageToWebview({
|
||||
type: "customToolsResult",
|
||||
tools: [],
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case "saveApiConfiguration":
|
||||
if (message.text && message.apiConfiguration) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ async function main() {
|
|||
const production = process.argv.includes("--production")
|
||||
const watch = process.argv.includes("--watch")
|
||||
const minify = production
|
||||
const sourcemap = true // Always generate source maps for error handling
|
||||
const sourcemap = true // Always generate source maps for error handling.
|
||||
|
||||
/**
|
||||
* @type {import('esbuild').BuildOptions}
|
||||
|
|
@ -100,7 +100,7 @@ async function main() {
|
|||
plugins,
|
||||
entryPoints: ["extension.ts"],
|
||||
outfile: "dist/extension.js",
|
||||
external: ["vscode"],
|
||||
external: ["vscode", "esbuild"],
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ try {
|
|||
import type { CloudUserInfo, AuthState } from "@roo-code/types"
|
||||
import { CloudService, BridgeOrchestrator } from "@roo-code/cloud"
|
||||
import { TelemetryService, PostHogTelemetryClient } from "@roo-code/telemetry"
|
||||
import { customToolRegistry } from "@roo-code/core"
|
||||
|
||||
import "./utils/path" // Necessary to have access to String.prototype.toPosix.
|
||||
import { createOutputChannelLogger, createDualLogger } from "./utils/outputChannelLogger"
|
||||
|
|
@ -67,6 +68,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||
context.subscriptions.push(outputChannel)
|
||||
outputChannel.appendLine(`${Package.name} extension activated - ${JSON.stringify(Package)}`)
|
||||
|
||||
// Set extension path for custom tool registry to find bundled esbuild
|
||||
customToolRegistry.setExtensionPath(context.extensionPath)
|
||||
|
||||
// Migrate old settings to new
|
||||
await migrateSettings(context, outputChannel)
|
||||
|
||||
|
|
|
|||
|
|
@ -441,6 +441,7 @@
|
|||
"@modelcontextprotocol/sdk": "1.12.0",
|
||||
"@qdrant/js-client-rest": "^1.14.0",
|
||||
"@roo-code/cloud": "workspace:^",
|
||||
"@roo-code/core": "workspace:^",
|
||||
"@roo-code/ipc": "workspace:^",
|
||||
"@roo-code/telemetry": "workspace:^",
|
||||
"@roo-code/types": "workspace:^",
|
||||
|
|
@ -534,7 +535,7 @@
|
|||
"@types/vscode": "^1.84.0",
|
||||
"@vscode/test-electron": "^2.5.2",
|
||||
"@vscode/vsce": "3.3.2",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild-wasm": "^0.25.0",
|
||||
"execa": "^9.5.2",
|
||||
"glob": "^11.1.0",
|
||||
"mkdirp": "^3.0.1",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import type {
|
|||
OrganizationAllowList,
|
||||
ShareVisibility,
|
||||
QueuedMessage,
|
||||
SerializedCustomToolDefinition,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import { GitCommit } from "../utils/git"
|
||||
|
|
@ -133,6 +134,7 @@ export interface ExtensionMessage {
|
|||
| "browserSessionUpdate"
|
||||
| "browserSessionNavigate"
|
||||
| "claudeCodeRateLimits"
|
||||
| "customToolsResult"
|
||||
text?: string
|
||||
payload?: any // Add a generic payload for now, can refine later
|
||||
// Checkpoint warning message
|
||||
|
|
@ -218,6 +220,7 @@ export interface ExtensionMessage {
|
|||
browserSessionMessages?: ClineMessage[] // For browser session panel updates
|
||||
isBrowserSessionActive?: boolean // For browser session panel updates
|
||||
stepIndex?: number // For browserSessionNavigate: the target step index to display
|
||||
tools?: SerializedCustomToolDefinition[] // For customToolsResult
|
||||
}
|
||||
|
||||
export type ExtensionState = Pick<
|
||||
|
|
|
|||
|
|
@ -180,6 +180,7 @@ export interface WebviewMessage {
|
|||
| "openDebugUiHistory"
|
||||
| "downloadErrorDiagnostics"
|
||||
| "requestClaudeCodeRateLimits"
|
||||
| "refreshCustomTools"
|
||||
text?: string
|
||||
editedMessageContent?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ describe("experiments", () => {
|
|||
imageGeneration: false,
|
||||
runSlashCommand: false,
|
||||
multipleNativeToolCalls: false,
|
||||
customTools: false,
|
||||
}
|
||||
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
|
||||
})
|
||||
|
|
@ -44,6 +45,7 @@ describe("experiments", () => {
|
|||
imageGeneration: false,
|
||||
runSlashCommand: false,
|
||||
multipleNativeToolCalls: false,
|
||||
customTools: false,
|
||||
}
|
||||
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true)
|
||||
})
|
||||
|
|
@ -56,6 +58,7 @@ describe("experiments", () => {
|
|||
imageGeneration: false,
|
||||
runSlashCommand: false,
|
||||
multipleNativeToolCalls: false,
|
||||
customTools: false,
|
||||
}
|
||||
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export const EXPERIMENT_IDS = {
|
|||
IMAGE_GENERATION: "imageGeneration",
|
||||
RUN_SLASH_COMMAND: "runSlashCommand",
|
||||
MULTIPLE_NATIVE_TOOL_CALLS: "multipleNativeToolCalls",
|
||||
CUSTOM_TOOLS: "customTools",
|
||||
} as const satisfies Record<string, ExperimentId>
|
||||
|
||||
type _AssertExperimentIds = AssertEqual<Equals<ExperimentId, Values<typeof EXPERIMENT_IDS>>>
|
||||
|
|
@ -24,6 +25,7 @@ export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
|
|||
IMAGE_GENERATION: { enabled: false },
|
||||
RUN_SLASH_COMMAND: { enabled: false },
|
||||
MULTIPLE_NATIVE_TOOL_CALLS: { enabled: false },
|
||||
CUSTOM_TOOLS: { enabled: false },
|
||||
}
|
||||
|
||||
export const experimentDefault = Object.fromEntries(
|
||||
|
|
|
|||
|
|
@ -17,15 +17,33 @@ vi.mock("fs/promises", () => ({
|
|||
readFile: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("path", () => ({
|
||||
join: vi.fn((...args: string[]) => args.join("/")),
|
||||
isAbsolute: vi.fn((p: string) => p.startsWith("/")),
|
||||
basename: vi.fn((p: string) => p.split("/").pop() || ""),
|
||||
}))
|
||||
vi.mock("path", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("path")>()
|
||||
return {
|
||||
...actual,
|
||||
default: {
|
||||
...actual,
|
||||
join: vi.fn((...args: string[]) => args.join("/")),
|
||||
isAbsolute: vi.fn((p: string) => p.startsWith("/")),
|
||||
basename: vi.fn((p: string) => p.split("/").pop() || ""),
|
||||
},
|
||||
join: vi.fn((...args: string[]) => args.join("/")),
|
||||
isAbsolute: vi.fn((p: string) => p.startsWith("/")),
|
||||
basename: vi.fn((p: string) => p.split("/").pop() || ""),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock("os", () => ({
|
||||
homedir: vi.fn(() => "/home/user"),
|
||||
}))
|
||||
vi.mock("os", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("os")>()
|
||||
return {
|
||||
...actual,
|
||||
default: {
|
||||
...actual,
|
||||
homedir: vi.fn(() => "/home/user"),
|
||||
},
|
||||
homedir: vi.fn(() => "/home/user"),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock("../fs", () => ({
|
||||
fileExistsAtPath: vi.fn(),
|
||||
|
|
|
|||
183
webview-ui/src/components/settings/CustomToolsSettings.tsx
Normal file
183
webview-ui/src/components/settings/CustomToolsSettings.tsx
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
import { useState, useEffect, useCallback, useMemo } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { RefreshCw, Loader2, FileCode } from "lucide-react"
|
||||
|
||||
import type { SerializedCustomToolDefinition } from "@roo-code/types"
|
||||
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
|
||||
import { vscode } from "@/utils/vscode"
|
||||
|
||||
import { Button } from "@/components/ui"
|
||||
|
||||
interface ToolParameter {
|
||||
name: string
|
||||
type: string
|
||||
description?: string
|
||||
required: boolean
|
||||
}
|
||||
|
||||
interface ProcessedTool {
|
||||
name: string
|
||||
description: string
|
||||
parameters: ToolParameter[]
|
||||
source?: string
|
||||
}
|
||||
|
||||
interface CustomToolsSettingsProps {
|
||||
enabled: boolean
|
||||
onChange: (enabled: boolean) => void
|
||||
}
|
||||
|
||||
export const CustomToolsSettings = ({ enabled, onChange }: CustomToolsSettingsProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
const [tools, setTools] = useState<SerializedCustomToolDefinition[]>([])
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [refreshError, setRefreshError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
vscode.postMessage({ type: "refreshCustomTools" })
|
||||
} else {
|
||||
setTools([])
|
||||
}
|
||||
}, [enabled])
|
||||
|
||||
useEvent("message", (event: MessageEvent) => {
|
||||
const message = event.data
|
||||
|
||||
if (message.type === "customToolsResult") {
|
||||
setTools(message.tools || [])
|
||||
setIsRefreshing(false)
|
||||
setRefreshError(message.error ?? null)
|
||||
}
|
||||
})
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
setIsRefreshing(true)
|
||||
setRefreshError(null)
|
||||
vscode.postMessage({ type: "refreshCustomTools" })
|
||||
}, [])
|
||||
|
||||
const processedTools = useMemo<ProcessedTool[]>(
|
||||
() =>
|
||||
tools.map((tool) => {
|
||||
const params = tool.parameters
|
||||
const properties = (params?.properties ?? {}) as Record<string, { type?: string; description?: string }>
|
||||
const required = (params?.required as string[] | undefined) ?? []
|
||||
|
||||
return {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
source: tool.source,
|
||||
parameters: Object.entries(properties).map(([name, def]) => ({
|
||||
name,
|
||||
type: def.type ?? "any",
|
||||
description: def.description,
|
||||
required: required.includes(name),
|
||||
})),
|
||||
}
|
||||
}),
|
||||
[tools],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<VSCodeCheckbox checked={enabled} onChange={(e: any) => onChange(e.target.checked)}>
|
||||
<span className="font-medium">{t("settings:experimental.CUSTOM_TOOLS.name")}</span>
|
||||
</VSCodeCheckbox>
|
||||
</div>
|
||||
<p className="text-vscode-descriptionForeground text-sm mt-0">
|
||||
{t("settings:experimental.CUSTOM_TOOLS.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{enabled && (
|
||||
<div className="ml-2 space-y-3">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<label className="block font-medium">
|
||||
{t("settings:experimental.CUSTOM_TOOLS.toolsHeader")}
|
||||
</label>
|
||||
<Button variant="outline" onClick={onRefresh} disabled={isRefreshing}>
|
||||
<div className="flex items-center gap-2">
|
||||
{isRefreshing ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
)}
|
||||
{isRefreshing
|
||||
? t("settings:experimental.CUSTOM_TOOLS.refreshing")
|
||||
: t("settings:experimental.CUSTOM_TOOLS.refreshButton")}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{refreshError && (
|
||||
<div className="p-2 bg-vscode-inputValidation-errorBackground text-vscode-errorForeground rounded text-sm border border-vscode-inputValidation-errorBorder">
|
||||
{t("settings:experimental.CUSTOM_TOOLS.refreshError")}: {refreshError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{processedTools.length === 0 ? (
|
||||
<p className="text-vscode-descriptionForeground text-sm italic">
|
||||
{t("settings:experimental.CUSTOM_TOOLS.noTools")}
|
||||
</p>
|
||||
) : (
|
||||
processedTools.map((tool) => (
|
||||
<div
|
||||
key={tool.name}
|
||||
className="bg-vscode-editor-background border border-vscode-panel-border rounded space-y-3 p-3">
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium text-vscode-foreground">{tool.name}</div>
|
||||
{tool.source && (
|
||||
<div className="flex items-center text-xs text-vscode-descriptionForeground">
|
||||
<FileCode className="size-3 flex-shrink-0" />
|
||||
<span className="font-mono truncate" title={tool.source}>
|
||||
{tool.source}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-vscode-descriptionForeground text-sm">{tool.description}</div>
|
||||
{tool.parameters.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs font-medium text-vscode-foreground">
|
||||
{t("settings:experimental.CUSTOM_TOOLS.toolParameters")}:
|
||||
</div>
|
||||
<div>
|
||||
{tool.parameters.map((param) => (
|
||||
<div
|
||||
key={param.name}
|
||||
className="flex items-start gap-2 text-xs pl-2 py-1 border-l-2 border-vscode-panel-border">
|
||||
<code className="text-vscode-textLink-foreground font-mono">
|
||||
{param.name}
|
||||
</code>
|
||||
<span className="text-vscode-descriptionForeground">
|
||||
({param.type})
|
||||
</span>
|
||||
{param.required && (
|
||||
<span className="text-vscode-errorForeground text-[10px] uppercase">
|
||||
required
|
||||
</span>
|
||||
)}
|
||||
{param.description && (
|
||||
<span className="text-vscode-descriptionForeground">
|
||||
— {param.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import { SectionHeader } from "./SectionHeader"
|
|||
import { Section } from "./Section"
|
||||
import { ExperimentalFeature } from "./ExperimentalFeature"
|
||||
import { ImageGenerationSettings } from "./ImageGenerationSettings"
|
||||
import { CustomToolsSettings } from "./CustomToolsSettings"
|
||||
|
||||
type ExperimentalSettingsProps = HTMLAttributes<HTMLDivElement> & {
|
||||
experiments: Experiments
|
||||
|
|
@ -92,6 +93,15 @@ export const ExperimentalSettings = ({
|
|||
/>
|
||||
)
|
||||
}
|
||||
if (config[0] === "CUSTOM_TOOLS") {
|
||||
return (
|
||||
<CustomToolsSettings
|
||||
key={config[0]}
|
||||
enabled={experiments[EXPERIMENT_IDS.CUSTOM_TOOLS] ?? false}
|
||||
onChange={(enabled) => setExperimentEnabled(EXPERIMENT_IDS.CUSTOM_TOOLS, enabled)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<ExperimentalFeature
|
||||
key={config[0]}
|
||||
|
|
|
|||
|
|
@ -240,6 +240,7 @@ describe("mergeExtensionState", () => {
|
|||
runSlashCommand: false,
|
||||
nativeToolCalling: false,
|
||||
multipleNativeToolCalls: false,
|
||||
customTools: false,
|
||||
} as Record<ExperimentId, boolean>,
|
||||
checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS + 5,
|
||||
}
|
||||
|
|
@ -263,6 +264,7 @@ describe("mergeExtensionState", () => {
|
|||
runSlashCommand: false,
|
||||
nativeToolCalling: false,
|
||||
multipleNativeToolCalls: false,
|
||||
customTools: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
11
webview-ui/src/i18n/locales/ca/settings.json
generated
11
webview-ui/src/i18n/locales/ca/settings.json
generated
|
|
@ -824,6 +824,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "Crides paral·leles a eines",
|
||||
"description": "Quan està activat, el protocol natiu pot executar múltiples eines en un sol torn de missatge de l'assistent."
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "Habilitar eines personalitzades",
|
||||
"description": "Quan està habilitat, Roo pot carregar i utilitzar eines TypeScript/JavaScript personalitzades des del directori .roo/tools del vostre projecte o ~/.roo/tools per a eines globals. Nota: aquestes eines s'aprovaran automàticament.",
|
||||
"toolsHeader": "Eines personalitzades disponibles",
|
||||
"noTools": "No s'han carregat eines personalitzades. Afegiu fitxers .ts o .js al directori .roo/tools del vostre projecte o ~/.roo/tools per a eines globals.",
|
||||
"refreshButton": "Actualitzar",
|
||||
"refreshing": "Actualitzant...",
|
||||
"refreshSuccess": "Eines actualitzades correctament",
|
||||
"refreshError": "Error en actualitzar les eines",
|
||||
"toolParameters": "Paràmetres"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
11
webview-ui/src/i18n/locales/de/settings.json
generated
11
webview-ui/src/i18n/locales/de/settings.json
generated
|
|
@ -824,6 +824,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "Parallele Tool-Aufrufe",
|
||||
"description": "Wenn aktiviert, kann das native Protokoll mehrere Tools in einer einzigen Assistenten-Nachrichtenrunde ausführen."
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "Benutzerdefinierte Tools aktivieren",
|
||||
"description": "Wenn aktiviert, kann Roo benutzerdefinierte TypeScript/JavaScript-Tools aus dem .roo/tools-Verzeichnis deines Projekts oder ~/.roo/tools für globale Tools laden und verwenden. Hinweis: Diese Tools werden automatisch genehmigt.",
|
||||
"toolsHeader": "Verfügbare benutzerdefinierte Tools",
|
||||
"noTools": "Keine benutzerdefinierten Tools geladen. Füge .ts- oder .js-Dateien zum .roo/tools-Verzeichnis deines Projekts oder ~/.roo/tools für globale Tools hinzu.",
|
||||
"refreshButton": "Aktualisieren",
|
||||
"refreshing": "Aktualisieren...",
|
||||
"refreshSuccess": "Tools erfolgreich aktualisiert",
|
||||
"refreshError": "Fehler beim Aktualisieren der Tools",
|
||||
"toolParameters": "Parameter"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
|
|
@ -833,6 +833,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "Parallel tool calls",
|
||||
"description": "When enabled, the native protocol can execute multiple tools in a single assistant message turn."
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "Enable custom tools",
|
||||
"description": "When enabled, Roo can load and use custom TypeScript/JavaScript tools from your project's .roo/tools directory or ~/.roo/tools for global tools. Note: these tools will automatically be auto-approved.",
|
||||
"toolsHeader": "Available Custom Tools",
|
||||
"noTools": "No custom tools loaded. Add .ts or .js files to your project's .roo/tools directory or ~/.roo/tools for global tools.",
|
||||
"refreshButton": "Refresh",
|
||||
"refreshing": "Refreshing...",
|
||||
"refreshSuccess": "Tools refreshed successfully",
|
||||
"refreshError": "Failed to refresh tools",
|
||||
"toolParameters": "Parameters"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
11
webview-ui/src/i18n/locales/es/settings.json
generated
11
webview-ui/src/i18n/locales/es/settings.json
generated
|
|
@ -824,6 +824,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "Llamadas paralelas a herramientas",
|
||||
"description": "Cuando está habilitado, el protocolo nativo puede ejecutar múltiples herramientas en un solo turno de mensaje del asistente."
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "Habilitar herramientas personalizadas",
|
||||
"description": "Cuando está habilitado, Roo puede cargar y usar herramientas TypeScript/JavaScript personalizadas desde el directorio .roo/tools de tu proyecto o ~/.roo/tools para herramientas globales. Nota: estas herramientas se aprobarán automáticamente.",
|
||||
"toolsHeader": "Herramientas personalizadas disponibles",
|
||||
"noTools": "No hay herramientas personalizadas cargadas. Añade archivos .ts o .js al directorio .roo/tools de tu proyecto o ~/.roo/tools para herramientas globales.",
|
||||
"refreshButton": "Actualizar",
|
||||
"refreshing": "Actualizando...",
|
||||
"refreshSuccess": "Herramientas actualizadas correctamente",
|
||||
"refreshError": "Error al actualizar las herramientas",
|
||||
"toolParameters": "Parámetros"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
11
webview-ui/src/i18n/locales/fr/settings.json
generated
11
webview-ui/src/i18n/locales/fr/settings.json
generated
|
|
@ -824,6 +824,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "Appels d'outils parallèles",
|
||||
"description": "Lorsqu'activé, le protocole natif peut exécuter plusieurs outils en un seul tour de message d'assistant."
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "Activer les outils personnalisés",
|
||||
"description": "Lorsqu'activé, Roo peut charger et utiliser des outils TypeScript/JavaScript personnalisés à partir du répertoire .roo/tools de votre projet ou ~/.roo/tools pour des outils globaux. Remarque : ces outils seront automatiquement approuvés.",
|
||||
"toolsHeader": "Outils personnalisés disponibles",
|
||||
"noTools": "Aucun outil personnalisé chargé. Ajoutez des fichiers .ts ou .js au répertoire .roo/tools de votre projet ou ~/.roo/tools pour des outils globaux.",
|
||||
"refreshButton": "Actualiser",
|
||||
"refreshing": "Actualisation...",
|
||||
"refreshSuccess": "Outils actualisés avec succès",
|
||||
"refreshError": "Échec de l'actualisation des outils",
|
||||
"toolParameters": "Paramètres"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
11
webview-ui/src/i18n/locales/hi/settings.json
generated
11
webview-ui/src/i18n/locales/hi/settings.json
generated
|
|
@ -825,6 +825,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "समानांतर टूल कॉल",
|
||||
"description": "सक्षम होने पर, नेटिव प्रोटोकॉल एकल सहायक संदेश टर्न में एकाधिक टूल निष्पादित कर सकता है।"
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "कस्टम टूल्स सक्षम करें",
|
||||
"description": "सक्षम होने पर, Roo आपके प्रोजेक्ट की .roo/tools निर्देशिका या वैश्विक टूल्स के लिए ~/.roo/tools से कस्टम TypeScript/JavaScript टूल्स लोड और उपयोग कर सकता है। नोट: ये टूल्स स्वचालित रूप से स्वत:-अनुमोदित होंगे।",
|
||||
"toolsHeader": "उपलब्ध कस्टम टूल्स",
|
||||
"noTools": "कोई कस्टम टूल लोड नहीं हुआ। अपने प्रोजेक्ट की .roo/tools निर्देशिका या वैश्विक टूल्स के लिए ~/.roo/tools में .ts या .js फ़ाइलें जोड़ें।",
|
||||
"refreshButton": "रिफ्रेश करें",
|
||||
"refreshing": "रिफ्रेश हो रहा है...",
|
||||
"refreshSuccess": "टूल्स सफलतापूर्वक रिफ्रेश हुए",
|
||||
"refreshError": "टूल्स रिफ्रेश करने में विफल",
|
||||
"toolParameters": "पैरामीटर्स"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
11
webview-ui/src/i18n/locales/id/settings.json
generated
11
webview-ui/src/i18n/locales/id/settings.json
generated
|
|
@ -854,6 +854,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "Panggilan tool paralel",
|
||||
"description": "Ketika diaktifkan, protokol native dapat mengeksekusi beberapa tool dalam satu giliran pesan asisten."
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "Aktifkan tool kustom",
|
||||
"description": "Ketika diaktifkan, Roo dapat memuat dan menggunakan tool TypeScript/JavaScript kustom dari direktori .roo/tools proyek Anda atau ~/.roo/tools untuk tool global. Catatan: tool ini akan disetujui otomatis.",
|
||||
"toolsHeader": "Tool Kustom yang Tersedia",
|
||||
"noTools": "Tidak ada tool kustom yang dimuat. Tambahkan file .ts atau .js ke direktori .roo/tools proyek Anda atau ~/.roo/tools untuk tool global.",
|
||||
"refreshButton": "Refresh",
|
||||
"refreshing": "Merefresh...",
|
||||
"refreshSuccess": "Tool berhasil direfresh",
|
||||
"refreshError": "Gagal merefresh tool",
|
||||
"toolParameters": "Parameter"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
11
webview-ui/src/i18n/locales/it/settings.json
generated
11
webview-ui/src/i18n/locales/it/settings.json
generated
|
|
@ -825,6 +825,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "Chiamate parallele agli strumenti",
|
||||
"description": "Quando abilitato, il protocollo nativo può eseguire più strumenti in un singolo turno di messaggio dell'assistente."
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "Abilita strumenti personalizzati",
|
||||
"description": "Quando abilitato, Roo può caricare e utilizzare strumenti TypeScript/JavaScript personalizzati dalla directory .roo/tools del tuo progetto o ~/.roo/tools per strumenti globali. Nota: questi strumenti saranno automaticamente approvati.",
|
||||
"toolsHeader": "Strumenti personalizzati disponibili",
|
||||
"noTools": "Nessuno strumento personalizzato caricato. Aggiungi file .ts o .js alla directory .roo/tools del tuo progetto o ~/.roo/tools per strumenti globali.",
|
||||
"refreshButton": "Aggiorna",
|
||||
"refreshing": "Aggiornamento...",
|
||||
"refreshSuccess": "Strumenti aggiornati con successo",
|
||||
"refreshError": "Impossibile aggiornare gli strumenti",
|
||||
"toolParameters": "Parametri"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
11
webview-ui/src/i18n/locales/ja/settings.json
generated
11
webview-ui/src/i18n/locales/ja/settings.json
generated
|
|
@ -825,6 +825,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "並列ツール呼び出し",
|
||||
"description": "有効にすると、ネイティブプロトコルは単一のアシスタントメッセージターンで複数のツールを実行できます。"
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "カスタムツールを有効化",
|
||||
"description": "有効にすると、Rooはプロジェクトの.roo/toolsディレクトリまたはグローバルツール用の~/.roo/toolsからカスタムTypeScript/JavaScriptツールを読み込んで使用できます。注意:これらのツールは自動的に承認されます。",
|
||||
"toolsHeader": "利用可能なカスタムツール",
|
||||
"noTools": "カスタムツールが読み込まれていません。プロジェクトの.roo/toolsディレクトリまたはグローバルツール用の~/.roo/toolsに.tsまたは.jsファイルを追加してください。",
|
||||
"refreshButton": "更新",
|
||||
"refreshing": "更新中...",
|
||||
"refreshSuccess": "ツールが正常に更新されました",
|
||||
"refreshError": "ツールの更新に失敗しました",
|
||||
"toolParameters": "パラメーター"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
11
webview-ui/src/i18n/locales/ko/settings.json
generated
11
webview-ui/src/i18n/locales/ko/settings.json
generated
|
|
@ -825,6 +825,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "병렬 도구 호출",
|
||||
"description": "활성화되면 네이티브 프로토콜이 단일 어시스턴트 메시지 턴에서 여러 도구를 실행할 수 있습니다."
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "사용자 정의 도구 활성화",
|
||||
"description": "활성화하면 Roo가 프로젝트의 .roo/tools 디렉터리 또는 전역 도구를 위한 ~/.roo/tools에서 사용자 정의 TypeScript/JavaScript 도구를 로드하고 사용할 수 있습니다. 참고: 이러한 도구는 자동으로 자동 승인됩니다.",
|
||||
"toolsHeader": "사용 가능한 사용자 정의 도구",
|
||||
"noTools": "로드된 사용자 정의 도구가 없습니다. 프로젝트의 .roo/tools 디렉터리 또는 전역 도구를 위한 ~/.roo/tools에 .ts 또는 .js 파일을 추가하세요.",
|
||||
"refreshButton": "새로고침",
|
||||
"refreshing": "새로고침 중...",
|
||||
"refreshSuccess": "도구가 성공적으로 새로고침되었습니다",
|
||||
"refreshError": "도구 새로고침에 실패했습니다",
|
||||
"toolParameters": "매개변수"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
11
webview-ui/src/i18n/locales/nl/settings.json
generated
11
webview-ui/src/i18n/locales/nl/settings.json
generated
|
|
@ -825,6 +825,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "Parallelle tool-aanroepen",
|
||||
"description": "Wanneer ingeschakeld, kan het native protocol meerdere tools uitvoeren in één enkele assistent-berichtbeurt."
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "Aangepaste tools inschakelen",
|
||||
"description": "Indien ingeschakeld kan Roo aangepaste TypeScript/JavaScript-tools laden en gebruiken uit de map .roo/tools van je project of ~/.roo/tools voor globale tools. Opmerking: deze tools worden automatisch goedgekeurd.",
|
||||
"toolsHeader": "Beschikbare aangepaste tools",
|
||||
"noTools": "Geen aangepaste tools geladen. Voeg .ts- of .js-bestanden toe aan de map .roo/tools van je project of ~/.roo/tools voor globale tools.",
|
||||
"refreshButton": "Vernieuwen",
|
||||
"refreshing": "Vernieuwen...",
|
||||
"refreshSuccess": "Tools succesvol vernieuwd",
|
||||
"refreshError": "Fout bij vernieuwen van tools",
|
||||
"toolParameters": "Parameters"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
11
webview-ui/src/i18n/locales/pl/settings.json
generated
11
webview-ui/src/i18n/locales/pl/settings.json
generated
|
|
@ -825,6 +825,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "Równoległe wywołania narzędzi",
|
||||
"description": "Po włączeniu protokół natywny może wykonywać wiele narzędzi w jednej turze wiadomości asystenta."
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "Włącz niestandardowe narzędzia",
|
||||
"description": "Gdy włączone, Roo może ładować i używać niestandardowych narzędzi TypeScript/JavaScript z katalogu .roo/tools Twojego projektu lub ~/.roo/tools dla narzędzi globalnych. Uwaga: te narzędzia będą automatycznie zatwierdzane.",
|
||||
"toolsHeader": "Dostępne niestandardowe narzędzia",
|
||||
"noTools": "Nie załadowano niestandardowych narzędzi. Dodaj pliki .ts lub .js do katalogu .roo/tools swojego projektu lub ~/.roo/tools dla narzędzi globalnych.",
|
||||
"refreshButton": "Odśwież",
|
||||
"refreshing": "Odświeżanie...",
|
||||
"refreshSuccess": "Narzędzia odświeżone pomyślnie",
|
||||
"refreshError": "Nie udało się odświeżyć narzędzi",
|
||||
"toolParameters": "Parametry"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
11
webview-ui/src/i18n/locales/pt-BR/settings.json
generated
11
webview-ui/src/i18n/locales/pt-BR/settings.json
generated
|
|
@ -825,6 +825,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "Chamadas paralelas de ferramentas",
|
||||
"description": "Quando habilitado, o protocolo nativo pode executar múltiplas ferramentas em um único turno de mensagem do assistente."
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "Ativar ferramentas personalizadas",
|
||||
"description": "Quando habilitado, o Roo pode carregar e usar ferramentas TypeScript/JavaScript personalizadas do diretório .roo/tools do seu projeto ou ~/.roo/tools para ferramentas globais. Nota: estas ferramentas serão aprovadas automaticamente.",
|
||||
"toolsHeader": "Ferramentas personalizadas disponíveis",
|
||||
"noTools": "Nenhuma ferramenta personalizada carregada. Adicione arquivos .ts ou .js ao diretório .roo/tools do seu projeto ou ~/.roo/tools para ferramentas globais.",
|
||||
"refreshButton": "Atualizar",
|
||||
"refreshing": "Atualizando...",
|
||||
"refreshSuccess": "Ferramentas atualizadas com sucesso",
|
||||
"refreshError": "Falha ao atualizar ferramentas",
|
||||
"toolParameters": "Parâmetros"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
11
webview-ui/src/i18n/locales/ru/settings.json
generated
11
webview-ui/src/i18n/locales/ru/settings.json
generated
|
|
@ -825,6 +825,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "Параллельные вызовы инструментов",
|
||||
"description": "При включении нативный протокол может выполнять несколько инструментов в одном ходе сообщения ассистента."
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "Включить пользовательские инструменты",
|
||||
"description": "Если включено, Roo сможет загружать и использовать пользовательские инструменты TypeScript/JavaScript из каталога .roo/tools вашего проекта или ~/.roo/tools для глобальных инструментов. Примечание: эти инструменты будут одобрены автоматически.",
|
||||
"toolsHeader": "Доступные пользовательские инструменты",
|
||||
"noTools": "Пользовательские инструменты не загружены. Добавьте файлы .ts или .js в каталог .roo/tools вашего проекта или ~/.roo/tools для глобальных инструментов.",
|
||||
"refreshButton": "Обновить",
|
||||
"refreshing": "Обновление...",
|
||||
"refreshSuccess": "Инструменты успешно обновлены",
|
||||
"refreshError": "Не удалось обновить инструменты",
|
||||
"toolParameters": "Параметры"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
11
webview-ui/src/i18n/locales/tr/settings.json
generated
11
webview-ui/src/i18n/locales/tr/settings.json
generated
|
|
@ -825,6 +825,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "Paralel araç çağrıları",
|
||||
"description": "Etkinleştirildiğinde, yerel protokol tek bir asistan mesaj turunda birden fazla araç yürütebilir."
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "Özel araçları etkinleştir",
|
||||
"description": "Etkinleştirildiğinde, Roo projenizin .roo/tools dizininden veya global araçlar için ~/.roo/tools dizininden özel TypeScript/JavaScript araçlarını yükleyebilir ve kullanabilir. Not: Bu araçlar otomatik olarak onaylanacaktır.",
|
||||
"toolsHeader": "Kullanılabilir Özel Araçlar",
|
||||
"noTools": "Özel araç yüklenmedi. Projenizin .roo/tools dizinine veya global araçlar için ~/.roo/tools dizinine .ts veya .js dosyaları ekleyin.",
|
||||
"refreshButton": "Yenile",
|
||||
"refreshing": "Yenileniyor...",
|
||||
"refreshSuccess": "Araçlar başarıyla yenilendi",
|
||||
"refreshError": "Araçlar yenilenemedi",
|
||||
"toolParameters": "Parametreler"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
11
webview-ui/src/i18n/locales/vi/settings.json
generated
11
webview-ui/src/i18n/locales/vi/settings.json
generated
|
|
@ -825,6 +825,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "Lệnh gọi công cụ song song",
|
||||
"description": "Khi được bật, giao thức native có thể thực thi nhiều công cụ trong một lượt tin nhắn của trợ lý."
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "Bật công cụ tùy chỉnh",
|
||||
"description": "Khi được bật, Roo có thể tải và sử dụng các công cụ TypeScript/JavaScript tùy chỉnh từ thư mục .roo/tools của dự án hoặc ~/.roo/tools cho các công cụ toàn cục. Lưu ý: các công cụ này sẽ được tự động phê duyệt.",
|
||||
"toolsHeader": "Công cụ tùy chỉnh có sẵn",
|
||||
"noTools": "Không có công cụ tùy chỉnh nào được tải. Thêm tệp .ts hoặc .js vào thư mục .roo/tools của dự án hoặc ~/.roo/tools cho các công cụ toàn cục.",
|
||||
"refreshButton": "Làm mới",
|
||||
"refreshing": "Đang làm mới...",
|
||||
"refreshSuccess": "Làm mới công cụ thành công",
|
||||
"refreshError": "Không thể làm mới công cụ",
|
||||
"toolParameters": "Thông số"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
11
webview-ui/src/i18n/locales/zh-CN/settings.json
generated
11
webview-ui/src/i18n/locales/zh-CN/settings.json
generated
|
|
@ -825,6 +825,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "并行工具调用",
|
||||
"description": "启用后,原生协议可在单个助手消息轮次中执行多个工具。"
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "启用自定义工具",
|
||||
"description": "启用后 Roo 可从项目中的 .roo/tools 目录或全局工具目录 ~/.roo/tools 加载并使用自定义 TypeScript/JavaScript 工具。注意:这些工具将自动获批。",
|
||||
"toolsHeader": "可用自定义工具",
|
||||
"noTools": "未加载自定义工具。请向项目的 .roo/tools 目录或全局工具目录 ~/.roo/tools 添加 .ts 或 .js 文件。",
|
||||
"refreshButton": "刷新",
|
||||
"refreshing": "正在刷新...",
|
||||
"refreshSuccess": "工具刷新成功",
|
||||
"refreshError": "工具刷新失败",
|
||||
"toolParameters": "参数"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
11
webview-ui/src/i18n/locales/zh-TW/settings.json
generated
11
webview-ui/src/i18n/locales/zh-TW/settings.json
generated
|
|
@ -825,6 +825,17 @@
|
|||
"MULTIPLE_NATIVE_TOOL_CALLS": {
|
||||
"name": "並行工具呼叫",
|
||||
"description": "啟用後,原生協定可在單個助理訊息輪次中執行多個工具。"
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"name": "啟用自訂工具",
|
||||
"description": "啟用後,Roo 可以從專案中的 .roo/tools 目錄或全域工具目錄 ~/.roo/tools 載入並使用自訂 TypeScript/JavaScript 工具。注意:這些工具將自動獲得核准。",
|
||||
"toolsHeader": "可用自訂工具",
|
||||
"noTools": "未載入自訂工具。請向專案的 .roo/tools 目錄或全域工具目錄 ~/.roo/tools 新增 .ts 或 .js 檔案。",
|
||||
"refreshButton": "重新整理",
|
||||
"refreshing": "正在重新整理...",
|
||||
"refreshSuccess": "工具重新整理成功",
|
||||
"refreshError": "工具重新整理失敗",
|
||||
"toolParameters": "參數"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue