diff --git a/apps/cli/src/ui/components/tools/types.ts b/apps/cli/src/ui/components/tools/types.ts index 28a1b5faa02..a16fbd60ea3 100644 --- a/apps/cli/src/ui/components/tools/types.ts +++ b/apps/cli/src/ui/components/tools/types.ts @@ -16,15 +16,7 @@ export type ToolCategory = | "other" export function getToolCategory(toolName: string): ToolCategory { - const fileReadTools = [ - "readFile", - "read_file", - "fetchInstructions", - "fetch_instructions", - "listFilesTopLevel", - "listFilesRecursive", - "list_files", - ] + const fileReadTools = ["readFile", "read_file", "skill", "listFilesTopLevel", "listFilesRecursive", "list_files"] const fileWriteTools = [ "editedExistingFile", diff --git a/apps/cli/src/ui/components/tools/utils.ts b/apps/cli/src/ui/components/tools/utils.ts index 5eaee33b127..31acf2cccbc 100644 --- a/apps/cli/src/ui/components/tools/utils.ts +++ b/apps/cli/src/ui/components/tools/utils.ts @@ -50,8 +50,7 @@ export function getToolDisplayName(toolName: string): string { // File read operations readFile: "Read", read_file: "Read", - fetchInstructions: "Fetch Instructions", - fetch_instructions: "Fetch Instructions", + skill: "Load Skill", listFilesTopLevel: "List Files", listFilesRecursive: "List Files (Recursive)", list_files: "List Files", @@ -107,8 +106,7 @@ export function getToolIconName(toolName: string): IconName { // File read operations readFile: "file", read_file: "file", - fetchInstructions: "file", - fetch_instructions: "file", + skill: "file", listFilesTopLevel: "folder", listFilesRecursive: "folder", list_files: "folder", diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index d57ec616ff4..72c8b8256f8 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -199,7 +199,6 @@ export const globalSettingsSchema = z.object({ telemetrySetting: telemetrySettingsSchema.optional(), mcpEnabled: z.boolean().optional(), - enableMcpServerCreation: z.boolean().optional(), mode: z.string().optional(), modeApiConfigs: z.record(z.string(), z.string()).optional(), diff --git a/packages/types/src/skills.ts b/packages/types/src/skills.ts index 2c4ac176b0a..b50b4e6d471 100644 --- a/packages/types/src/skills.ts +++ b/packages/types/src/skills.ts @@ -5,8 +5,8 @@ export interface SkillMetadata { name: string // Required: skill identifier description: string // Required: when to use this skill - path: string // Absolute path to SKILL.md - source: "global" | "project" // Where the skill was discovered + path: string // Absolute path to SKILL.md (or "" for built-in skills) + source: "global" | "project" | "built-in" // Where the skill was discovered mode?: string // If set, skill is only available in this mode } diff --git a/packages/types/src/tool.ts b/packages/types/src/tool.ts index f90ef42ede4..03144055c9a 100644 --- a/packages/types/src/tool.ts +++ b/packages/types/src/tool.ts @@ -33,10 +33,10 @@ export const toolNames = [ "attempt_completion", "switch_mode", "new_task", - "fetch_instructions", "codebase_search", "update_todo_list", "run_slash_command", + "skill", "generate_image", "custom_tool", ] as const diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index d8c421f4b8d..7b407481ebd 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -362,7 +362,6 @@ export type ExtensionState = Pick< experiments: Experiments // Map of experiment IDs to their enabled state mcpEnabled: boolean - enableMcpServerCreation: boolean mode: string customModes: ModeConfig[] @@ -502,7 +501,6 @@ export interface WebviewMessage { | "deleteMessageConfirm" | "submitEditedMessage" | "editMessageConfirm" - | "enableMcpServerCreation" | "remoteControlEnabled" | "taskSyncEnabled" | "searchCommits" @@ -643,7 +641,7 @@ export interface WebviewMessage { modeConfig?: ModeConfig timeout?: number payload?: WebViewMessagePayload - source?: "global" | "project" + source?: "global" | "project" | "built-in" skillName?: string // For skill operations (createSkill, deleteSkill, openSkillFile) skillMode?: string // For skill operations (mode restriction) skillDescription?: string // For createSkill (skill description) @@ -790,7 +788,6 @@ export interface ClineSayTool { | "codebaseSearch" | "readFile" | "readCommandOutput" - | "fetchInstructions" | "listFilesTopLevel" | "listFilesRecursive" | "searchFiles" @@ -801,6 +798,7 @@ export interface ClineSayTool { | "imageGenerated" | "runSlashCommand" | "updateTodoList" + | "skill" path?: string // For readCommandOutput readStart?: number @@ -847,6 +845,8 @@ export interface ClineSayTool { args?: string source?: string description?: string + // Properties for skill tool + skill?: string } // Must keep in sync with system prompt. diff --git a/src/core/assistant-message/NativeToolCallParser.ts b/src/core/assistant-message/NativeToolCallParser.ts index 8aa369f74da..ecf649e2734 100644 --- a/src/core/assistant-message/NativeToolCallParser.ts +++ b/src/core/assistant-message/NativeToolCallParser.ts @@ -449,14 +449,6 @@ export class NativeToolCallParser { } break - case "fetch_instructions": - if (partialArgs.task !== undefined) { - nativeArgs = { - task: partialArgs.task, - } - } - break - case "generate_image": if (partialArgs.prompt !== undefined || partialArgs.path !== undefined) { nativeArgs = { @@ -476,6 +468,15 @@ export class NativeToolCallParser { } break + case "skill": + if (partialArgs.skill !== undefined) { + nativeArgs = { + skill: partialArgs.skill, + args: partialArgs.args, + } + } + break + case "search_files": if (partialArgs.path !== undefined || partialArgs.regex !== undefined) { nativeArgs = { @@ -736,14 +737,6 @@ export class NativeToolCallParser { } break - case "fetch_instructions": - if (args.task !== undefined) { - nativeArgs = { - task: args.task, - } as NativeArgsFor - } - break - case "generate_image": if (args.prompt !== undefined && args.path !== undefined) { nativeArgs = { @@ -763,6 +756,15 @@ export class NativeToolCallParser { } break + case "skill": + if (args.skill !== undefined) { + nativeArgs = { + skill: args.skill, + args: args.args, + } as NativeArgsFor + } + break + case "search_files": if (args.path !== undefined && args.regex !== undefined) { nativeArgs = { diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index f905600b9d0..1d69f39cc79 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -14,7 +14,6 @@ import type { ToolParamName, ToolResponse, ToolUse, McpToolUse } from "../../sha import { AskIgnoredError } from "../task/AskIgnoredError" import { Task } from "../task/Task" -import { fetchInstructionsTool } from "../tools/FetchInstructionsTool" import { listFilesTool } from "../tools/ListFilesTool" import { readFileTool } from "../tools/ReadFileTool" import { readCommandOutputTool } from "../tools/ReadCommandOutputTool" @@ -34,6 +33,7 @@ import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/Atte import { newTaskTool } from "../tools/NewTaskTool" import { updateTodoListTool } from "../tools/UpdateTodoListTool" import { runSlashCommandTool } from "../tools/RunSlashCommandTool" +import { skillTool } from "../tools/SkillTool" import { generateImageTool } from "../tools/GenerateImageTool" import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool" import { isValidToolName, validateToolUse } from "../tools/validateToolUse" @@ -347,8 +347,6 @@ export async function presentAssistantMessage(cline: Task) { return readFileTool.getReadFileToolDescription(block.name, block.nativeArgs) } return readFileTool.getReadFileToolDescription(block.name, block.params) - case "fetch_instructions": - return `[${block.name} for '${block.params.task}']` case "write_to_file": return `[${block.name} for '${block.params.path}']` case "apply_diff": @@ -394,6 +392,8 @@ export async function presentAssistantMessage(cline: Task) { } case "run_slash_command": return `[${block.name} for '${block.params.command}'${block.params.args ? ` with args: ${block.params.args}` : ""}]` + case "skill": + return `[${block.name} for '${block.params.skill}'${block.params.args ? ` with args: ${block.params.args}` : ""}]` case "generate_image": return `[${block.name} for '${block.params.path}']` default: @@ -760,13 +760,6 @@ export async function presentAssistantMessage(cline: Task) { pushToolResult, }) break - case "fetch_instructions": - await fetchInstructionsTool.handle(cline, block as ToolUse<"fetch_instructions">, { - askApproval, - handleError, - pushToolResult, - }) - break case "list_files": await listFilesTool.handle(cline, block as ToolUse<"list_files">, { askApproval, @@ -870,6 +863,13 @@ export async function presentAssistantMessage(cline: Task) { pushToolResult, }) break + case "skill": + await skillTool.handle(cline, block as ToolUse<"skill">, { + askApproval, + handleError, + pushToolResult, + }) + break case "generate_image": await checkpointSaveAndMark(cline) await generateImageTool.handle(cline, block as ToolUse<"generate_image">, { @@ -1049,7 +1049,6 @@ function containsXmlToolMarkup(text: string): boolean { "codebase_search", "edit_file", "execute_command", - "fetch_instructions", "generate_image", "list_files", "new_task", diff --git a/src/core/auto-approval/index.ts b/src/core/auto-approval/index.ts index f2951405010..f9de2ccfe36 100644 --- a/src/core/auto-approval/index.ts +++ b/src/core/auto-approval/index.ts @@ -151,14 +151,11 @@ export async function checkAutoApproval({ return { decision: "approve" } } - if (tool?.tool === "fetchInstructions") { - if (tool.content === "create_mode") { - return state.alwaysAllowModeSwitch === true ? { decision: "approve" } : { decision: "ask" } - } - - if (tool.content === "create_mcp_server") { - return state.alwaysAllowMcp === true ? { decision: "approve" } : { decision: "ask" } - } + // The skill tool only loads pre-defined instructions from built-in, global, or project skills. + // It does not read arbitrary files - skills must be explicitly installed/defined by the user. + // Auto-approval is intentional to provide a seamless experience when loading task instructions. + if (tool.tool === "skill") { + return { decision: "approve" } } if (tool?.tool === "switchMode") { diff --git a/src/core/prompts/__tests__/add-custom-instructions.spec.ts b/src/core/prompts/__tests__/add-custom-instructions.spec.ts index 79399f40b2b..b7813d0f5b8 100644 --- a/src/core/prompts/__tests__/add-custom-instructions.spec.ts +++ b/src/core/prompts/__tests__/add-custom-instructions.spec.ts @@ -211,7 +211,6 @@ describe("addCustomInstructions", () => { undefined, // customModes undefined, // globalCustomInstructions undefined, // experiments - true, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -233,7 +232,6 @@ describe("addCustomInstructions", () => { undefined, // customModes undefined, // globalCustomInstructions undefined, // experiments - true, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -257,7 +255,6 @@ describe("addCustomInstructions", () => { undefined, // customModes, undefined, // globalCustomInstructions undefined, // experiments - false, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -280,7 +277,6 @@ describe("addCustomInstructions", () => { undefined, // customModes, undefined, // globalCustomInstructions undefined, // experiments - true, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions true, // partialReadsEnabled diff --git a/src/core/prompts/__tests__/custom-system-prompt.spec.ts b/src/core/prompts/__tests__/custom-system-prompt.spec.ts index 5399b92c651..0ec2956b317 100644 --- a/src/core/prompts/__tests__/custom-system-prompt.spec.ts +++ b/src/core/prompts/__tests__/custom-system-prompt.spec.ts @@ -105,7 +105,6 @@ describe("File-Based Custom System Prompt", () => { undefined, // customModes undefined, // globalCustomInstructions undefined, // experiments - true, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -142,7 +141,6 @@ describe("File-Based Custom System Prompt", () => { undefined, // customModes undefined, // globalCustomInstructions undefined, // experiments - true, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -187,7 +185,6 @@ describe("File-Based Custom System Prompt", () => { undefined, // customModes undefined, // globalCustomInstructions undefined, // experiments - true, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled diff --git a/src/core/prompts/__tests__/system-prompt.spec.ts b/src/core/prompts/__tests__/system-prompt.spec.ts index d171e135077..91fb9350b4c 100644 --- a/src/core/prompts/__tests__/system-prompt.spec.ts +++ b/src/core/prompts/__tests__/system-prompt.spec.ts @@ -226,7 +226,6 @@ describe("SYSTEM_PROMPT", () => { undefined, // customModes undefined, // globalCustomInstructions experiments, - true, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -248,7 +247,6 @@ describe("SYSTEM_PROMPT", () => { undefined, // customModes, undefined, // globalCustomInstructions experiments, - true, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -272,7 +270,6 @@ describe("SYSTEM_PROMPT", () => { undefined, // customModes, undefined, // globalCustomInstructions experiments, - true, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -294,7 +291,6 @@ describe("SYSTEM_PROMPT", () => { undefined, // customModes, undefined, // globalCustomInstructions experiments, - true, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -316,7 +312,6 @@ describe("SYSTEM_PROMPT", () => { undefined, // customModes, undefined, // globalCustomInstructions experiments, - true, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -324,6 +319,7 @@ describe("SYSTEM_PROMPT", () => { expect(prompt).toMatchFileSnapshot("./__snapshots__/system-prompt/with-different-viewport-size.snap") }) + it("should include vscode language in custom instructions", async () => { // Mock vscode.env.language const vscode = vi.mocked(await import("vscode")) as any @@ -364,7 +360,6 @@ describe("SYSTEM_PROMPT", () => { undefined, // customModes undefined, // globalCustomInstructions undefined, // experiments - true, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -424,7 +419,6 @@ describe("SYSTEM_PROMPT", () => { customModes, // customModes "Global instructions", // globalCustomInstructions experiments, - true, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -461,7 +455,6 @@ describe("SYSTEM_PROMPT", () => { undefined, // customModes undefined, // globalCustomInstructions undefined, // experiments - false, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -493,7 +486,6 @@ describe("SYSTEM_PROMPT", () => { undefined, // customModes undefined, // globalCustomInstructions undefined, // experiments - false, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -523,7 +515,6 @@ describe("SYSTEM_PROMPT", () => { undefined, // customModes undefined, // globalCustomInstructions experiments, - true, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -555,7 +546,6 @@ describe("SYSTEM_PROMPT", () => { undefined, // customModes undefined, // globalCustomInstructions experiments, - true, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -587,7 +577,6 @@ describe("SYSTEM_PROMPT", () => { undefined, // customModes undefined, // globalCustomInstructions experiments, - true, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -619,7 +608,6 @@ describe("SYSTEM_PROMPT", () => { undefined, // customModes undefined, // globalCustomInstructions experiments, - true, // enableMcpServerCreation undefined, // language undefined, // rooIgnoreInstructions undefined, // partialReadsEnabled @@ -654,6 +642,7 @@ describe("SYSTEM_PROMPT", () => { expect(prompt).toContain("SYSTEM INFORMATION") expect(prompt).toContain("OBJECTIVE") }) + afterAll(() => { vi.restoreAllMocks() }) diff --git a/src/core/prompts/instructions/create-mode.ts b/src/core/prompts/instructions/create-mode.ts deleted file mode 100644 index 9623aae0cd1..00000000000 --- a/src/core/prompts/instructions/create-mode.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as path from "path" -import * as vscode from "vscode" - -import { GlobalFileNames } from "../../../shared/globalFileNames" -import { getSettingsDirectoryPath } from "../../../utils/storage" - -export async function createModeInstructions(context: vscode.ExtensionContext | undefined): Promise { - if (!context) throw new Error("Missing VSCode Extension Context") - - const settingsDir = await getSettingsDirectoryPath(context.globalStorageUri.fsPath) - const customModesPath = path.join(settingsDir, GlobalFileNames.customModes) - - return ` -Custom modes can be configured in two ways: - 1. Globally via '${customModesPath}' (created automatically on startup) - 2. Per-workspace via '.roomodes' in the workspace root directory - -When modes with the same slug exist in both files, the workspace-specific .roomodes version takes precedence. This allows projects to override global modes or define project-specific modes. - -If asked to create a project mode, create it in .roomodes in the workspace root. If asked to create a global mode, use the global custom modes file. - -- The following fields are required and must not be empty: - * slug: A valid slug (lowercase letters, numbers, and hyphens). Must be unique, and shorter is better. - * name: The display name for the mode - * roleDefinition: A detailed description of the mode's role and capabilities - * groups: Array of allowed tool groups (can be empty). Each group can be specified either as a string (e.g., "edit" to allow editing any file) or with file restrictions (e.g., ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }] to only allow editing markdown files) - -- The following fields are optional but highly recommended: - * description: A short, human-readable description of what this mode does (5 words) - * whenToUse: A clear description of when this mode should be selected and what types of tasks it's best suited for. This helps the Orchestrator mode make better decisions. - * customInstructions: Additional instructions for how the mode should operate - -- For multi-line text, include newline characters in the string like "This is the first line.\\nThis is the next line.\\n\\nThis is a double line break." - -Both files should follow this structure (in YAML format): - -customModes: - - slug: designer # Required: unique slug with lowercase letters, numbers, and hyphens - name: Designer # Required: mode display name - description: UI/UX design systems expert # Optional but recommended: short description (5 words) - roleDefinition: >- - You are Roo, a UI/UX expert specializing in design systems and frontend development. Your expertise includes: - - Creating and maintaining design systems - - Implementing responsive and accessible web interfaces - - Working with CSS, HTML, and modern frontend frameworks - - Ensuring consistent user experiences across platforms # Required: non-empty - whenToUse: >- - Use this mode when creating or modifying UI components, implementing design systems, - or ensuring responsive web interfaces. This mode is especially effective with CSS, - HTML, and modern frontend frameworks. # Optional but recommended - groups: # Required: array of tool groups (can be empty) - - read # Read files group (read_file, fetch_instructions, search_files, list_files) - - edit # Edit files group (apply_diff, write_to_file) - allows editing any file - # Or with file restrictions: - # - - edit - # - fileRegex: \\.md$ - # description: Markdown files only # Edit group that only allows editing markdown files - - browser # Browser group (browser_action) - - command # Command group (execute_command) - - mcp # MCP group (use_mcp_tool, access_mcp_resource) - customInstructions: Additional instructions for the Designer mode # Optional` -} diff --git a/src/core/prompts/instructions/instructions.ts b/src/core/prompts/instructions/instructions.ts deleted file mode 100644 index c1ff2a1899e..00000000000 --- a/src/core/prompts/instructions/instructions.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createMCPServerInstructions } from "./create-mcp-server" -import { createModeInstructions } from "./create-mode" -import { McpHub } from "../../../services/mcp/McpHub" -import { DiffStrategy } from "../../../shared/tools" -import * as vscode from "vscode" - -interface InstructionsDetail { - mcpHub?: McpHub - diffStrategy?: DiffStrategy - context?: vscode.ExtensionContext -} - -export async function fetchInstructions(text: string, detail: InstructionsDetail): Promise { - switch (text) { - case "create_mcp_server": { - return await createMCPServerInstructions(detail.mcpHub, detail.diffStrategy) - } - case "create_mode": { - return await createModeInstructions(detail.context) - } - default: { - return "" - } - } -} diff --git a/src/core/prompts/sections/modes.ts b/src/core/prompts/sections/modes.ts index 1925405aa87..5c4ea2cf53a 100644 --- a/src/core/prompts/sections/modes.ts +++ b/src/core/prompts/sections/modes.ts @@ -5,17 +5,14 @@ import type { ModeConfig } from "@roo-code/types" import { getAllModesWithPrompts } from "../../../shared/modes" import { ensureSettingsDirectoryExists } from "../../../utils/globalContext" -export async function getModesSection( - context: vscode.ExtensionContext, - skipXmlExamples: boolean = false, -): Promise { +export async function getModesSection(context: vscode.ExtensionContext): Promise { // Make sure path gets created await ensureSettingsDirectoryExists(context) // Get all modes with their overrides from extension state const allModes = await getAllModesWithPrompts(context) - let modesContent = `==== + const modesContent = `==== MODES @@ -34,18 +31,5 @@ ${allModes }) .join("\n")}` - if (!skipXmlExamples) { - modesContent += ` -If the user asks you to create or edit a new mode for this project, you should read the instructions by using the fetch_instructions tool, like this: - -create_mode - -` - } else { - modesContent += ` -If the user asks you to create or edit a new mode for this project, you should read the instructions by using the fetch_instructions tool. -` - } - return modesContent } diff --git a/src/core/prompts/sections/skills.ts b/src/core/prompts/sections/skills.ts index 53ba8b95f19..39cfca405b5 100644 --- a/src/core/prompts/sections/skills.ts +++ b/src/core/prompts/sections/skills.ts @@ -33,10 +33,11 @@ export async function getSkillsSection( .map((skill) => { const name = escapeXml(skill.name) const description = escapeXml(skill.description) - // Per the Agent Skills integration guidance for filesystem-based agents, - // location should be an absolute path to the SKILL.md file. - const location = escapeXml(skill.path) - return ` \n ${name}\n ${description}\n ${location}\n ` + // Only include location for file-based skills (not built-in) + // Built-in skills are loaded via the skill tool by name, not by path + const isFileBasedSkill = skill.source !== "built-in" && skill.path !== "built-in" + const locationLine = isFileBasedSkill ? `\n ${escapeXml(skill.path)}` : "" + return ` \n ${name}\n ${description}${locationLine}\n ` }) .join("\n") @@ -62,9 +63,9 @@ Step 2: Branching Decision - Select EXACTLY ONE skill. - Prefer the most specific skill when multiple skills match. -- Read the full SKILL.md file at the skill's . -- Load the SKILL.md contents fully into context BEFORE continuing. -- Follow the SKILL.md instructions precisely. +- Use the skill tool to load the skill by name. +- Load the skill's instructions fully into context BEFORE continuing. +- Follow the skill instructions precisely. - Do NOT respond outside the skill-defined flow. @@ -74,15 +75,15 @@ Step 2: Branching Decision CONSTRAINTS: -- Do NOT load every SKILL.md up front. -- Load SKILL.md ONLY after a skill is selected. +- Do NOT load every skill up front. +- Load skills ONLY after a skill is selected. - Do NOT skip this check. - FAILURE to perform this check is an error. -- When a SKILL.md is loaded, ONLY the contents of SKILL.md are present. -- Files linked from SKILL.md are NOT loaded automatically. +- When a skill is loaded, ONLY the skill instructions are present. +- Files linked from the skill are NOT loaded automatically. - The model MUST explicitly decide to read a linked file based on task relevance. - Do NOT assume the contents of linked files unless they have been explicitly read. - Prefer reading the minimum necessary linked file. diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 1f2a170ab75..4b66b36be35 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -53,7 +53,6 @@ async function generatePrompt( customModeConfigs?: ModeConfig[], globalCustomInstructions?: string, experiments?: Record, - enableMcpServerCreation?: boolean, language?: string, rooIgnoreInstructions?: string, partialReadsEnabled?: boolean, @@ -127,7 +126,6 @@ export const SYSTEM_PROMPT = async ( customModes?: ModeConfig[], globalCustomInstructions?: string, experiments?: Record, - enableMcpServerCreation?: boolean, language?: string, rooIgnoreInstructions?: string, partialReadsEnabled?: boolean, @@ -196,7 +194,6 @@ ${customInstructions}` customModes, globalCustomInstructions, experiments, - enableMcpServerCreation, language, rooIgnoreInstructions, partialReadsEnabled, diff --git a/src/core/prompts/tools/native-tools/fetch_instructions.ts b/src/core/prompts/tools/native-tools/fetch_instructions.ts deleted file mode 100644 index 86ab184c58d..00000000000 --- a/src/core/prompts/tools/native-tools/fetch_instructions.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type OpenAI from "openai" - -const FETCH_INSTRUCTIONS_DESCRIPTION = `Retrieve detailed instructions for performing a predefined task, such as creating an MCP server or creating a mode.` - -const TASK_PARAMETER_DESCRIPTION = `Task identifier to fetch instructions for` - -export default { - type: "function", - function: { - name: "fetch_instructions", - description: FETCH_INSTRUCTIONS_DESCRIPTION, - strict: true, - parameters: { - type: "object", - properties: { - task: { - type: "string", - description: TASK_PARAMETER_DESCRIPTION, - enum: ["create_mcp_server", "create_mode"], - }, - }, - required: ["task"], - additionalProperties: false, - }, - }, -} satisfies OpenAI.Chat.ChatCompletionTool diff --git a/src/core/prompts/tools/native-tools/index.ts b/src/core/prompts/tools/native-tools/index.ts index b6af18fa154..f23a7b2f28f 100644 --- a/src/core/prompts/tools/native-tools/index.ts +++ b/src/core/prompts/tools/native-tools/index.ts @@ -7,13 +7,13 @@ import attemptCompletion from "./attempt_completion" import browserAction from "./browser_action" import codebaseSearch from "./codebase_search" import executeCommand from "./execute_command" -import fetchInstructions from "./fetch_instructions" import generateImage from "./generate_image" import listFiles from "./list_files" import newTask from "./new_task" import readCommandOutput from "./read_command_output" import { createReadFileTool, type ReadFileToolOptions } from "./read_file" import runSlashCommand from "./run_slash_command" +import skill from "./skill" import searchAndReplace from "./search_and_replace" import searchReplace from "./search_replace" import edit_file from "./edit_file" @@ -62,13 +62,13 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch browserAction, codebaseSearch, executeCommand, - fetchInstructions, generateImage, listFiles, newTask, readCommandOutput, createReadFileTool(readFileOptions), runSlashCommand, + skill, searchAndReplace, searchReplace, edit_file, diff --git a/src/core/prompts/tools/native-tools/skill.ts b/src/core/prompts/tools/native-tools/skill.ts new file mode 100644 index 00000000000..98a2d98cc8d --- /dev/null +++ b/src/core/prompts/tools/native-tools/skill.ts @@ -0,0 +1,33 @@ +import type OpenAI from "openai" + +const SKILL_DESCRIPTION = `Load and execute a skill by name. Skills provide specialized instructions for common tasks like creating MCP servers or custom modes. + +Use this tool when you need to follow specific procedures documented in a skill. Available skills are listed in the AVAILABLE SKILLS section of the system prompt.` + +const SKILL_PARAMETER_DESCRIPTION = `Name of the skill to load (e.g., create-mcp-server, create-mode). Must match a skill name from the available skills list.` + +const ARGS_PARAMETER_DESCRIPTION = `Optional context or arguments to pass to the skill` + +export default { + type: "function", + function: { + name: "skill", + description: SKILL_DESCRIPTION, + strict: true, + parameters: { + type: "object", + properties: { + skill: { + type: "string", + description: SKILL_PARAMETER_DESCRIPTION, + }, + args: { + type: ["string", "null"], + description: ARGS_PARAMETER_DESCRIPTION, + }, + }, + required: ["skill", "args"], + additionalProperties: false, + }, + }, +} satisfies OpenAI.Chat.ChatCompletionTool diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 62d91e4a1e0..6bc2ef4ea71 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -3758,7 +3758,6 @@ export class Task extends EventEmitter implements TaskLike { customModePrompts, customInstructions, experiments, - enableMcpServerCreation, browserToolEnabled, language, maxConcurrentFileReads, @@ -3797,7 +3796,6 @@ export class Task extends EventEmitter implements TaskLike { customModes, customInstructions, experiments, - enableMcpServerCreation, language, rooIgnoreInstructions, maxReadFileLine !== -1, diff --git a/src/core/tools/FetchInstructionsTool.ts b/src/core/tools/FetchInstructionsTool.ts deleted file mode 100644 index f800e57fc4b..00000000000 --- a/src/core/tools/FetchInstructionsTool.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { type ClineSayTool } from "@roo-code/types" - -import { Task } from "../task/Task" -import { fetchInstructions } from "../prompts/instructions/instructions" -import { formatResponse } from "../prompts/responses" -import type { ToolUse } from "../../shared/tools" - -import { BaseTool, ToolCallbacks } from "./BaseTool" - -interface FetchInstructionsParams { - task: string -} - -export class FetchInstructionsTool extends BaseTool<"fetch_instructions"> { - readonly name = "fetch_instructions" as const - - async execute(params: FetchInstructionsParams, task: Task, callbacks: ToolCallbacks): Promise { - const { handleError, pushToolResult, askApproval } = callbacks - const { task: taskParam } = params - - try { - if (!taskParam) { - task.consecutiveMistakeCount++ - task.recordToolError("fetch_instructions") - task.didToolFailInCurrentTurn = true - pushToolResult(await task.sayAndCreateMissingParamError("fetch_instructions", "task")) - return - } - - task.consecutiveMistakeCount = 0 - - const completeMessage = JSON.stringify({ - tool: "fetchInstructions", - content: taskParam, - } satisfies ClineSayTool) - - const didApprove = await askApproval("tool", completeMessage) - - if (!didApprove) { - return - } - - // Now fetch the content and provide it to the agent. - const provider = task.providerRef.deref() - const mcpHub = provider?.getMcpHub() - - if (!mcpHub) { - throw new Error("MCP hub not available") - } - - const diffStrategy = task.diffStrategy - const context = provider?.context - const content = await fetchInstructions(taskParam, { mcpHub, diffStrategy, context }) - - if (!content) { - pushToolResult(formatResponse.toolError(`Invalid instructions request: ${taskParam}`)) - return - } - - pushToolResult(content) - } catch (error) { - await handleError("fetch instructions", error as Error) - } - } - - override async handlePartial(task: Task, block: ToolUse<"fetch_instructions">): Promise { - const taskParam: string | undefined = block.params.task - const sharedMessageProps: ClineSayTool = { tool: "fetchInstructions", content: taskParam } - - const partialMessage = JSON.stringify({ ...sharedMessageProps, content: undefined } satisfies ClineSayTool) - await task.ask("tool", partialMessage, block.partial).catch(() => {}) - } -} - -export const fetchInstructionsTool = new FetchInstructionsTool() diff --git a/src/core/tools/SkillTool.ts b/src/core/tools/SkillTool.ts new file mode 100644 index 00000000000..e346f9924c3 --- /dev/null +++ b/src/core/tools/SkillTool.ts @@ -0,0 +1,112 @@ +import { Task } from "../task/Task" +import { formatResponse } from "../prompts/responses" +import { BaseTool, ToolCallbacks } from "./BaseTool" +import type { ToolUse } from "../../shared/tools" + +interface SkillParams { + skill: string + args?: string | null +} + +export class SkillTool extends BaseTool<"skill"> { + readonly name = "skill" as const + + async execute(params: SkillParams, task: Task, callbacks: ToolCallbacks): Promise { + const { skill: skillName, args } = params + const { askApproval, handleError, pushToolResult } = callbacks + + try { + // Validate skill name parameter + if (!skillName) { + task.consecutiveMistakeCount++ + task.recordToolError("skill") + task.didToolFailInCurrentTurn = true + pushToolResult(await task.sayAndCreateMissingParamError("skill", "skill")) + return + } + + task.consecutiveMistakeCount = 0 + + // Get SkillsManager from provider + const provider = task.providerRef.deref() + const skillsManager = provider?.getSkillsManager() + + if (!skillsManager) { + task.recordToolError("skill") + task.didToolFailInCurrentTurn = true + pushToolResult(formatResponse.toolError("Skills Manager not available")) + return + } + + // Get current mode for skill resolution + const state = await provider?.getState() + const currentMode = state?.mode ?? "code" + + // Fetch skill content + const skillContent = await skillsManager.getSkillContent(skillName, currentMode) + + if (!skillContent) { + // Get available skills for error message + const availableSkills = skillsManager.getSkillsForMode(currentMode) + const skillNames = availableSkills.map((s) => s.name) + + task.recordToolError("skill") + task.didToolFailInCurrentTurn = true + pushToolResult( + formatResponse.toolError( + `Skill '${skillName}' not found. Available skills: ${skillNames.join(", ") || "(none)"}`, + ), + ) + return + } + + // Build approval message + const toolMessage = JSON.stringify({ + tool: "skill", + skill: skillName, + args: args, + source: skillContent.source, + description: skillContent.description, + }) + + const didApprove = await askApproval("tool", toolMessage) + + if (!didApprove) { + return + } + + // Build the result message + let result = `Skill: ${skillName}` + + if (skillContent.description) { + result += `\nDescription: ${skillContent.description}` + } + + if (args) { + result += `\nProvided arguments: ${args}` + } + + result += `\nSource: ${skillContent.source}` + result += `\n\n--- Skill Instructions ---\n\n${skillContent.instructions}` + + pushToolResult(result) + } catch (error) { + await handleError("executing skill", error as Error) + } + } + + override async handlePartial(task: Task, block: ToolUse<"skill">): Promise { + const skillName: string | undefined = block.params.skill + const args: string | undefined = block.params.args + + const partialMessage = JSON.stringify({ + tool: "skill", + skill: skillName, + args: args, + }) + + await task.ask("tool", partialMessage, block.partial).catch(() => {}) + } +} + +export const skillTool = new SkillTool() diff --git a/src/core/tools/__tests__/skillTool.spec.ts b/src/core/tools/__tests__/skillTool.spec.ts new file mode 100644 index 00000000000..fc1b3396e50 --- /dev/null +++ b/src/core/tools/__tests__/skillTool.spec.ts @@ -0,0 +1,345 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import { skillTool } from "../SkillTool" +import { Task } from "../../task/Task" +import { formatResponse } from "../../prompts/responses" +import type { ToolUse } from "../../../shared/tools" + +describe("skillTool", () => { + let mockTask: any + let mockCallbacks: any + let mockSkillsManager: any + + beforeEach(() => { + vi.clearAllMocks() + + mockSkillsManager = { + getSkillContent: vi.fn(), + getSkillsForMode: vi.fn().mockReturnValue([]), + } + + mockTask = { + consecutiveMistakeCount: 0, + recordToolError: vi.fn(), + didToolFailInCurrentTurn: false, + sayAndCreateMissingParamError: vi.fn().mockResolvedValue("Missing parameter error"), + ask: vi.fn().mockResolvedValue({}), + providerRef: { + deref: vi.fn().mockReturnValue({ + getState: vi.fn().mockResolvedValue({ mode: "code" }), + getSkillsManager: vi.fn().mockReturnValue(mockSkillsManager), + }), + }, + } + + mockCallbacks = { + askApproval: vi.fn().mockResolvedValue(true), + handleError: vi.fn(), + pushToolResult: vi.fn(), + } + }) + + it("should handle missing skill parameter", async () => { + const block: ToolUse<"skill"> = { + type: "tool_use" as const, + name: "skill" as const, + params: {}, + partial: false, + nativeArgs: { + skill: "", + }, + } + + await skillTool.handle(mockTask as Task, block, mockCallbacks) + + expect(mockTask.consecutiveMistakeCount).toBe(1) + expect(mockTask.recordToolError).toHaveBeenCalledWith("skill") + expect(mockTask.sayAndCreateMissingParamError).toHaveBeenCalledWith("skill", "skill") + expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith("Missing parameter error") + }) + + it("should handle skill not found", async () => { + const block: ToolUse<"skill"> = { + type: "tool_use" as const, + name: "skill" as const, + params: {}, + partial: false, + nativeArgs: { + skill: "non-existent", + }, + } + + mockSkillsManager.getSkillContent.mockResolvedValue(null) + mockSkillsManager.getSkillsForMode.mockReturnValue([{ name: "create-mcp-server" }]) + + await skillTool.handle(mockTask as Task, block, mockCallbacks) + + expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith( + formatResponse.toolError("Skill 'non-existent' not found. Available skills: create-mcp-server"), + ) + }) + + it("should handle empty available skills list", async () => { + const block: ToolUse<"skill"> = { + type: "tool_use" as const, + name: "skill" as const, + params: {}, + partial: false, + nativeArgs: { + skill: "non-existent", + }, + } + + mockSkillsManager.getSkillContent.mockResolvedValue(null) + mockSkillsManager.getSkillsForMode.mockReturnValue([]) + + await skillTool.handle(mockTask as Task, block, mockCallbacks) + + expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith( + formatResponse.toolError("Skill 'non-existent' not found. Available skills: (none)"), + ) + }) + + it("should successfully load built-in skill", async () => { + const block: ToolUse<"skill"> = { + type: "tool_use" as const, + name: "skill" as const, + params: {}, + partial: false, + nativeArgs: { + skill: "create-mcp-server", + }, + } + + const mockSkillContent = { + name: "create-mcp-server", + description: "Instructions for creating MCP servers", + source: "built-in", + instructions: "Step 1: Create the server...", + } + + mockSkillsManager.getSkillContent.mockResolvedValue(mockSkillContent) + + await skillTool.handle(mockTask as Task, block, mockCallbacks) + + expect(mockCallbacks.askApproval).toHaveBeenCalledWith( + "tool", + JSON.stringify({ + tool: "skill", + skill: "create-mcp-server", + args: undefined, + source: "built-in", + description: "Instructions for creating MCP servers", + }), + ) + + expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith( + `Skill: create-mcp-server +Description: Instructions for creating MCP servers +Source: built-in + +--- Skill Instructions --- + +Step 1: Create the server...`, + ) + }) + + it("should successfully load skill with arguments", async () => { + const block: ToolUse<"skill"> = { + type: "tool_use" as const, + name: "skill" as const, + params: {}, + partial: false, + nativeArgs: { + skill: "create-mcp-server", + args: "weather API server", + }, + } + + const mockSkillContent = { + name: "create-mcp-server", + description: "Instructions for creating MCP servers", + source: "built-in", + instructions: "Step 1: Create the server...", + } + + mockSkillsManager.getSkillContent.mockResolvedValue(mockSkillContent) + + await skillTool.handle(mockTask as Task, block, mockCallbacks) + + expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith( + `Skill: create-mcp-server +Description: Instructions for creating MCP servers +Provided arguments: weather API server +Source: built-in + +--- Skill Instructions --- + +Step 1: Create the server...`, + ) + }) + + it("should handle user rejection", async () => { + const block: ToolUse<"skill"> = { + type: "tool_use" as const, + name: "skill" as const, + params: {}, + partial: false, + nativeArgs: { + skill: "create-mcp-server", + }, + } + + mockSkillsManager.getSkillContent.mockResolvedValue({ + name: "create-mcp-server", + description: "Test", + source: "built-in", + instructions: "Test instructions", + }) + + mockCallbacks.askApproval.mockResolvedValue(false) + + await skillTool.handle(mockTask as Task, block, mockCallbacks) + + expect(mockCallbacks.pushToolResult).not.toHaveBeenCalled() + }) + + it("should handle partial block", async () => { + const block: ToolUse<"skill"> = { + type: "tool_use" as const, + name: "skill" as const, + params: { + skill: "create-mcp-server", + args: "", + }, + partial: true, + } + + await skillTool.handle(mockTask as Task, block, mockCallbacks) + + expect(mockTask.ask).toHaveBeenCalledWith( + "tool", + JSON.stringify({ + tool: "skill", + skill: "create-mcp-server", + args: "", + }), + true, + ) + + expect(mockCallbacks.pushToolResult).not.toHaveBeenCalled() + }) + + it("should handle errors during execution", async () => { + const block: ToolUse<"skill"> = { + type: "tool_use" as const, + name: "skill" as const, + params: {}, + partial: false, + nativeArgs: { + skill: "create-mcp-server", + }, + } + + const error = new Error("Test error") + mockSkillsManager.getSkillContent.mockRejectedValue(error) + + await skillTool.handle(mockTask as Task, block, mockCallbacks) + + expect(mockCallbacks.handleError).toHaveBeenCalledWith("executing skill", error) + }) + + it("should reset consecutive mistake count on valid skill", async () => { + const block: ToolUse<"skill"> = { + type: "tool_use" as const, + name: "skill" as const, + params: {}, + partial: false, + nativeArgs: { + skill: "create-mcp-server", + }, + } + + mockTask.consecutiveMistakeCount = 5 + + const mockSkillContent = { + name: "create-mcp-server", + description: "Test", + source: "built-in", + instructions: "Test instructions", + } + + mockSkillsManager.getSkillContent.mockResolvedValue(mockSkillContent) + + await skillTool.handle(mockTask as Task, block, mockCallbacks) + + expect(mockTask.consecutiveMistakeCount).toBe(0) + }) + + it("should handle Skills Manager not available", async () => { + const block: ToolUse<"skill"> = { + type: "tool_use" as const, + name: "skill" as const, + params: {}, + partial: false, + nativeArgs: { + skill: "create-mcp-server", + }, + } + + mockTask.providerRef.deref = vi.fn().mockReturnValue({ + getState: vi.fn().mockResolvedValue({ mode: "code" }), + getSkillsManager: vi.fn().mockReturnValue(undefined), + }) + + await skillTool.handle(mockTask as Task, block, mockCallbacks) + + expect(mockTask.recordToolError).toHaveBeenCalledWith("skill") + expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith( + formatResponse.toolError("Skills Manager not available"), + ) + }) + + it("should load project skill", async () => { + const block: ToolUse<"skill"> = { + type: "tool_use" as const, + name: "skill" as const, + params: {}, + partial: false, + nativeArgs: { + skill: "my-project-skill", + }, + } + + const mockSkillContent = { + name: "my-project-skill", + description: "A custom project skill", + source: "project", + instructions: "Follow these project-specific instructions...", + } + + mockSkillsManager.getSkillContent.mockResolvedValue(mockSkillContent) + + await skillTool.handle(mockTask as Task, block, mockCallbacks) + + expect(mockCallbacks.askApproval).toHaveBeenCalledWith( + "tool", + JSON.stringify({ + tool: "skill", + skill: "my-project-skill", + args: undefined, + source: "project", + description: "A custom project skill", + }), + ) + + expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith( + `Skill: my-project-skill +Description: A custom project skill +Source: project + +--- Skill Instructions --- + +Follow these project-specific instructions...`, + ) + }) +}) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index b101bee7d29..e722ce37f85 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2024,7 +2024,6 @@ export class ClineProvider terminalZshP10k, terminalZdotdir, mcpEnabled, - enableMcpServerCreation, currentApiConfigName, listApiConfigMeta, pinnedApiConfigs, @@ -2162,7 +2161,6 @@ export class ClineProvider terminalZshP10k: terminalZshP10k ?? false, terminalZdotdir: terminalZdotdir ?? false, mcpEnabled: mcpEnabled ?? true, - enableMcpServerCreation: enableMcpServerCreation ?? true, currentApiConfigName: currentApiConfigName ?? "default", listApiConfigMeta: listApiConfigMeta ?? [], pinnedApiConfigs: pinnedApiConfigs ?? {}, @@ -2408,7 +2406,6 @@ export class ClineProvider mode: stateValues.mode ?? defaultModeSlug, language: stateValues.language ?? formatLanguage(vscode.env.language), mcpEnabled: stateValues.mcpEnabled ?? true, - enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true, mcpServers: this.mcpHub?.getAllServers() ?? [], currentApiConfigName: stateValues.currentApiConfigName ?? "default", listApiConfigMeta: stateValues.listApiConfigMeta ?? [], diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index cacaf26004d..c08ff8cad92 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -315,6 +315,7 @@ vi.mock("../../../api/providers/fetchers/modelCache", () => ({ vi.mock("../diff/strategies/multi-search-replace", () => ({ MultiSearchReplaceDiffStrategy: vi.fn().mockImplementation(() => ({ + getToolDescription: () => "test", getName: () => "test-strategy", applyDiff: vi.fn(), })), @@ -557,7 +558,6 @@ describe("ClineProvider", () => { writeDelayMs: 1000, browserViewportSize: "900x600", mcpEnabled: true, - enableMcpServerCreation: false, mode: defaultModeSlug, customModes: [], experiments: experimentDefault, @@ -1349,7 +1349,6 @@ describe("ClineProvider", () => { apiProvider: "openrouter" as const, }, mcpEnabled: true, - enableMcpServerCreation: false, mode: "code" as const, experiments: experimentDefault, } as any) @@ -1374,7 +1373,6 @@ describe("ClineProvider", () => { apiProvider: "openrouter" as const, }, mcpEnabled: false, - enableMcpServerCreation: false, mode: "code" as const, experiments: experimentDefault, } as any) @@ -1431,38 +1429,6 @@ describe("ClineProvider", () => { ) }) - test("generates system prompt with various configurations", async () => { - await provider.resolveWebviewView(mockWebviewView) - - // Mock getState with typical configuration - vi.spyOn(provider, "getState").mockResolvedValue({ - apiConfiguration: { - apiProvider: "openrouter", - apiModelId: "test-model", - }, - customModePrompts: {}, - mode: "code", - enableMcpServerCreation: true, - mcpEnabled: false, - browserViewportSize: "900x600", - experiments: experimentDefault, - browserToolEnabled: true, - } as any) - - // Trigger getSystemPrompt - const handler = getMessageHandler() - await handler({ type: "getSystemPrompt", mode: "code" }) - - // Verify system prompt was generated and sent - expect(mockPostMessage).toHaveBeenCalledWith( - expect.objectContaining({ - type: "systemPrompt", - text: expect.any(String), - mode: "code", - }), - ) - }) - test("uses correct mode-specific instructions when mode is specified", async () => { await provider.resolveWebviewView(mockWebviewView) @@ -1475,7 +1441,6 @@ describe("ClineProvider", () => { architect: { customInstructions: "Architect mode instructions" }, }, mode: "architect", - enableMcpServerCreation: false, mcpEnabled: false, browserViewportSize: "900x600", experiments: experimentDefault, diff --git a/src/core/webview/__tests__/generateSystemPrompt.browser-capability.spec.ts b/src/core/webview/__tests__/generateSystemPrompt.browser-capability.spec.ts index 702c932fd5d..3b521c0f14b 100644 --- a/src/core/webview/__tests__/generateSystemPrompt.browser-capability.spec.ts +++ b/src/core/webview/__tests__/generateSystemPrompt.browser-capability.spec.ts @@ -60,7 +60,6 @@ function makeProviderStub() { browserViewportSize: "900x600", mcpEnabled: false, experiments: {}, - enableMcpServerCreation: false, browserToolEnabled: true, // critical: enabled in settings language: "en", maxReadFileLine: -1, diff --git a/src/core/webview/generateSystemPrompt.ts b/src/core/webview/generateSystemPrompt.ts index ed1ab9e2726..b6f77d3842c 100644 --- a/src/core/webview/generateSystemPrompt.ts +++ b/src/core/webview/generateSystemPrompt.ts @@ -17,7 +17,6 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web browserViewportSize, mcpEnabled, experiments, - enableMcpServerCreation, browserToolEnabled, language, maxReadFileLine, @@ -69,7 +68,6 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web customModes, customInstructions, experiments, - enableMcpServerCreation, language, rooIgnoreInstructions, maxReadFileLine !== -1, diff --git a/src/core/webview/skillsMessageHandler.ts b/src/core/webview/skillsMessageHandler.ts index 649c036b4cc..ae284a8d551 100644 --- a/src/core/webview/skillsMessageHandler.ts +++ b/src/core/webview/skillsMessageHandler.ts @@ -44,6 +44,11 @@ export async function handleCreateSkill( throw new Error(t("skills:errors.missing_create_fields")) } + // Built-in skills cannot be created + if (source === "built-in") { + throw new Error(t("skills:errors.cannot_modify_builtin")) + } + const skillsManager = provider.getSkillsManager() if (!skillsManager) { throw new Error(t("skills:errors.manager_unavailable")) @@ -82,6 +87,11 @@ export async function handleDeleteSkill( throw new Error(t("skills:errors.missing_delete_fields")) } + // Built-in skills cannot be deleted + if (source === "built-in") { + throw new Error(t("skills:errors.cannot_modify_builtin")) + } + const skillsManager = provider.getSkillsManager() if (!skillsManager) { throw new Error(t("skills:errors.manager_unavailable")) @@ -114,6 +124,11 @@ export async function handleOpenSkillFile(provider: ClineProvider, message: Webv throw new Error(t("skills:errors.missing_delete_fields")) } + // Built-in skills cannot be opened as files (they have no file path) + if (source === "built-in") { + throw new Error(t("skills:errors.cannot_open_builtin")) + } + const skillsManager = provider.getSkillsManager() if (!skillsManager) { throw new Error(t("skills:errors.manager_unavailable")) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 8246dda472e..051586b119d 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1444,10 +1444,6 @@ export const webviewMessageHandler = async ( } break } - case "enableMcpServerCreation": - await updateGlobalState("enableMcpServerCreation", message.bool ?? true) - await provider.postStateToWebview() - break case "remoteControlEnabled": try { await CloudService.instance.updateUserSettings({ extensionBridgeEnabled: message.bool ?? false }) @@ -2249,10 +2245,9 @@ export const webviewMessageHandler = async ( const yamlContent = await fs.readFile(fileUri[0].fsPath, "utf-8") // Import the mode with the specified source level - const result = await provider.customModesManager.importModeWithRules( - yamlContent, - message.source || "project", // Default to project if not specified - ) + // Note: "built-in" is not a valid source for importing modes + const importSource = message.source === "global" ? "global" : "project" + const result = await provider.customModesManager.importModeWithRules(yamlContent, importSource) if (result.success) { // Update state after importing diff --git a/src/i18n/locales/ca/skills.json b/src/i18n/locales/ca/skills.json index cce8bf45b0e..1879c78a49b 100644 --- a/src/i18n/locales/ca/skills.json +++ b/src/i18n/locales/ca/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "Falten camps obligatoris: skillName, source o skillDescription", "manager_unavailable": "El gestor d'habilitats no està disponible", "missing_delete_fields": "Falten camps obligatoris: skillName o source", - "skill_not_found": "No s'ha trobat l'habilitat \"{{name}}\"" + "skill_not_found": "No s'ha trobat l'habilitat \"{{name}}\"", + "cannot_modify_builtin": "Les habilitats integrades no es poden crear ni eliminar", + "cannot_open_builtin": "Les habilitats integrades no es poden obrir com a fitxers" } } diff --git a/src/i18n/locales/de/skills.json b/src/i18n/locales/de/skills.json index 90356110408..614cc9ed633 100644 --- a/src/i18n/locales/de/skills.json +++ b/src/i18n/locales/de/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "Erforderliche Felder fehlen: skillName, source oder skillDescription", "manager_unavailable": "Skill-Manager nicht verfügbar", "missing_delete_fields": "Erforderliche Felder fehlen: skillName oder source", - "skill_not_found": "Skill \"{{name}}\" nicht gefunden" + "skill_not_found": "Skill \"{{name}}\" nicht gefunden", + "cannot_modify_builtin": "Integrierte Skills können nicht erstellt oder gelöscht werden", + "cannot_open_builtin": "Integrierte Skills können nicht als Dateien geöffnet werden" } } diff --git a/src/i18n/locales/en/skills.json b/src/i18n/locales/en/skills.json index 9cf7369bc9b..3a86dc3293d 100644 --- a/src/i18n/locales/en/skills.json +++ b/src/i18n/locales/en/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "Missing required fields: skillName, source, or skillDescription", "manager_unavailable": "Skills manager not available", "missing_delete_fields": "Missing required fields: skillName or source", - "skill_not_found": "Skill \"{{name}}\" not found" + "skill_not_found": "Skill \"{{name}}\" not found", + "cannot_modify_builtin": "Built-in skills cannot be created or deleted", + "cannot_open_builtin": "Built-in skills cannot be opened as files" } } diff --git a/src/i18n/locales/es/skills.json b/src/i18n/locales/es/skills.json index d6d0727262f..c6e8aacebed 100644 --- a/src/i18n/locales/es/skills.json +++ b/src/i18n/locales/es/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "Faltan campos obligatorios: skillName, source o skillDescription", "manager_unavailable": "El gestor de habilidades no está disponible", "missing_delete_fields": "Faltan campos obligatorios: skillName o source", - "skill_not_found": "No se encontró la habilidad \"{{name}}\"" + "skill_not_found": "No se encontró la habilidad \"{{name}}\"", + "cannot_modify_builtin": "Las habilidades integradas no se pueden crear ni eliminar", + "cannot_open_builtin": "Las habilidades integradas no se pueden abrir como archivos" } } diff --git a/src/i18n/locales/fr/skills.json b/src/i18n/locales/fr/skills.json index 1337cb49b8e..6e1cc8e9d0c 100644 --- a/src/i18n/locales/fr/skills.json +++ b/src/i18n/locales/fr/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "Champs obligatoires manquants : skillName, source ou skillDescription", "manager_unavailable": "Le gestionnaire de compétences n'est pas disponible", "missing_delete_fields": "Champs obligatoires manquants : skillName ou source", - "skill_not_found": "Compétence \"{{name}}\" introuvable" + "skill_not_found": "Compétence \"{{name}}\" introuvable", + "cannot_modify_builtin": "Les compétences intégrées ne peuvent pas être créées ou supprimées", + "cannot_open_builtin": "Les compétences intégrées ne peuvent pas être ouvertes en tant que fichiers" } } diff --git a/src/i18n/locales/hi/skills.json b/src/i18n/locales/hi/skills.json index bd5235c24db..63191af5d05 100644 --- a/src/i18n/locales/hi/skills.json +++ b/src/i18n/locales/hi/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "आवश्यक फ़ील्ड गायब हैं: skillName, source, या skillDescription", "manager_unavailable": "स्किल मैनेजर उपलब्ध नहीं है", "missing_delete_fields": "आवश्यक फ़ील्ड गायब हैं: skillName या source", - "skill_not_found": "स्किल \"{{name}}\" नहीं मिला" + "skill_not_found": "स्किल \"{{name}}\" नहीं मिला", + "cannot_modify_builtin": "बिल्ट-इन स्किल्स को बनाया या हटाया नहीं जा सकता", + "cannot_open_builtin": "बिल्ट-इन स्किल्स को फाइलों के रूप में नहीं खोला जा सकता" } } diff --git a/src/i18n/locales/id/skills.json b/src/i18n/locales/id/skills.json index 0d9958a7744..379421b39c6 100644 --- a/src/i18n/locales/id/skills.json +++ b/src/i18n/locales/id/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "Bidang wajib tidak ada: skillName, source, atau skillDescription", "manager_unavailable": "Manajer skill tidak tersedia", "missing_delete_fields": "Bidang wajib tidak ada: skillName atau source", - "skill_not_found": "Skill \"{{name}}\" tidak ditemukan" + "skill_not_found": "Skill \"{{name}}\" tidak ditemukan", + "cannot_modify_builtin": "Skill bawaan tidak dapat dibuat atau dihapus", + "cannot_open_builtin": "Skill bawaan tidak dapat dibuka sebagai file" } } diff --git a/src/i18n/locales/it/skills.json b/src/i18n/locales/it/skills.json index fa0fe6559e8..4e7ac0495bb 100644 --- a/src/i18n/locales/it/skills.json +++ b/src/i18n/locales/it/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "Campi obbligatori mancanti: skillName, source o skillDescription", "manager_unavailable": "Il gestore delle skill non è disponibile", "missing_delete_fields": "Campi obbligatori mancanti: skillName o source", - "skill_not_found": "Skill \"{{name}}\" non trovata" + "skill_not_found": "Skill \"{{name}}\" non trovata", + "cannot_modify_builtin": "Le skill integrate non possono essere create o eliminate", + "cannot_open_builtin": "Le skill integrate non possono essere aperte come file" } } diff --git a/src/i18n/locales/ja/skills.json b/src/i18n/locales/ja/skills.json index baef99a5012..da8f3f8566b 100644 --- a/src/i18n/locales/ja/skills.json +++ b/src/i18n/locales/ja/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "必須フィールドが不足しています:skillName、source、またはskillDescription", "manager_unavailable": "スキルマネージャーが利用できません", "missing_delete_fields": "必須フィールドが不足しています:skillNameまたはsource", - "skill_not_found": "スキル「{{name}}」が見つかりません" + "skill_not_found": "スキル「{{name}}」が見つかりません", + "cannot_modify_builtin": "組み込みスキルは作成または削除できません", + "cannot_open_builtin": "組み込みスキルはファイルとして開けません" } } diff --git a/src/i18n/locales/ko/skills.json b/src/i18n/locales/ko/skills.json index 0561b10dbbb..040fcd2950b 100644 --- a/src/i18n/locales/ko/skills.json +++ b/src/i18n/locales/ko/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "필수 필드 누락: skillName, source 또는 skillDescription", "manager_unavailable": "스킬 관리자를 사용할 수 없습니다", "missing_delete_fields": "필수 필드 누락: skillName 또는 source", - "skill_not_found": "스킬 \"{{name}}\"을(를) 찾을 수 없습니다" + "skill_not_found": "스킬 \"{{name}}\"을(를) 찾을 수 없습니다", + "cannot_modify_builtin": "기본 제공 스킬은 생성하거나 삭제할 수 없습니다", + "cannot_open_builtin": "기본 제공 스킬은 파일로 열 수 없습니다" } } diff --git a/src/i18n/locales/nl/skills.json b/src/i18n/locales/nl/skills.json index 2a6fd2f733b..17375070901 100644 --- a/src/i18n/locales/nl/skills.json +++ b/src/i18n/locales/nl/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "Vereiste velden ontbreken: skillName, source of skillDescription", "manager_unavailable": "Vaardigheidenbeheerder niet beschikbaar", "missing_delete_fields": "Vereiste velden ontbreken: skillName of source", - "skill_not_found": "Vaardigheid \"{{name}}\" niet gevonden" + "skill_not_found": "Vaardigheid \"{{name}}\" niet gevonden", + "cannot_modify_builtin": "Ingebouwde vaardigheden kunnen niet worden aangemaakt of verwijderd", + "cannot_open_builtin": "Ingebouwde vaardigheden kunnen niet als bestanden worden geopend" } } diff --git a/src/i18n/locales/pl/skills.json b/src/i18n/locales/pl/skills.json index 6f0fb3a7d48..dbbb883d013 100644 --- a/src/i18n/locales/pl/skills.json +++ b/src/i18n/locales/pl/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "Brakuje wymaganych pól: skillName, source lub skillDescription", "manager_unavailable": "Menedżer umiejętności niedostępny", "missing_delete_fields": "Brakuje wymaganych pól: skillName lub source", - "skill_not_found": "Nie znaleziono umiejętności \"{{name}}\"" + "skill_not_found": "Nie znaleziono umiejętności \"{{name}}\"", + "cannot_modify_builtin": "Wbudowane umiejętności nie mogą być tworzone ani usuwane", + "cannot_open_builtin": "Wbudowane umiejętności nie mogą być otwierane jako pliki" } } diff --git a/src/i18n/locales/pt-BR/skills.json b/src/i18n/locales/pt-BR/skills.json index b54655f483c..9ed29abfdec 100644 --- a/src/i18n/locales/pt-BR/skills.json +++ b/src/i18n/locales/pt-BR/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "Campos obrigatórios ausentes: skillName, source ou skillDescription", "manager_unavailable": "Gerenciador de habilidades não disponível", "missing_delete_fields": "Campos obrigatórios ausentes: skillName ou source", - "skill_not_found": "Habilidade \"{{name}}\" não encontrada" + "skill_not_found": "Habilidade \"{{name}}\" não encontrada", + "cannot_modify_builtin": "Habilidades integradas não podem ser criadas ou excluídas", + "cannot_open_builtin": "Habilidades integradas não podem ser abertas como arquivos" } } diff --git a/src/i18n/locales/ru/skills.json b/src/i18n/locales/ru/skills.json index 7feee5d6c29..2429e9a17ac 100644 --- a/src/i18n/locales/ru/skills.json +++ b/src/i18n/locales/ru/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "Отсутствуют обязательные поля: skillName, source или skillDescription", "manager_unavailable": "Менеджер навыков недоступен", "missing_delete_fields": "Отсутствуют обязательные поля: skillName или source", - "skill_not_found": "Навык \"{{name}}\" не найден" + "skill_not_found": "Навык \"{{name}}\" не найден", + "cannot_modify_builtin": "Встроенные навыки нельзя создавать или удалять", + "cannot_open_builtin": "Встроенные навыки нельзя открыть как файлы" } } diff --git a/src/i18n/locales/tr/skills.json b/src/i18n/locales/tr/skills.json index 2e01d37378a..eadab29a13f 100644 --- a/src/i18n/locales/tr/skills.json +++ b/src/i18n/locales/tr/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "Gerekli alanlar eksik: skillName, source veya skillDescription", "manager_unavailable": "Beceri yöneticisi kullanılamıyor", "missing_delete_fields": "Gerekli alanlar eksik: skillName veya source", - "skill_not_found": "\"{{name}}\" becerisi bulunamadı" + "skill_not_found": "\"{{name}}\" becerisi bulunamadı", + "cannot_modify_builtin": "Yerleşik beceriler oluşturulamaz veya silinemez", + "cannot_open_builtin": "Yerleşik beceriler dosya olarak açılamaz" } } diff --git a/src/i18n/locales/vi/skills.json b/src/i18n/locales/vi/skills.json index bc3074a1c84..b36131b6bf6 100644 --- a/src/i18n/locales/vi/skills.json +++ b/src/i18n/locales/vi/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "Thiếu các trường bắt buộc: skillName, source hoặc skillDescription", "manager_unavailable": "Trình quản lý kỹ năng không khả dụng", "missing_delete_fields": "Thiếu các trường bắt buộc: skillName hoặc source", - "skill_not_found": "Không tìm thấy kỹ năng \"{{name}}\"" + "skill_not_found": "Không tìm thấy kỹ năng \"{{name}}\"", + "cannot_modify_builtin": "Không thể tạo hoặc xóa kỹ năng tích hợp sẵn", + "cannot_open_builtin": "Không thể mở kỹ năng tích hợp sẵn dưới dạng tệp" } } diff --git a/src/i18n/locales/zh-CN/skills.json b/src/i18n/locales/zh-CN/skills.json index 629aaeb6d74..46885cb0d4c 100644 --- a/src/i18n/locales/zh-CN/skills.json +++ b/src/i18n/locales/zh-CN/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "缺少必填字段:skillName、source 或 skillDescription", "manager_unavailable": "技能管理器不可用", "missing_delete_fields": "缺少必填字段:skillName 或 source", - "skill_not_found": "未找到技能 \"{{name}}\"" + "skill_not_found": "未找到技能 \"{{name}}\"", + "cannot_modify_builtin": "内置技能无法创建或删除", + "cannot_open_builtin": "内置技能无法作为文件打开" } } diff --git a/src/i18n/locales/zh-TW/skills.json b/src/i18n/locales/zh-TW/skills.json index 10705f4cce8..0ff0969e578 100644 --- a/src/i18n/locales/zh-TW/skills.json +++ b/src/i18n/locales/zh-TW/skills.json @@ -9,6 +9,8 @@ "missing_create_fields": "缺少必填欄位:skillName、source 或 skillDescription", "manager_unavailable": "技能管理器無法使用", "missing_delete_fields": "缺少必填欄位:skillName 或 source", - "skill_not_found": "找不到技能「{{name}}」" + "skill_not_found": "找不到技能「{{name}}」", + "cannot_modify_builtin": "內建技能無法建立或刪除", + "cannot_open_builtin": "內建技能無法作為檔案開啟" } } diff --git a/src/package.json b/src/package.json index 736ffdea131..acea49056af 100644 --- a/src/package.json +++ b/src/package.json @@ -439,6 +439,8 @@ "pretest": "turbo run bundle --cwd ..", "test": "vitest run", "format": "prettier --write .", + "generate:skills": "tsx services/skills/generate-built-in-skills.ts", + "prebundle": "pnpm generate:skills", "bundle": "node esbuild.mjs", "vscode:prepublish": "pnpm bundle --production", "vsix": "mkdirp ../bin && vsce package --no-dependencies --out ../bin", diff --git a/src/services/skills/SkillsManager.ts b/src/services/skills/SkillsManager.ts index f9d27255f81..6f7eeadc736 100644 --- a/src/services/skills/SkillsManager.ts +++ b/src/services/skills/SkillsManager.ts @@ -15,6 +15,7 @@ import { SKILL_NAME_MAX_LENGTH, } from "@roo-code/types" import { t } from "../../i18n" +import { getBuiltInSkills, getBuiltInSkillContent } from "./built-in-skills" // Re-export for convenience export type { SkillMetadata, SkillContent } @@ -159,13 +160,19 @@ export class SkillsManager { /** * Get skills available for the current mode. - * Resolves overrides: project > global, mode-specific > generic. + * Resolves overrides: project > global > built-in, mode-specific > generic. * * @param currentMode - The current mode slug (e.g., 'code', 'architect') */ getSkillsForMode(currentMode: string): SkillMetadata[] { const resolvedSkills = new Map() + // First, add built-in skills (lowest priority) + for (const skill of getBuiltInSkills()) { + resolvedSkills.set(skill.name, skill) + } + + // Then, add discovered skills (will override built-in skills with same name) for (const skill of this.skills.values()) { // Skip mode-specific skills that don't match current mode if (skill.mode && skill.mode !== currentMode) continue @@ -189,12 +196,22 @@ export class SkillsManager { /** * Determine if newSkill should override existingSkill based on priority rules. - * Priority: project > global, mode-specific > generic + * Priority: project > global > built-in, mode-specific > generic */ private shouldOverrideSkill(existing: SkillMetadata, newSkill: SkillMetadata): boolean { - // Project always overrides global - if (newSkill.source === "project" && existing.source === "global") return true - if (newSkill.source === "global" && existing.source === "project") return false + // Define source priority: project > global > built-in + const sourcePriority: Record = { + project: 3, + global: 2, + "built-in": 1, + } + + const existingPriority = sourcePriority[existing.source] ?? 0 + const newPriority = sourcePriority[newSkill.source] ?? 0 + + // Higher priority source always wins + if (newPriority > existingPriority) return true + if (newPriority < existingPriority) return false // Same source: mode-specific overrides generic if (newSkill.mode && !existing.mode) return true @@ -219,12 +236,21 @@ export class SkillsManager { const modeSkills = this.getSkillsForMode(currentMode) skill = modeSkills.find((s) => s.name === name) } else { - // Fall back to any skill with this name + // Fall back to any skill with this name (check discovered skills first, then built-in) skill = Array.from(this.skills.values()).find((s) => s.name === name) + if (!skill) { + skill = getBuiltInSkills().find((s) => s.name === name) + } } if (!skill) return null + // For built-in skills, use the built-in content + if (skill.source === "built-in") { + return getBuiltInSkillContent(name) + } + + // For file-based skills, read from disk const fileContent = await fs.readFile(skill.path, "utf-8") const { content: body } = matter(fileContent) diff --git a/src/services/skills/__tests__/SkillsManager.spec.ts b/src/services/skills/__tests__/SkillsManager.spec.ts index 858c74fb5d7..407c8353c13 100644 --- a/src/services/skills/__tests__/SkillsManager.spec.ts +++ b/src/services/skills/__tests__/SkillsManager.spec.ts @@ -98,6 +98,14 @@ vi.mock("../../../i18n", () => ({ }, })) +// Mock built-in skills to isolate tests from actual built-in skills +vi.mock("../built-in-skills", () => ({ + getBuiltInSkills: () => [], + getBuiltInSkillContent: () => null, + isBuiltInSkill: () => false, + getBuiltInSkillNames: () => [], +})) + import { SkillsManager } from "../SkillsManager" import { ClineProvider } from "../../../core/webview/ClineProvider" diff --git a/src/services/skills/__tests__/generate-built-in-skills.spec.ts b/src/services/skills/__tests__/generate-built-in-skills.spec.ts new file mode 100644 index 00000000000..10b44b87163 --- /dev/null +++ b/src/services/skills/__tests__/generate-built-in-skills.spec.ts @@ -0,0 +1,175 @@ +/** + * Tests for the built-in skills generation script validation logic. + * + * Note: These tests focus on the validation functions since the main script + * is designed to be run as a CLI tool. The actual generation is tested + * via the integration with the build process. + */ + +describe("generate-built-in-skills validation", () => { + describe("validateSkillName", () => { + // Validation function extracted from the generation script + function validateSkillName(name: string): string[] { + const errors: string[] = [] + + if (name.length < 1 || name.length > 64) { + errors.push(`Name must be 1-64 characters (got ${name.length})`) + } + + const nameFormat = /^[a-z0-9]+(?:-[a-z0-9]+)*$/ + if (!nameFormat.test(name)) { + errors.push( + "Name must be lowercase letters/numbers/hyphens only (no leading/trailing hyphen, no consecutive hyphens)", + ) + } + + return errors + } + + it("should accept valid skill names", () => { + expect(validateSkillName("mcp-builder")).toHaveLength(0) + expect(validateSkillName("create-mode")).toHaveLength(0) + expect(validateSkillName("pdf-processing")).toHaveLength(0) + expect(validateSkillName("a")).toHaveLength(0) + expect(validateSkillName("skill123")).toHaveLength(0) + expect(validateSkillName("my-skill-v2")).toHaveLength(0) + }) + + it("should reject names with uppercase letters", () => { + const errors = validateSkillName("Create-MCP-Server") + expect(errors).toHaveLength(1) + expect(errors[0]).toContain("lowercase") + }) + + it("should reject names with leading hyphen", () => { + const errors = validateSkillName("-my-skill") + expect(errors).toHaveLength(1) + expect(errors[0]).toContain("leading/trailing hyphen") + }) + + it("should reject names with trailing hyphen", () => { + const errors = validateSkillName("my-skill-") + expect(errors).toHaveLength(1) + expect(errors[0]).toContain("leading/trailing hyphen") + }) + + it("should reject names with consecutive hyphens", () => { + const errors = validateSkillName("my--skill") + expect(errors).toHaveLength(1) + expect(errors[0]).toContain("consecutive hyphens") + }) + + it("should reject empty names", () => { + const errors = validateSkillName("") + expect(errors.length).toBeGreaterThan(0) + }) + + it("should reject names longer than 64 characters", () => { + const longName = "a".repeat(65) + const errors = validateSkillName(longName) + expect(errors).toHaveLength(1) + expect(errors[0]).toContain("1-64 characters") + }) + + it("should reject names with special characters", () => { + expect(validateSkillName("my_skill").length).toBeGreaterThan(0) + expect(validateSkillName("my.skill").length).toBeGreaterThan(0) + expect(validateSkillName("my skill").length).toBeGreaterThan(0) + }) + }) + + describe("validateDescription", () => { + // Validation function extracted from the generation script + function validateDescription(description: string): string[] { + const errors: string[] = [] + const trimmed = description.trim() + + if (trimmed.length < 1 || trimmed.length > 1024) { + errors.push(`Description must be 1-1024 characters (got ${trimmed.length})`) + } + + return errors + } + + it("should accept valid descriptions", () => { + expect(validateDescription("A short description")).toHaveLength(0) + expect(validateDescription("x")).toHaveLength(0) + expect(validateDescription("x".repeat(1024))).toHaveLength(0) + }) + + it("should reject empty descriptions", () => { + const errors = validateDescription("") + expect(errors).toHaveLength(1) + expect(errors[0]).toContain("1-1024 characters") + }) + + it("should reject whitespace-only descriptions", () => { + const errors = validateDescription(" ") + expect(errors).toHaveLength(1) + expect(errors[0]).toContain("got 0") + }) + + it("should reject descriptions longer than 1024 characters", () => { + const longDesc = "x".repeat(1025) + const errors = validateDescription(longDesc) + expect(errors).toHaveLength(1) + expect(errors[0]).toContain("got 1025") + }) + }) + + describe("escapeForTemplateLiteral", () => { + // Escape function extracted from the generation script + function escapeForTemplateLiteral(str: string): string { + return str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${") + } + + it("should escape backticks", () => { + expect(escapeForTemplateLiteral("code `example`")).toBe("code \\`example\\`") + }) + + it("should escape template literal interpolation", () => { + expect(escapeForTemplateLiteral("value: ${foo}")).toBe("value: \\${foo}") + }) + + it("should escape backslashes", () => { + expect(escapeForTemplateLiteral("path\\to\\file")).toBe("path\\\\to\\\\file") + }) + + it("should handle combined escapes", () => { + const input = "const x = `${value}`" + const expected = "const x = \\`\\${value}\\`" + expect(escapeForTemplateLiteral(input)).toBe(expected) + }) + }) +}) + +describe("built-in skills integration", () => { + it("should have valid skill names matching directory names", async () => { + // Import the generated built-in skills + const { getBuiltInSkills, getBuiltInSkillContent } = await import("../built-in-skills") + + const skills = getBuiltInSkills() + + // Verify we have the expected skills + const skillNames = skills.map((s) => s.name) + expect(skillNames).toContain("create-mcp-server") + expect(skillNames).toContain("create-mode") + + // Verify each skill has valid content + for (const skill of skills) { + expect(skill.source).toBe("built-in") + expect(skill.path).toBe("built-in") + + const content = getBuiltInSkillContent(skill.name) + expect(content).not.toBeNull() + expect(content!.instructions.length).toBeGreaterThan(0) + } + }) + + it("should return null for non-existent skills", async () => { + const { getBuiltInSkillContent } = await import("../built-in-skills") + + const content = getBuiltInSkillContent("non-existent-skill") + expect(content).toBeNull() + }) +}) diff --git a/src/services/skills/built-in-skills.ts b/src/services/skills/built-in-skills.ts new file mode 100644 index 00000000000..2a2ca38d391 --- /dev/null +++ b/src/services/skills/built-in-skills.ts @@ -0,0 +1,421 @@ +/** + * AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY + * + * This file is generated by generate-built-in-skills.ts from the SKILL.md files + * in the built-in/ directory. To modify built-in skills, edit the corresponding + * SKILL.md file and run: pnpm generate:skills + */ + +import { SkillMetadata, SkillContent } from "../../shared/skills" + +interface BuiltInSkillDefinition { + name: string + description: string + instructions: string +} + +const BUILT_IN_SKILLS: Record = { + "create-mcp-server": { + name: "create-mcp-server", + description: + "Instructions for creating MCP (Model Context Protocol) servers that expose tools and resources for the agent to use. Use when the user asks to create a new MCP server or add MCP capabilities.", + instructions: `You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with \`use_mcp_tool\` and \`access_mcp_resource\`. + +When creating MCP servers, it's important to understand that they operate in a non-interactive environment. The server cannot initiate OAuth flows, open browser windows, or prompt for user input during runtime. All credentials and authentication tokens must be provided upfront through environment variables in the MCP settings configuration. For example, Spotify's API uses OAuth to get a refresh token for the user, but the MCP server cannot initiate this flow. While you can walk the user through obtaining an application client ID and secret, you may have to create a separate one-time setup script (like get-refresh-token.js) that captures and logs the final piece of the puzzle: the user's refresh token (i.e. you might run the script using execute_command which would open a browser for authentication, and then log the refresh token so that you can see it in the command output for you to use in the MCP settings configuration). + +Unless the user specifies otherwise, new local MCP servers should be created in your MCP servers directory. You can find the path to this directory by checking the MCP settings file, or ask the user where they'd like the server created. + +### MCP Server Types and Configuration + +MCP servers can be configured in two ways in the MCP settings file: + +1. Local (Stdio) Server Configuration: + +\`\`\`json +{ + "mcpServers": { + "local-weather": { + "command": "node", + "args": ["/path/to/weather-server/build/index.js"], + "env": { + "OPENWEATHER_API_KEY": "your-api-key" + } + } + } +} +\`\`\` + +2. Remote (SSE) Server Configuration: + +\`\`\`json +{ + "mcpServers": { + "remote-weather": { + "url": "https://api.example.com/mcp", + "headers": { + "Authorization": "Bearer your-api-key" + } + } + } +} +\`\`\` + +Common configuration options for both types: + +- \`disabled\`: (optional) Set to true to temporarily disable the server +- \`timeout\`: (optional) Maximum time in seconds to wait for server responses (default: 60) +- \`alwaysAllow\`: (optional) Array of tool names that don't require user confirmation +- \`disabledTools\`: (optional) Array of tool names that are not included in the system prompt and won't be used + +### Example Local MCP Server + +For example, if the user wanted to give you the ability to retrieve weather information, you could create an MCP server that uses the OpenWeather API to get weather information, add it to the MCP settings configuration file, and then notice that you now have access to new tools and resources in the system prompt that you might use to show the user your new capabilities. + +The following example demonstrates how to build a local MCP server that provides weather data functionality using the Stdio transport. While this example shows how to implement resources, resource templates, and tools, in practice you should prefer using tools since they are more flexible and can handle dynamic parameters. The resource and resource template implementations are included here mainly for demonstration purposes of the different MCP capabilities, but a real weather server would likely just expose tools for fetching weather data. (The following steps are for macOS) + +1. Use the \`create-typescript-server\` tool to bootstrap a new project in your MCP servers directory: + +\`\`\`bash +cd /path/to/your/mcp-servers +npx @modelcontextprotocol/create-server weather-server +cd weather-server +# Install dependencies +npm install axios zod @modelcontextprotocol/sdk +\`\`\` + +This will create a new project with the following structure: + +\`\`\` +weather-server/ + ├── package.json + { + ... + "type": "module", // added by default, uses ES module syntax (import/export) rather than CommonJS (require/module.exports) (Important to know if you create additional scripts in this server repository like a get-refresh-token.js script) + "scripts": { + "build": "tsc && node -e \\"require('fs').chmodSync('build/index.js', '755')\\"", + ... + } + ... + } + ├── tsconfig.json + └── src/ + └── index.ts # Main server implementation +\`\`\` + +2. Replace \`src/index.ts\` with the following: + +\`\`\`typescript +#!/usr/bin/env node +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js" +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" +import { z } from "zod" +import axios from "axios" + +const API_KEY = process.env.OPENWEATHER_API_KEY // provided by MCP config +if (!API_KEY) { + throw new Error("OPENWEATHER_API_KEY environment variable is required") +} + +// Define types for OpenWeather API responses +interface WeatherData { + main: { + temp: number + humidity: number + } + weather: Array<{ + description: string + }> + wind: { + speed: number + } +} + +interface ForecastData { + list: Array< + WeatherData & { + dt_txt: string + } + > +} + +// Create an MCP server +const server = new McpServer({ + name: "weather-server", + version: "0.1.0", +}) + +// Create axios instance for OpenWeather API +const weatherApi = axios.create({ + baseURL: "http://api.openweathermap.org/data/2.5", + params: { + appid: API_KEY, + units: "metric", + }, +}) + +// Add a tool for getting weather forecasts +server.tool( + "get_forecast", + { + city: z.string().describe("City name"), + days: z.number().min(1).max(5).optional().describe("Number of days (1-5)"), + }, + async ({ city, days = 3 }) => { + try { + const response = await weatherApi.get("forecast", { + params: { + q: city, + cnt: Math.min(days, 5) * 8, + }, + }) + + return { + content: [ + { + type: "text", + text: JSON.stringify(response.data.list, null, 2), + }, + ], + } + } catch (error) { + if (axios.isAxiosError(error)) { + return { + content: [ + { + type: "text", + text: \`Weather API error: \${error.response?.data.message ?? error.message}\`, + }, + ], + isError: true, + } + } + throw error + } + }, +) + +// Add a resource for current weather in San Francisco +server.resource("sf_weather", { uri: "weather://San Francisco/current", list: true }, async (uri) => { + try { + const response = weatherApi.get("weather", { + params: { q: "San Francisco" }, + }) + + return { + contents: [ + { + uri: uri.href, + mimeType: "application/json", + text: JSON.stringify( + { + temperature: response.data.main.temp, + conditions: response.data.weather[0].description, + humidity: response.data.main.humidity, + wind_speed: response.data.wind.speed, + timestamp: new Date().toISOString(), + }, + null, + 2, + ), + }, + ], + } + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error(\`Weather API error: \${error.response?.data.message ?? error.message}\`) + } + throw error + } +}) + +// Add a dynamic resource template for current weather by city +server.resource( + "current_weather", + new ResourceTemplate("weather://{city}/current", { list: true }), + async (uri, { city }) => { + try { + const response = await weatherApi.get("weather", { + params: { q: city }, + }) + + return { + contents: [ + { + uri: uri.href, + mimeType: "application/json", + text: JSON.stringify( + { + temperature: response.data.main.temp, + conditions: response.data.weather[0].description, + humidity: response.data.main.humidity, + wind_speed: response.data.wind.speed, + timestamp: new Date().toISOString(), + }, + null, + 2, + ), + }, + ], + } + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error(\`Weather API error: \${error.response?.data.message ?? error.message}\`) + } + throw error + } + }, +) + +// Start receiving messages on stdin and sending messages on stdout +const transport = new StdioServerTransport() +await server.connect(transport) +console.error("Weather MCP server running on stdio") +\`\`\` + +(Remember: This is just an example–you may use different dependencies, break the implementation up into multiple files, etc.) + +3. Build and compile the executable JavaScript file + +\`\`\`bash +npm run build +\`\`\` + +4. Whenever you need an environment variable such as an API key to configure the MCP server, walk the user through the process of getting the key. For example, they may need to create an account and go to a developer dashboard to generate the key. Provide step-by-step instructions and URLs to make it easy for the user to retrieve the necessary information. Then use the ask_followup_question tool to ask the user for the key, in this case the OpenWeather API key. + +5. Install the MCP Server by adding the MCP server configuration to the MCP settings file. On macOS/Linux this is typically at \`~/.roo-code/settings/mcp_settings.json\`, on Windows at \`%APPDATA%\\roo-code\\settings\\mcp_settings.json\`. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing \`mcpServers\` object. + +IMPORTANT: Regardless of what else you see in the MCP settings file, you must default any new MCP servers you create to disabled=false, alwaysAllow=[] and disabledTools=[]. + +\`\`\`json +{ + "mcpServers": { + ..., + "weather": { + "command": "node", + "args": ["/path/to/weather-server/build/index.js"], + "env": { + "OPENWEATHER_API_KEY": "user-provided-api-key" + } + }, + } +} +\`\`\` + +(Note: the user may also ask you to install the MCP server to the Claude desktop app, in which case you would read then modify \`~/Library/Application\\ Support/Claude/claude_desktop_config.json\` on macOS for example. It follows the same format of a top level \`mcpServers\` object.) + +6. After you have edited the MCP settings configuration file, the system will automatically run all the servers and expose the available tools and resources in the 'Connected MCP Servers' section. + +7. Now that you have access to these new tools and resources, you may suggest ways the user can command you to invoke them - for example, with this new weather tool now available, you can invite the user to ask "what's the weather in San Francisco?" + +## Editing MCP Servers + +The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' in the system prompt), e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use write_to_file or apply_diff to make changes to the files. + +However some MCP servers may be running from installed packages rather than a local repository, in which case it may make more sense to create a new MCP server. + +# MCP Servers Are Not Always Necessary + +The user may not always request the use or creation of MCP servers. Instead, they might provide tasks that can be completed with existing tools. While using the MCP SDK to extend your capabilities can be useful, it's important to understand that this is just one specialized type of task you can accomplish. You should only implement MCP servers when the user explicitly requests it (e.g., "add a tool that..."). + +Remember: The MCP documentation and example provided above are to help you understand and work with existing MCP servers or create new ones when requested by the user. You already have access to tools and capabilities that can be used to accomplish a wide range of tasks.`, + }, + "create-mode": { + name: "create-mode", + description: + "Instructions for creating custom modes in Roo Code. Use when the user asks to create a new mode, edit an existing mode, or configure mode settings.", + instructions: `Custom modes can be configured in two ways: + +1. Globally via the custom modes file in your Roo Code settings directory (typically ~/.roo-code/settings/custom_modes.yaml on macOS/Linux or %APPDATA%\\roo-code\\settings\\custom_modes.yaml on Windows) - created automatically on startup +2. Per-workspace via '.roomodes' in the workspace root directory + +When modes with the same slug exist in both files, the workspace-specific .roomodes version takes precedence. This allows projects to override global modes or define project-specific modes. + +If asked to create a project mode, create it in .roomodes in the workspace root. If asked to create a global mode, use the global custom modes file. + +- The following fields are required and must not be empty: + + - slug: A valid slug (lowercase letters, numbers, and hyphens). Must be unique, and shorter is better. + - name: The display name for the mode + - roleDefinition: A detailed description of the mode's role and capabilities + - groups: Array of allowed tool groups (can be empty). Each group can be specified either as a string (e.g., "edit" to allow editing any file) or with file restrictions (e.g., ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }] to only allow editing markdown files) + +- The following fields are optional but highly recommended: + + - description: A short, human-readable description of what this mode does (5 words) + - whenToUse: A clear description of when this mode should be selected and what types of tasks it's best suited for. This helps the Orchestrator mode make better decisions. + - customInstructions: Additional instructions for how the mode should operate + +- For multi-line text, include newline characters in the string like "This is the first line.\\nThis is the next line.\\n\\nThis is a double line break." + +Both files should follow this structure (in YAML format): + +customModes: + +- slug: designer # Required: unique slug with lowercase letters, numbers, and hyphens + name: Designer # Required: mode display name + description: UI/UX design systems expert # Optional but recommended: short description (5 words) + roleDefinition: >- + You are Roo, a UI/UX expert specializing in design systems and frontend development. Your expertise includes: + - Creating and maintaining design systems + - Implementing responsive and accessible web interfaces + - Working with CSS, HTML, and modern frontend frameworks + - Ensuring consistent user experiences across platforms # Required: non-empty + whenToUse: >- + Use this mode when creating or modifying UI components, implementing design systems, + or ensuring responsive web interfaces. This mode is especially effective with CSS, + HTML, and modern frontend frameworks. # Optional but recommended + groups: # Required: array of tool groups (can be empty) + - read # Read files group (read_file, search_files, list_files, codebase_search) + - edit # Edit files group (apply_diff, write_to_file) - allows editing any file + # Or with file restrictions: + # - - edit + # - fileRegex: \\.md$ + # description: Markdown files only # Edit group that only allows editing markdown files + - browser # Browser group (browser_action) + - command # Command group (execute_command) + - mcp # MCP group (use_mcp_tool, access_mcp_resource) + customInstructions: Additional instructions for the Designer mode # Optional`, + }, +} + +/** + * Get all built-in skills as SkillMetadata objects + */ +export function getBuiltInSkills(): SkillMetadata[] { + return Object.values(BUILT_IN_SKILLS).map((skill) => ({ + name: skill.name, + description: skill.description, + path: "built-in", + source: "built-in" as const, + })) +} + +/** + * Get a specific built-in skill's full content by name + */ +export function getBuiltInSkillContent(name: string): SkillContent | null { + const skill = BUILT_IN_SKILLS[name] + if (!skill) return null + + return { + name: skill.name, + description: skill.description, + path: "built-in", + source: "built-in" as const, + instructions: skill.instructions, + } +} + +/** + * Check if a skill name is a built-in skill + */ +export function isBuiltInSkill(name: string): boolean { + return name in BUILT_IN_SKILLS +} + +/** + * Get names of all built-in skills + */ +export function getBuiltInSkillNames(): string[] { + return Object.keys(BUILT_IN_SKILLS) +} diff --git a/src/core/prompts/instructions/create-mcp-server.ts b/src/services/skills/built-in/create-mcp-server/SKILL.md similarity index 52% rename from src/core/prompts/instructions/create-mcp-server.ts rename to src/services/skills/built-in/create-mcp-server/SKILL.md index a63fad1de56..be52e91c890 100644 --- a/src/core/prompts/instructions/create-mcp-server.ts +++ b/src/services/skills/built-in/create-mcp-server/SKILL.md @@ -1,24 +1,21 @@ -import { McpHub } from "../../../services/mcp/McpHub" -import { DiffStrategy } from "../../../shared/tools" +--- +name: create-mcp-server +description: Instructions for creating MCP (Model Context Protocol) servers that expose tools and resources for the agent to use. Use when the user asks to create a new MCP server or add MCP capabilities. +--- -export async function createMCPServerInstructions( - mcpHub: McpHub | undefined, - diffStrategy: DiffStrategy | undefined, -): Promise { - if (!diffStrategy || !mcpHub) throw new Error("Missing MCP Hub or Diff Strategy") - - return `You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with \`use_mcp_tool\` and \`access_mcp_resource\`. +You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with `use_mcp_tool` and `access_mcp_resource`. When creating MCP servers, it's important to understand that they operate in a non-interactive environment. The server cannot initiate OAuth flows, open browser windows, or prompt for user input during runtime. All credentials and authentication tokens must be provided upfront through environment variables in the MCP settings configuration. For example, Spotify's API uses OAuth to get a refresh token for the user, but the MCP server cannot initiate this flow. While you can walk the user through obtaining an application client ID and secret, you may have to create a separate one-time setup script (like get-refresh-token.js) that captures and logs the final piece of the puzzle: the user's refresh token (i.e. you might run the script using execute_command which would open a browser for authentication, and then log the refresh token so that you can see it in the command output for you to use in the MCP settings configuration). -Unless the user specifies otherwise, new local MCP servers should be created in: ${await mcpHub.getMcpServersPath()} +Unless the user specifies otherwise, new local MCP servers should be created in your MCP servers directory. You can find the path to this directory by checking the MCP settings file, or ask the user where they'd like the server created. ### MCP Server Types and Configuration MCP servers can be configured in two ways in the MCP settings file: 1. Local (Stdio) Server Configuration: -\`\`\`json + +```json { "mcpServers": { "local-weather": { @@ -30,10 +27,11 @@ MCP servers can be configured in two ways in the MCP settings file: } } } -\`\`\` +``` 2. Remote (SSE) Server Configuration: -\`\`\`json + +```json { "mcpServers": { "remote-weather": { @@ -44,13 +42,14 @@ MCP servers can be configured in two ways in the MCP settings file: } } } -\`\`\` +``` Common configuration options for both types: -- \`disabled\`: (optional) Set to true to temporarily disable the server -- \`timeout\`: (optional) Maximum time in seconds to wait for server responses (default: 60) -- \`alwaysAllow\`: (optional) Array of tool names that don't require user confirmation -- \`disabledTools\`: (optional) Array of tool names that are not included in the system prompt and won't be used + +- `disabled`: (optional) Set to true to temporarily disable the server +- `timeout`: (optional) Maximum time in seconds to wait for server responses (default: 60) +- `alwaysAllow`: (optional) Array of tool names that don't require user confirmation +- `disabledTools`: (optional) Array of tool names that are not included in the system prompt and won't be used ### Example Local MCP Server @@ -58,19 +57,19 @@ For example, if the user wanted to give you the ability to retrieve weather info The following example demonstrates how to build a local MCP server that provides weather data functionality using the Stdio transport. While this example shows how to implement resources, resource templates, and tools, in practice you should prefer using tools since they are more flexible and can handle dynamic parameters. The resource and resource template implementations are included here mainly for demonstration purposes of the different MCP capabilities, but a real weather server would likely just expose tools for fetching weather data. (The following steps are for macOS) -1. Use the \`create-typescript-server\` tool to bootstrap a new project in the default MCP servers directory: +1. Use the `create-typescript-server` tool to bootstrap a new project in your MCP servers directory: -\`\`\`bash -cd ${await mcpHub.getMcpServersPath()} +```bash +cd /path/to/your/mcp-servers npx @modelcontextprotocol/create-server weather-server cd weather-server # Install dependencies npm install axios zod @modelcontextprotocol/sdk -\`\`\` +``` This will create a new project with the following structure: -\`\`\` +``` weather-server/ ├── package.json { @@ -85,201 +84,193 @@ weather-server/ ├── tsconfig.json └── src/ └── index.ts # Main server implementation -\`\`\` +``` -2. Replace \`src/index.ts\` with the following: +2. Replace `src/index.ts` with the following: -\`\`\`typescript +```typescript #!/usr/bin/env node -import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { z } from "zod"; -import axios from 'axios'; +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js" +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" +import { z } from "zod" +import axios from "axios" -const API_KEY = process.env.OPENWEATHER_API_KEY; // provided by MCP config +const API_KEY = process.env.OPENWEATHER_API_KEY // provided by MCP config if (!API_KEY) { - throw new Error('OPENWEATHER_API_KEY environment variable is required'); + throw new Error("OPENWEATHER_API_KEY environment variable is required") } // Define types for OpenWeather API responses interface WeatherData { - main: { - temp: number; - humidity: number; - }; - weather: Array<{ - description: string; - }>; - wind: { - speed: number; - }; + main: { + temp: number + humidity: number + } + weather: Array<{ + description: string + }> + wind: { + speed: number + } } interface ForecastData { - list: Array; + list: Array< + WeatherData & { + dt_txt: string + } + > } // Create an MCP server const server = new McpServer({ - name: "weather-server", - version: "0.1.0" -}); + name: "weather-server", + version: "0.1.0", +}) // Create axios instance for OpenWeather API const weatherApi = axios.create({ - baseURL: 'http://api.openweathermap.org/data/2.5', - params: { - appid: API_KEY, - units: 'metric', - }, -}); + baseURL: "http://api.openweathermap.org/data/2.5", + params: { + appid: API_KEY, + units: "metric", + }, +}) // Add a tool for getting weather forecasts server.tool( - "get_forecast", - { - city: z.string().describe("City name"), - days: z.number().min(1).max(5).optional().describe("Number of days (1-5)"), - }, - async ({ city, days = 3 }) => { - try { - const response = await weatherApi.get('forecast', { - params: { - q: city, - cnt: Math.min(days, 5) * 8, - }, - }); - - return { - content: [ - { - type: "text", - text: JSON.stringify(response.data.list, null, 2), - }, - ], - }; - } catch (error) { - if (axios.isAxiosError(error)) { - return { - content: [ - { - type: "text", - text: \`Weather API error: \${ - error.response?.data.message ?? error.message - }\`, - }, - ], - isError: true, - }; - } - throw error; - } - } -); + "get_forecast", + { + city: z.string().describe("City name"), + days: z.number().min(1).max(5).optional().describe("Number of days (1-5)"), + }, + async ({ city, days = 3 }) => { + try { + const response = await weatherApi.get("forecast", { + params: { + q: city, + cnt: Math.min(days, 5) * 8, + }, + }) + + return { + content: [ + { + type: "text", + text: JSON.stringify(response.data.list, null, 2), + }, + ], + } + } catch (error) { + if (axios.isAxiosError(error)) { + return { + content: [ + { + type: "text", + text: `Weather API error: ${error.response?.data.message ?? error.message}`, + }, + ], + isError: true, + } + } + throw error + } + }, +) // Add a resource for current weather in San Francisco -server.resource( - "sf_weather", - { uri: "weather://San Francisco/current", list: true }, - async (uri) => { - try { - const response = weatherApi.get('weather', { - params: { q: "San Francisco" }, - }); - - return { - contents: [ - { - uri: uri.href, - mimeType: "application/json", - text: JSON.stringify( - { - temperature: response.data.main.temp, - conditions: response.data.weather[0].description, - humidity: response.data.main.humidity, - wind_speed: response.data.wind.speed, - timestamp: new Date().toISOString(), - }, - null, - 2 - ), - }, - ], - }; - } catch (error) { - if (axios.isAxiosError(error)) { - throw new Error(\`Weather API error: \${ - error.response?.data.message ?? error.message - }\`); - } - throw error; - } - } -); +server.resource("sf_weather", { uri: "weather://San Francisco/current", list: true }, async (uri) => { + try { + const response = weatherApi.get("weather", { + params: { q: "San Francisco" }, + }) + + return { + contents: [ + { + uri: uri.href, + mimeType: "application/json", + text: JSON.stringify( + { + temperature: response.data.main.temp, + conditions: response.data.weather[0].description, + humidity: response.data.main.humidity, + wind_speed: response.data.wind.speed, + timestamp: new Date().toISOString(), + }, + null, + 2, + ), + }, + ], + } + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error(`Weather API error: ${error.response?.data.message ?? error.message}`) + } + throw error + } +}) // Add a dynamic resource template for current weather by city server.resource( - "current_weather", - new ResourceTemplate("weather://{city}/current", { list: true }), - async (uri, { city }) => { - try { - const response = await weatherApi.get('weather', { - params: { q: city }, - }); - - return { - contents: [ - { - uri: uri.href, - mimeType: "application/json", - text: JSON.stringify( - { - temperature: response.data.main.temp, - conditions: response.data.weather[0].description, - humidity: response.data.main.humidity, - wind_speed: response.data.wind.speed, - timestamp: new Date().toISOString(), - }, - null, - 2 - ), - }, - ], - }; - } catch (error) { - if (axios.isAxiosError(error)) { - throw new Error(\`Weather API error: \${ - error.response?.data.message ?? error.message - }\`); - } - throw error; - } - } -); + "current_weather", + new ResourceTemplate("weather://{city}/current", { list: true }), + async (uri, { city }) => { + try { + const response = await weatherApi.get("weather", { + params: { q: city }, + }) + + return { + contents: [ + { + uri: uri.href, + mimeType: "application/json", + text: JSON.stringify( + { + temperature: response.data.main.temp, + conditions: response.data.weather[0].description, + humidity: response.data.main.humidity, + wind_speed: response.data.wind.speed, + timestamp: new Date().toISOString(), + }, + null, + 2, + ), + }, + ], + } + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error(`Weather API error: ${error.response?.data.message ?? error.message}`) + } + throw error + } + }, +) // Start receiving messages on stdin and sending messages on stdout -const transport = new StdioServerTransport(); -await server.connect(transport); -console.error('Weather MCP server running on stdio'); -\`\`\` +const transport = new StdioServerTransport() +await server.connect(transport) +console.error("Weather MCP server running on stdio") +``` (Remember: This is just an example–you may use different dependencies, break the implementation up into multiple files, etc.) 3. Build and compile the executable JavaScript file -\`\`\`bash +```bash npm run build -\`\`\` +``` 4. Whenever you need an environment variable such as an API key to configure the MCP server, walk the user through the process of getting the key. For example, they may need to create an account and go to a developer dashboard to generate the key. Provide step-by-step instructions and URLs to make it easy for the user to retrieve the necessary information. Then use the ask_followup_question tool to ask the user for the key, in this case the OpenWeather API key. -5. Install the MCP Server by adding the MCP server configuration to the settings file located at '${await mcpHub.getMcpSettingsFilePath()}'. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing \`mcpServers\` object. +5. Install the MCP Server by adding the MCP server configuration to the MCP settings file. On macOS/Linux this is typically at `~/.roo-code/settings/mcp_settings.json`, on Windows at `%APPDATA%\roo-code\settings\mcp_settings.json`. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing `mcpServers` object. IMPORTANT: Regardless of what else you see in the MCP settings file, you must default any new MCP servers you create to disabled=false, alwaysAllow=[] and disabledTools=[]. -\`\`\`json +```json { "mcpServers": { ..., @@ -292,9 +283,9 @@ IMPORTANT: Regardless of what else you see in the MCP settings file, you must de }, } } -\`\`\` +``` -(Note: the user may also ask you to install the MCP server to the Claude desktop app, in which case you would read then modify \`~/Library/Application\ Support/Claude/claude_desktop_config.json\` on macOS for example. It follows the same format of a top level \`mcpServers\` object.) +(Note: the user may also ask you to install the MCP server to the Claude desktop app, in which case you would read then modify `~/Library/Application\ Support/Claude/claude_desktop_config.json` on macOS for example. It follows the same format of a top level `mcpServers` object.) 6. After you have edited the MCP settings configuration file, the system will automatically run all the servers and expose the available tools and resources in the 'Connected MCP Servers' section. @@ -302,14 +293,7 @@ IMPORTANT: Regardless of what else you see in the MCP settings file, you must de ## Editing MCP Servers -The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' above: ${(() => { - if (!mcpHub) return "(None running currently)" - const servers = mcpHub - .getServers() - .map((server) => server.name) - .join(", ") - return servers || "(None running currently)" - })()}, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use write_to_file${diffStrategy ? " or apply_diff" : ""} to make changes to the files. +The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' in the system prompt), e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use write_to_file or apply_diff to make changes to the files. However some MCP servers may be running from installed packages rather than a local repository, in which case it may make more sense to create a new MCP server. @@ -317,5 +301,4 @@ However some MCP servers may be running from installed packages rather than a lo The user may not always request the use or creation of MCP servers. Instead, they might provide tasks that can be completed with existing tools. While using the MCP SDK to extend your capabilities can be useful, it's important to understand that this is just one specialized type of task you can accomplish. You should only implement MCP servers when the user explicitly requests it (e.g., "add a tool that..."). -Remember: The MCP documentation and example provided above are to help you understand and work with existing MCP servers or create new ones when requested by the user. You already have access to tools and capabilities that can be used to accomplish a wide range of tasks.` -} +Remember: The MCP documentation and example provided above are to help you understand and work with existing MCP servers or create new ones when requested by the user. You already have access to tools and capabilities that can be used to accomplish a wide range of tasks. diff --git a/src/services/skills/built-in/create-mode/SKILL.md b/src/services/skills/built-in/create-mode/SKILL.md new file mode 100644 index 00000000000..ec43ac9bea1 --- /dev/null +++ b/src/services/skills/built-in/create-mode/SKILL.md @@ -0,0 +1,57 @@ +--- +name: create-mode +description: Instructions for creating custom modes in Roo Code. Use when the user asks to create a new mode, edit an existing mode, or configure mode settings. +--- + +Custom modes can be configured in two ways: + +1. Globally via the custom modes file in your Roo Code settings directory (typically ~/.roo-code/settings/custom_modes.yaml on macOS/Linux or %APPDATA%\roo-code\settings\custom_modes.yaml on Windows) - created automatically on startup +2. Per-workspace via '.roomodes' in the workspace root directory + +When modes with the same slug exist in both files, the workspace-specific .roomodes version takes precedence. This allows projects to override global modes or define project-specific modes. + +If asked to create a project mode, create it in .roomodes in the workspace root. If asked to create a global mode, use the global custom modes file. + +- The following fields are required and must not be empty: + + - slug: A valid slug (lowercase letters, numbers, and hyphens). Must be unique, and shorter is better. + - name: The display name for the mode + - roleDefinition: A detailed description of the mode's role and capabilities + - groups: Array of allowed tool groups (can be empty). Each group can be specified either as a string (e.g., "edit" to allow editing any file) or with file restrictions (e.g., ["edit", { fileRegex: "\.md$", description: "Markdown files only" }] to only allow editing markdown files) + +- The following fields are optional but highly recommended: + + - description: A short, human-readable description of what this mode does (5 words) + - whenToUse: A clear description of when this mode should be selected and what types of tasks it's best suited for. This helps the Orchestrator mode make better decisions. + - customInstructions: Additional instructions for how the mode should operate + +- For multi-line text, include newline characters in the string like "This is the first line.\nThis is the next line.\n\nThis is a double line break." + +Both files should follow this structure (in YAML format): + +customModes: + +- slug: designer # Required: unique slug with lowercase letters, numbers, and hyphens + name: Designer # Required: mode display name + description: UI/UX design systems expert # Optional but recommended: short description (5 words) + roleDefinition: >- + You are Roo, a UI/UX expert specializing in design systems and frontend development. Your expertise includes: + - Creating and maintaining design systems + - Implementing responsive and accessible web interfaces + - Working with CSS, HTML, and modern frontend frameworks + - Ensuring consistent user experiences across platforms # Required: non-empty + whenToUse: >- + Use this mode when creating or modifying UI components, implementing design systems, + or ensuring responsive web interfaces. This mode is especially effective with CSS, + HTML, and modern frontend frameworks. # Optional but recommended + groups: # Required: array of tool groups (can be empty) + - read # Read files group (read_file, search_files, list_files, codebase_search) + - edit # Edit files group (apply_diff, write_to_file) - allows editing any file + # Or with file restrictions: + # - - edit + # - fileRegex: \.md$ + # description: Markdown files only # Edit group that only allows editing markdown files + - browser # Browser group (browser_action) + - command # Command group (execute_command) + - mcp # MCP group (use_mcp_tool, access_mcp_resource) + customInstructions: Additional instructions for the Designer mode # Optional diff --git a/src/services/skills/generate-built-in-skills.ts b/src/services/skills/generate-built-in-skills.ts new file mode 100644 index 00000000000..517040c010c --- /dev/null +++ b/src/services/skills/generate-built-in-skills.ts @@ -0,0 +1,300 @@ +#!/usr/bin/env tsx +/** + * Build script to generate built-in-skills.ts from SKILL.md files. + * + * This script scans the built-in/ directory for skill folders, parses each + * SKILL.md file using gray-matter, validates the frontmatter, and generates + * the built-in-skills.ts file. + * + * Run with: npx tsx src/services/skills/generate-built-in-skills.ts + */ + +import * as fs from "fs/promises" +import * as path from "path" +import { execSync } from "child_process" +import matter from "gray-matter" + +const BUILT_IN_DIR = path.join(__dirname, "built-in") +const OUTPUT_FILE = path.join(__dirname, "built-in-skills.ts") + +interface SkillData { + name: string + description: string + instructions: string +} + +interface ValidationError { + skillDir: string + errors: string[] +} + +/** + * Validate a skill name according to Agent Skills spec: + * - 1-64 characters + * - lowercase letters, numbers, and hyphens only + * - must not start/end with hyphen + * - must not contain consecutive hyphens + */ +function validateSkillName(name: string): string[] { + const errors: string[] = [] + + if (name.length < 1 || name.length > 64) { + errors.push(`Name must be 1-64 characters (got ${name.length})`) + } + + const nameFormat = /^[a-z0-9]+(?:-[a-z0-9]+)*$/ + if (!nameFormat.test(name)) { + errors.push( + "Name must be lowercase letters/numbers/hyphens only (no leading/trailing hyphen, no consecutive hyphens)", + ) + } + + return errors +} + +/** + * Validate a skill description: + * - 1-1024 characters (after trimming) + */ +function validateDescription(description: string): string[] { + const errors: string[] = [] + const trimmed = description.trim() + + if (trimmed.length < 1 || trimmed.length > 1024) { + errors.push(`Description must be 1-1024 characters (got ${trimmed.length})`) + } + + return errors +} + +/** + * Parse and validate a single SKILL.md file + */ +async function parseSkillFile( + skillDir: string, + dirName: string, +): Promise<{ skill?: SkillData; errors?: ValidationError }> { + const skillMdPath = path.join(skillDir, "SKILL.md") + + try { + const fileContent = await fs.readFile(skillMdPath, "utf-8") + const { data: frontmatter, content: body } = matter(fileContent) + + const errors: string[] = [] + + // Validate required fields + if (!frontmatter.name || typeof frontmatter.name !== "string") { + errors.push("Missing required 'name' field in frontmatter") + } + if (!frontmatter.description || typeof frontmatter.description !== "string") { + errors.push("Missing required 'description' field in frontmatter") + } + + if (errors.length > 0) { + return { errors: { skillDir, errors } } + } + + // Validate name matches directory name + if (frontmatter.name !== dirName) { + errors.push(`Frontmatter name "${frontmatter.name}" doesn't match directory name "${dirName}"`) + } + + // Validate name format + errors.push(...validateSkillName(dirName)) + + // Validate description + errors.push(...validateDescription(frontmatter.description)) + + if (errors.length > 0) { + return { errors: { skillDir, errors } } + } + + return { + skill: { + name: frontmatter.name, + description: frontmatter.description.trim(), + instructions: body.trim(), + }, + } + } catch (error) { + return { + errors: { + skillDir, + errors: [`Failed to read or parse SKILL.md: ${error instanceof Error ? error.message : String(error)}`], + }, + } + } +} + +/** + * Escape a string for use in TypeScript template literal + */ +function escapeForTemplateLiteral(str: string): string { + return str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${") +} + +/** + * Generate the TypeScript code for built-in-skills.ts + */ +function generateTypeScript(skills: Record): string { + const skillEntries = Object.entries(skills) + .map(([key, skill]) => { + const escapedInstructions = escapeForTemplateLiteral(skill.instructions) + return `\t"${key}": { + name: "${skill.name}", + description: "${skill.description.replace(/"/g, '\\"')}", + instructions: \`${escapedInstructions}\`, + }` + }) + .join(",\n") + + return `/** + * AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY + * + * This file is generated by generate-built-in-skills.ts from the SKILL.md files + * in the built-in/ directory. To modify built-in skills, edit the corresponding + * SKILL.md file and run: pnpm generate:skills + */ + +import { SkillMetadata, SkillContent } from "../../shared/skills" + +interface BuiltInSkillDefinition { + name: string + description: string + instructions: string +} + +const BUILT_IN_SKILLS: Record = { +${skillEntries} +} + +/** + * Get all built-in skills as SkillMetadata objects + */ +export function getBuiltInSkills(): SkillMetadata[] { + return Object.values(BUILT_IN_SKILLS).map((skill) => ({ + name: skill.name, + description: skill.description, + path: "built-in", + source: "built-in" as const, + })) +} + +/** + * Get a specific built-in skill's full content by name + */ +export function getBuiltInSkillContent(name: string): SkillContent | null { + const skill = BUILT_IN_SKILLS[name] + if (!skill) return null + + return { + name: skill.name, + description: skill.description, + path: "built-in", + source: "built-in" as const, + instructions: skill.instructions, + } +} + +/** + * Check if a skill name is a built-in skill + */ +export function isBuiltInSkill(name: string): boolean { + return name in BUILT_IN_SKILLS +} + +/** + * Get names of all built-in skills + */ +export function getBuiltInSkillNames(): string[] { + return Object.keys(BUILT_IN_SKILLS) +} +` +} + +async function main() { + console.log("Generating built-in skills from SKILL.md files...") + + // Check if built-in directory exists + try { + await fs.access(BUILT_IN_DIR) + } catch { + console.error(`Error: Built-in skills directory not found: ${BUILT_IN_DIR}`) + process.exit(1) + } + + // Scan for skill directories + const entries = await fs.readdir(BUILT_IN_DIR) + const skills: Record = {} + const validationErrors: ValidationError[] = [] + + for (const entry of entries) { + const skillDir = path.join(BUILT_IN_DIR, entry) + const stats = await fs.stat(skillDir) + + if (!stats.isDirectory()) { + continue + } + + // Check if SKILL.md exists + const skillMdPath = path.join(skillDir, "SKILL.md") + try { + await fs.access(skillMdPath) + } catch { + console.warn(`Warning: No SKILL.md found in ${entry}, skipping`) + continue + } + + const result = await parseSkillFile(skillDir, entry) + + if (result.errors) { + validationErrors.push(result.errors) + } else if (result.skill) { + skills[entry] = result.skill + console.log(` ✓ Parsed ${entry}`) + } + } + + // Report validation errors + if (validationErrors.length > 0) { + console.error("\nValidation errors:") + for (const { skillDir, errors } of validationErrors) { + console.error(`\n ${path.basename(skillDir)}:`) + for (const error of errors) { + console.error(` - ${error}`) + } + } + process.exit(1) + } + + // Check if any skills were found + if (Object.keys(skills).length === 0) { + console.error("Error: No valid skills found in built-in directory") + process.exit(1) + } + + // Generate TypeScript + const output = generateTypeScript(skills) + + // Write output file + await fs.writeFile(OUTPUT_FILE, output, "utf-8") + + // Format with prettier to ensure stable output + // Run from workspace root (3 levels up from src/services/skills/) to find .prettierrc.json + const workspaceRoot = path.resolve(__dirname, "..", "..", "..") + try { + execSync(`npx prettier --write "${OUTPUT_FILE}"`, { + cwd: workspaceRoot, + stdio: "pipe", + }) + console.log(`\n✓ Generated and formatted ${OUTPUT_FILE}`) + } catch { + console.log(`\n✓ Generated ${OUTPUT_FILE} (prettier not available)`) + } + console.log(` Skills: ${Object.keys(skills).join(", ")}`) +} + +main().catch((error) => { + console.error("Fatal error:", error) + process.exit(1) +}) diff --git a/src/shared/skills.ts b/src/shared/skills.ts index 7ed85816aa8..ae35b8c3878 100644 --- a/src/shared/skills.ts +++ b/src/shared/skills.ts @@ -5,8 +5,8 @@ export interface SkillMetadata { name: string // Required: skill identifier description: string // Required: when to use this skill - path: string // Absolute path to SKILL.md - source: "global" | "project" // Where the skill was discovered + path: string // Absolute path to SKILL.md (or "" for built-in skills) + source: "global" | "project" | "built-in" // Where the skill was discovered mode?: string // If set, skill is only available in this mode } diff --git a/src/shared/tools.ts b/src/shared/tools.ts index dc1615c0654..64668f3e4eb 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -60,6 +60,7 @@ export const toolParamNames = [ "size", "query", "args", + "skill", // skill tool parameter "start_line", "end_line", "todos", @@ -103,9 +104,9 @@ export type NativeToolArgs = { } browser_action: BrowserActionParams codebase_search: { query: string; path?: string } - fetch_instructions: { task: string } generate_image: GenerateImageParams run_slash_command: { command: string; args?: string } + skill: { skill: string; args?: string | null } search_files: { path: string; regex: string; file_pattern?: string | null } switch_mode: { mode_slug: string; reason: string } update_todo_list: { todos: string } @@ -167,11 +168,6 @@ export interface ReadFileToolUse extends ToolUse<"read_file"> { params: Partial, "args" | "path" | "start_line" | "end_line" | "files">> } -export interface FetchInstructionsToolUse extends ToolUse<"fetch_instructions"> { - name: "fetch_instructions" - params: Partial, "task">> -} - export interface WriteToFileToolUse extends ToolUse<"write_to_file"> { name: "write_to_file" params: Partial, "path" | "content">> @@ -232,6 +228,11 @@ export interface RunSlashCommandToolUse extends ToolUse<"run_slash_command"> { params: Partial, "command" | "args">> } +export interface SkillToolUse extends ToolUse<"skill"> { + name: "skill" + params: Partial, "skill" | "args">> +} + export interface GenerateImageToolUse extends ToolUse<"generate_image"> { name: "generate_image" params: Partial, "prompt" | "path" | "image">> @@ -248,7 +249,6 @@ export const TOOL_DISPLAY_NAMES: Record = { execute_command: "run commands", read_file: "read files", read_command_output: "read command output", - fetch_instructions: "fetch instructions", write_to_file: "write files", apply_diff: "apply changes", search_and_replace: "apply changes using search and replace", @@ -267,6 +267,7 @@ export const TOOL_DISPLAY_NAMES: Record = { codebase_search: "codebase search", update_todo_list: "update todo list", run_slash_command: "run slash command", + skill: "load skill", generate_image: "generate images", custom_tool: "use custom tools", } as const @@ -274,7 +275,7 @@ export const TOOL_DISPLAY_NAMES: Record = { // Define available tool groups. export const TOOL_GROUPS: Record = { read: { - tools: ["read_file", "fetch_instructions", "search_files", "list_files", "codebase_search"], + tools: ["read_file", "search_files", "list_files", "codebase_search"], }, edit: { tools: ["apply_diff", "write_to_file", "generate_image"], @@ -303,6 +304,7 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [ "new_task", "update_todo_list", "run_slash_command", + "skill", ] as const /** diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 6a6d5c3f6df..25bcd61ee3f 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -666,24 +666,75 @@ export const ChatRowContent = ({ ) - case "fetchInstructions": + case "skill": { + const skillInfo = tool return ( <>
- {toolIcon("file-code")} - {t("chat:instructions.wantsToFetch")} + {toolIcon("book")} + + {message.type === "ask" ? t("chat:skill.wantsToLoad") : t("chat:skill.didLoad")} +
-
- +
+ +
+ + {skillInfo.skill} + + {skillInfo.source && ( + + {skillInfo.source} + + )} +
+ +
+ {isExpanded && (skillInfo.args || skillInfo.description) && ( +
+ {skillInfo.description && ( +
+ {skillInfo.description} +
+ )} + {skillInfo.args && ( +
+ Arguments: + + {skillInfo.args} + +
+ )} +
+ )}
) + } case "listFilesTopLevel": return ( <> diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index 2167ee18b3e..75a9a1a3800 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -1,12 +1,6 @@ import React, { useState } from "react" import { Trans } from "react-i18next" -import { - VSCodeCheckbox, - VSCodeLink, - VSCodePanels, - VSCodePanelTab, - VSCodePanelView, -} from "@vscode/webview-ui-toolkit/react" +import { VSCodeLink, VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react" import type { McpServer } from "@roo-code/types" @@ -35,13 +29,7 @@ import McpEnabledToggle from "./McpEnabledToggle" import { McpErrorRow } from "./McpErrorRow" const McpView = () => { - const { - mcpServers: servers, - alwaysAllowMcp, - mcpEnabled, - enableMcpServerCreation, - setEnableMcpServerCreation, - } = useExtensionState() + const { mcpServers: servers, alwaysAllowMcp, mcpEnabled } = useExtensionState() const { t } = useAppTranslation() const { isOverThreshold, title, message } = useTooManyTools() @@ -71,36 +59,6 @@ const McpView = () => { {mcpEnabled && ( <> -
- { - setEnableMcpServerCreation(e.target.checked) - vscode.postMessage({ type: "enableMcpServerCreation", bool: e.target.checked }) - }}> - {t("mcp:enableServerCreation.title")} - -
- - - Learn about server creation - - new - -

{t("mcp:enableServerCreation.hint")}

-
-
- {/* Too Many Tools Warning */} {isOverThreshold && (
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 1d2c43ff008..fcefdb5bd13 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -102,8 +102,6 @@ export interface ExtensionStateContextType extends ExtensionState { setTerminalOutputPreviewSize: (value: "small" | "medium" | "large") => void mcpEnabled: boolean setMcpEnabled: (value: boolean) => void - enableMcpServerCreation: boolean - setEnableMcpServerCreation: (value: boolean) => void remoteControlEnabled: boolean setRemoteControlEnabled: (value: boolean) => void taskSyncEnabled: boolean @@ -213,7 +211,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode screenshotQuality: 75, terminalShellIntegrationTimeout: 4000, mcpEnabled: true, - enableMcpServerCreation: false, remoteControlEnabled: false, taskSyncEnabled: false, featureRoomoteControlEnabled: false, @@ -553,8 +550,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setState((prevState) => ({ ...prevState, terminalShellIntegrationDisabled: value })), setTerminalZdotdir: (value) => setState((prevState) => ({ ...prevState, terminalZdotdir: value })), setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })), - setEnableMcpServerCreation: (value) => - setState((prevState) => ({ ...prevState, enableMcpServerCreation: value })), setRemoteControlEnabled: (value) => setState((prevState) => ({ ...prevState, remoteControlEnabled: value })), setTaskSyncEnabled: (value) => setState((prevState) => ({ ...prevState, taskSyncEnabled: value }) as any), setFeatureRoomoteControlEnabled: (value) => diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 9598e98681d..4d8be85728f 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -187,7 +187,6 @@ describe("mergeExtensionState", () => { const baseState: ExtensionState = { version: "", mcpEnabled: false, - enableMcpServerCreation: false, clineMessages: [], taskHistory: [], shouldShowAnnouncement: false, diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index c44a8724335..d3653c057d4 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -324,6 +324,10 @@ "description": "S'han eliminat missatges més antics de la conversa per mantenir-se dins del límit de la finestra de context. Aquest és un enfocament ràpid però menys conservador del context en comparació amb la condensació." } }, + "skill": { + "wantsToLoad": "En Roo vol carregar una habilitat", + "didLoad": "En Roo ha carregat una habilitat" + }, "followUpSuggest": { "copyToInput": "Copiar a l'entrada (o Shift + clic)", "timerPrefix": "Aprovació automàtica habilitada. Seleccionant en {{seconds}}s…" diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 1f3f11bc81c..e3aefa36298 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -324,6 +324,10 @@ "description": "Ältere Nachrichten wurden aus der Konversation entfernt, um innerhalb des Kontextfenster-Limits zu bleiben. Dies ist ein schnellerer, aber weniger kontexterhaltender Ansatz im Vergleich zur Komprimierung." } }, + "skill": { + "wantsToLoad": "Roo möchte eine Fähigkeit laden", + "didLoad": "Roo hat eine Fähigkeit geladen" + }, "followUpSuggest": { "copyToInput": "In Eingabefeld kopieren (oder Shift + Klick)", "timerPrefix": "Automatische Genehmigung aktiviert. Wähle in {{seconds}}s…" diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index d167a19ff3e..7c2d811021a 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -203,8 +203,9 @@ "description": "Older messages were removed from the conversation to stay within the context window limit. This is a fast but less context-preserving approach compared to condensation." } }, - "instructions": { - "wantsToFetch": "Roo wants to fetch detailed instructions to assist with the current task" + "skill": { + "wantsToLoad": "Roo wants to load a skill", + "didLoad": "Roo loaded a skill" }, "fileOperations": { "wantsToRead": "Roo wants to read this file", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 2c9418cfa7e..d06d55cdfa5 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -324,6 +324,10 @@ "description": "Se eliminaron mensajes más antiguos de la conversación para mantenerse dentro del límite de la ventana de contexto. Este es un enfoque rápido pero menos conservador del contexto en comparación con la condensación." } }, + "skill": { + "wantsToLoad": "Roo quiere cargar una habilidad", + "didLoad": "Roo cargó una habilidad" + }, "followUpSuggest": { "copyToInput": "Copiar a la entrada (o Shift + clic)", "timerPrefix": "Aprobación automática habilitada. Seleccionando en {{seconds}}s…" diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 8aa09075dc3..96781dfb712 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -324,6 +324,10 @@ "description": "Les messages plus anciens ont été supprimés de la conversation pour rester dans la limite de la fenêtre de contexte. C'est une approche rapide mais moins conservatrice du contexte par rapport à la condensation." } }, + "skill": { + "wantsToLoad": "Roo veut charger une compétence", + "didLoad": "Roo a chargé une compétence" + }, "followUpSuggest": { "copyToInput": "Copier vers l'entrée (ou Shift + clic)", "timerPrefix": "Approbation automatique activée. Sélection dans {{seconds}}s…" diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 9c155e62ec4..225539cd7c1 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -324,6 +324,10 @@ "description": "संदर्भ विंडो सीमा के भीतर रहने के लिए बातचीत से पुराने संदेश हटा दिए गए। संघनन की तुलना में यह एक तेज़ लेकिन कम संदर्भ-संरक्षित दृष्टिकोण है।" } }, + "skill": { + "wantsToLoad": "Roo एक कौशल लोड करना चाहता है", + "didLoad": "Roo ने एक कौशल लोड किया" + }, "followUpSuggest": { "copyToInput": "इनपुट में कॉपी करें (या Shift + क्लिक)", "timerPrefix": "ऑटो-अनुमोदन सक्षम है। {{seconds}}s में चयन किया जा रहा है…" diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index c8569f36460..437d588c308 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -206,6 +206,10 @@ "description": "Pesan lama telah dihapus dari percakapan untuk tetap dalam batas jendela konteks. Ini adalah pendekatan yang cepat tetapi kurang mempertahankan konteks dibandingkan dengan kondensasi." } }, + "skill": { + "wantsToLoad": "Roo ingin memuat keterampilan", + "didLoad": "Roo telah memuat keterampilan" + }, "instructions": { "wantsToFetch": "Roo ingin mengambil instruksi detail untuk membantu tugas saat ini" }, diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index ac00a6dea09..7396f31a457 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -324,6 +324,10 @@ "description": "I messaggi più vecchi sono stati rimossi dalla conversazione per rimanere entro il limite della finestra di contesto. Questo è un approccio veloce ma meno conservativo del contesto rispetto alla condensazione." } }, + "skill": { + "wantsToLoad": "Roo vuole caricare una competenza", + "didLoad": "Roo ha caricato una competenza" + }, "followUpSuggest": { "copyToInput": "Copia nell'input (o Shift + clic)", "timerPrefix": "Approvazione automatica abilitata. Selezione tra {{seconds}}s…" diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 34a494ba23a..913ae45238d 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -324,6 +324,10 @@ "description": "コンテキストウィンドウの制限内に収めるため、古いメッセージが会話から削除されました。これは圧縮と比較して高速ですが、コンテキストの保持性が低いアプローチです。" } }, + "skill": { + "wantsToLoad": "Rooはスキルを読み込もうとしています", + "didLoad": "Rooはスキルを読み込みました" + }, "followUpSuggest": { "copyToInput": "入力欄にコピー(またはShift + クリック)", "timerPrefix": "自動承認が有効です。{{seconds}}秒後に選択中…" diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 18d0089e340..dca0fb149fd 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -324,6 +324,10 @@ "description": "컨텍스트 윈도우 제한 내에 유지하기 위해 대화에서 오래된 메시지가 제거되었습니다. 이것은 압축에 비해 빠르지만 컨텍스트 보존 능력이 낮은 접근 방식입니다." } }, + "skill": { + "wantsToLoad": "Roo가 스킬을 로드하려고 합니다", + "didLoad": "Roo가 스킬을 로드했습니다" + }, "followUpSuggest": { "copyToInput": "입력창에 복사 (또는 Shift + 클릭)", "timerPrefix": "자동 승인 활성화됨. {{seconds}}초 후 선택 중…" diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 5f0f6936194..1995a5f9a14 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -346,6 +346,10 @@ "description": "Oudere berichten zijn uit het gesprek verwijderd om binnen de limiet van het contextvenster te blijven. Dit is een snelle maar minder contextbehoudende aanpak in vergelijking met samenvoeging." } }, + "skill": { + "wantsToLoad": "Roo wil een vaardigheid laden", + "didLoad": "Roo heeft een vaardigheid geladen" + }, "followUpSuggest": { "copyToInput": "Kopiëren naar invoer (zelfde als shift + klik)", "timerPrefix": "Automatisch goedkeuren ingeschakeld. Selecteren in {{seconds}}s…" diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index fd90a260034..b1e86920bd2 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -324,6 +324,10 @@ "description": "Starsze wiadomości zostały usunięte z konwersacji, aby pozostać w granicach okna kontekstu. To szybsze, ale mniej zachowujące kontekst podejście w porównaniu z kondensacją." } }, + "skill": { + "wantsToLoad": "Roo chce załadować umiejętność", + "didLoad": "Roo załadował umiejętność" + }, "followUpSuggest": { "copyToInput": "Kopiuj do pola wprowadzania (lub Shift + kliknięcie)", "timerPrefix": "Automatyczne zatwierdzanie włączone. Zaznaczanie za {{seconds}}s…" diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index c6fdc35e824..dc3ae3b3816 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -324,6 +324,10 @@ "description": "Mensagens mais antigas foram removidas da conversa para permanecer dentro do limite da janela de contexto. Esta é uma abordagem rápida, mas menos preservadora de contexto em comparação com a condensação." } }, + "skill": { + "wantsToLoad": "Roo quer carregar uma habilidade", + "didLoad": "Roo carregou uma habilidade" + }, "followUpSuggest": { "copyToInput": "Copiar para entrada (ou Shift + clique)", "timerPrefix": "Aprovação automática ativada. Selecionando em {{seconds}}s…" diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index dffbc64e8d4..ae182d126f6 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -347,6 +347,10 @@ "description": "Более старые сообщения были удалены из разговора, чтобы остаться в пределах контекстного окна. Это быстрый, но менее сохраняющий контекст подход по сравнению со сжатием." } }, + "skill": { + "wantsToLoad": "Roo хочет загрузить навык", + "didLoad": "Roo загрузил навык" + }, "followUpSuggest": { "copyToInput": "Скопировать во ввод (то же, что shift + клик)", "timerPrefix": "Автоматическое одобрение включено. Выбор через {{seconds}}s…" diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 5d5d93893e3..267eae1dba6 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -325,6 +325,10 @@ "description": "Bağlam penceresi sınırında kalmak için eski mesajlar konuşmadan kaldırıldı. Bu, yoğunlaştırmaya kıyasla hızlı ancak daha az bağlam koruyucu bir yaklaşımdır." } }, + "skill": { + "wantsToLoad": "Roo bir beceri yüklemek istiyor", + "didLoad": "Roo bir beceri yükledi" + }, "followUpSuggest": { "copyToInput": "Giriş alanına kopyala (veya Shift + tıklama)", "timerPrefix": "Otomatik onay etkinleştirildi. {{seconds}}s içinde seçim yapılıyor…" diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 76191a03cf1..5d37f66203a 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -325,6 +325,10 @@ "description": "Các tin nhắn cũ hơn đã bị xóa khỏi cuộc trò chuyện để giữ trong giới hạn cửa sổ ngữ cảnh. Đây là cách tiếp cận nhanh nhưng ít bảo toàn ngữ cảnh hơn so với cô đọng." } }, + "skill": { + "wantsToLoad": "Roo muốn tải một kỹ năng", + "didLoad": "Roo đã tải một kỹ năng" + }, "followUpSuggest": { "copyToInput": "Sao chép vào ô nhập liệu (hoặc Shift + nhấp chuột)", "timerPrefix": "Phê duyệt tự động được bật. Chọn trong {{seconds}}s…" diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index e63cc5dd08e..8a46b1e8a48 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -325,6 +325,10 @@ "description": "为保持在上下文窗口限制内,已从对话中移除较旧的消息。与压缩相比,这是一种快速但上下文保留较少的方法。" } }, + "skill": { + "wantsToLoad": "Roo 想要加载技能", + "didLoad": "Roo 加载了技能" + }, "followUpSuggest": { "copyToInput": "复制到输入框(或按住Shift点击)", "timerPrefix": "自动批准已启用。{{seconds}}秒后选择中…" diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 95a96503baf..a8bce99ffe7 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -203,6 +203,10 @@ "description": "為保持在上下文視窗限制內,已從對話中移除較舊的訊息。與壓縮相比,這是一種快速但上下文保留較少的方法。" } }, + "skill": { + "wantsToLoad": "Roo 想要載入技能", + "didLoad": "Roo 載入了技能" + }, "instructions": { "wantsToFetch": "Roo 想要取得詳細指示以協助目前工作" },