Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/__tests__/command-integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe("Command Integration Tests", () => {
const testWorkspaceDir = path.join(__dirname, "../../")

it("should discover command files in .roo/commands/", async () => {
const commands = await getCommands(testWorkspaceDir)
const commands = await getCommands(testWorkspaceDir, undefined)

// Should be able to discover commands (may be empty in test environment)
expect(Array.isArray(commands)).toBe(true)
Expand All @@ -22,7 +22,7 @@ describe("Command Integration Tests", () => {
})

it("should return command names correctly", async () => {
const commandNames = await getCommandNames(testWorkspaceDir)
const commandNames = await getCommandNames(testWorkspaceDir, undefined)

// Should return an array (may be empty in test environment)
expect(Array.isArray(commandNames)).toBe(true)
Expand All @@ -35,11 +35,11 @@ describe("Command Integration Tests", () => {
})

it("should load command content if commands exist", async () => {
const commands = await getCommands(testWorkspaceDir)
const commands = await getCommands(testWorkspaceDir, undefined)

if (commands.length > 0) {
const firstCommand = commands[0]
const loadedCommand = await getCommand(testWorkspaceDir, firstCommand.name)
const loadedCommand = await getCommand(testWorkspaceDir, firstCommand.name, undefined)

expect(loadedCommand).toBeDefined()
expect(loadedCommand?.name).toBe(firstCommand.name)
Expand All @@ -50,7 +50,7 @@ describe("Command Integration Tests", () => {
})

it("should handle non-existent commands gracefully", async () => {
const nonExistentCommand = await getCommand(testWorkspaceDir, "non-existent-command")
const nonExistentCommand = await getCommand(testWorkspaceDir, "non-existent-command", undefined)
expect(nonExistentCommand).toBeUndefined()
})
})
10 changes: 5 additions & 5 deletions src/__tests__/command-mentions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe("Command Mentions", () => {
const input = "/setup Please help me set up the project"
const result = await callParseMentions(input)

expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup")
expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup", undefined)
expect(result).toContain('<command name="setup">')
expect(result).toContain(commandContent)
expect(result).toContain("</command>")
Expand Down Expand Up @@ -94,8 +94,8 @@ describe("Command Mentions", () => {
const input = "/setup the project\nThen /deploy later"
const result = await callParseMentions(input)

expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup")
expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "deploy")
expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup", undefined)
expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "deploy", undefined)
expect(mockGetCommand).toHaveBeenCalledTimes(2) // Each unique command called once (optimized)
expect(result).toContain('<command name="setup">')
expect(result).toContain("# Setup Environment")
Expand All @@ -110,7 +110,7 @@ describe("Command Mentions", () => {
const input = "/nonexistent command"
const result = await callParseMentions(input)

expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "nonexistent")
expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "nonexistent", undefined)
// The command should remain unchanged in the text
expect(result).toBe("/nonexistent command")
// Should not contain any command tags
Expand Down Expand Up @@ -159,7 +159,7 @@ describe("Command Mentions", () => {
const input = "/setup-dev for the project"
const result = await callParseMentions(input)

expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup-dev")
expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup-dev", undefined)
expect(result).toContain('<command name="setup-dev">')
expect(result).toContain("# Dev setup")
})
Expand Down
16 changes: 8 additions & 8 deletions src/__tests__/commands.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,21 @@ describe("Command Utilities", () => {
describe("getCommands", () => {
it("should return empty array when no command directories exist", async () => {
// This will fail to find directories but should return empty array gracefully
const commands = await getCommands(testCwd)
const commands = await getCommands(testCwd, undefined)
expect(Array.isArray(commands)).toBe(true)
})
})

describe("getCommandNames", () => {
it("should return empty array when no commands exist", async () => {
const names = await getCommandNames(testCwd)
const names = await getCommandNames(testCwd, undefined)
expect(Array.isArray(names)).toBe(true)
})
})

describe("getCommand", () => {
it("should return undefined for non-existent command", async () => {
const result = await getCommand(testCwd, "non-existent")
const result = await getCommand(testCwd, "non-existent", undefined)
expect(result).toBeUndefined()
})
})
Expand All @@ -78,8 +78,8 @@ describe("Command Utilities", () => {

describe("command loading behavior", () => {
it("should handle multiple calls to getCommands", async () => {
const commands1 = await getCommands(testCwd)
const commands2 = await getCommands(testCwd)
const commands1 = await getCommands(testCwd, undefined)
const commands2 = await getCommands(testCwd, undefined)
expect(Array.isArray(commands1)).toBe(true)
expect(Array.isArray(commands2)).toBe(true)
})
Expand All @@ -88,9 +88,9 @@ describe("Command Utilities", () => {
describe("error handling", () => {
it("should handle invalid command names gracefully", async () => {
// These should not throw errors
expect(await getCommand(testCwd, "")).toBeUndefined()
expect(await getCommand(testCwd, " ")).toBeUndefined()
expect(await getCommand(testCwd, "non/existent/path")).toBeUndefined()
expect(await getCommand(testCwd, "", undefined)).toBeUndefined()
expect(await getCommand(testCwd, " ", undefined)).toBeUndefined()
expect(await getCommand(testCwd, "non/existent/path", undefined)).toBeUndefined()
})
})
})
3 changes: 2 additions & 1 deletion src/core/mentions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export async function parseMentions(
const commandExistenceChecks = await Promise.all(
Array.from(uniqueCommandNames).map(async (commandName) => {
try {
const command = await getCommand(cwd, commandName)
// TODO: Pass McpHub instance when available for MCP prompt support
const command = await getCommand(cwd, commandName, undefined)
return { commandName, command }
} catch (error) {
// If there's an error checking command existence, treat it as non-existent
Expand Down
52 changes: 48 additions & 4 deletions src/core/tools/runSlashCommandTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } f
import { formatResponse } from "../prompts/responses"
import { getCommand, getCommandNames } from "../../services/command/commands"
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
import { McpServerManager } from "../../services/mcp/McpServerManager"

export async function runSlashCommandTool(
task: Task,
Expand Down Expand Up @@ -49,12 +50,20 @@ export async function runSlashCommandTool(

task.consecutiveMistakeCount = 0

// Get the command from the commands service
const command = await getCommand(task.cwd, commandName)
// Get the command from the commands service (pass McpHub for MCP prompt support)
let mcpHub = undefined
if (provider) {
try {
mcpHub = await McpServerManager.getInstance(provider.context, provider)
} catch (error) {
console.error("Failed to get MCP hub:", error)
}
}
const command = await getCommand(task.cwd, commandName, mcpHub)

if (!command) {
// Get available commands for error message
const availableCommands = await getCommandNames(task.cwd)
const availableCommands = await getCommandNames(task.cwd, mcpHub)
task.recordToolError("run_slash_command")
pushToolResult(
formatResponse.toolError(
Expand All @@ -64,6 +73,41 @@ export async function runSlashCommandTool(
return
}

// Handle MCP prompt commands differently
let commandContent = command.content

if (command.source === "mcp" && command.name.startsWith("mcp.") && mcpHub) {
const parts = command.name.split(".")
if (parts.length >= 3) {
const serverName = parts[1]
const promptName = parts.slice(2).join(".")

try {
const { executeMcpPrompt, parsePromptArguments } = await import(
"../../services/command/mcp-prompts"
)

// Parse arguments if provided
let promptArgs: Record<string, unknown> = {}
if (args) {
const servers = mcpHub.getAllServers()
const server = servers.find((s) => s.name === serverName)
const prompt = server?.prompts?.find((p) => p.name === promptName)

if (prompt) {
promptArgs = parsePromptArguments(prompt, args)
}
}

// Execute the MCP prompt to get the actual content
commandContent = await executeMcpPrompt(mcpHub, serverName, promptName, promptArgs)
} catch (error) {
console.error(`Failed to execute MCP prompt ${command.name}:`, error)
commandContent = `Error executing MCP prompt: ${error instanceof Error ? error.message : String(error)}`
}
}
}

const toolMessage = JSON.stringify({
tool: "runSlashCommand",
command: commandName,
Expand Down Expand Up @@ -94,7 +138,7 @@ export async function runSlashCommandTool(
}

result += `\nSource: ${command.source}`
result += `\n\n--- Command Content ---\n\n${command.content}`
result += `\n\n--- Command Content ---\n\n${commandContent}`

// Return the command content as the tool result
pushToolResult(result)
Expand Down
7 changes: 5 additions & 2 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2817,7 +2817,10 @@ export const webviewMessageHandler = async (
try {
if (message.text) {
const { getCommand } = await import("../../services/command/commands")
const command = await getCommand(getCurrentCwd(), message.text)
const { executeMcpPrompt, parsePromptArguments } = await import(
"../../services/command/mcp-prompts"
)
const command = await getCommand(getCurrentCwd(), message.text, provider.mcpHub)

if (command && command.filePath) {
openFile(command.filePath)
Expand Down Expand Up @@ -2954,7 +2957,7 @@ export const webviewMessageHandler = async (

// Refresh commands list
const { getCommands } = await import("../../services/command/commands")
const commands = await getCommands(getCurrentCwd() || "")
const commands = await getCommands(getCurrentCwd() || "", provider.mcpHub)
const commandList = commands.map((command) => ({
name: command.name,
source: command.source,
Expand Down
26 changes: 13 additions & 13 deletions src/services/command/__tests__/frontmatter-commands.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ npm run build
mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
mockFs.readFile = vi.fn().mockResolvedValue(commandContent)

const result = await getCommand("/test/cwd", "setup")
const result = await getCommand("/test/cwd", "setup", undefined)

expect(result).toEqual({
name: "setup",
Expand All @@ -64,7 +64,7 @@ npm run build
mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
mockFs.readFile = vi.fn().mockResolvedValue(commandContent)

const result = await getCommand("/test/cwd", "setup")
const result = await getCommand("/test/cwd", "setup", undefined)

expect(result).toEqual({
name: "setup",
Expand All @@ -89,7 +89,7 @@ Command content here.`
mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
mockFs.readFile = vi.fn().mockResolvedValue(commandContent)

const result = await getCommand("/test/cwd", "setup")
const result = await getCommand("/test/cwd", "setup", undefined)

expect(result?.description).toBeUndefined()
})
Expand All @@ -107,7 +107,7 @@ Command content here.`
mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
mockFs.readFile = vi.fn().mockResolvedValue(commandContent)

const result = await getCommand("/test/cwd", "setup")
const result = await getCommand("/test/cwd", "setup", undefined)

expect(result).toEqual({
name: "setup",
Expand Down Expand Up @@ -142,7 +142,7 @@ Global setup instructions.`
.mockResolvedValueOnce(projectCommandContent) // First call for project
.mockResolvedValueOnce(globalCommandContent) // Second call for global (shouldn't be used)

const result = await getCommand("/test/cwd", "setup")
const result = await getCommand("/test/cwd", "setup", undefined)

expect(result).toEqual({
name: "setup",
Expand All @@ -169,7 +169,7 @@ Global setup instructions.`
.mockRejectedValueOnce(new Error("File not found")) // Project command doesn't exist
.mockResolvedValueOnce(globalCommandContent) // Global command exists

const result = await getCommand("/test/cwd", "setup")
const result = await getCommand("/test/cwd", "setup", undefined)

expect(result).toEqual({
name: "setup",
Expand All @@ -196,7 +196,7 @@ Create a new release.`
mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
mockFs.readFile = vi.fn().mockResolvedValue(commandContent)

const result = await getCommand("/test/cwd", "release")
const result = await getCommand("/test/cwd", "release", undefined)

expect(result).toEqual({
name: "release",
Expand All @@ -222,7 +222,7 @@ Deploy the application.`
mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
mockFs.readFile = vi.fn().mockResolvedValue(commandContent)

const result = await getCommand("/test/cwd", "deploy")
const result = await getCommand("/test/cwd", "deploy", undefined)

expect(result).toEqual({
name: "deploy",
Expand All @@ -247,7 +247,7 @@ Test content.`
mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
mockFs.readFile = vi.fn().mockResolvedValue(commandContent)

const result = await getCommand("/test/cwd", "test")
const result = await getCommand("/test/cwd", "test", undefined)

expect(result?.argumentHint).toBeUndefined()
})
Expand All @@ -265,7 +265,7 @@ Test content.`
mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
mockFs.readFile = vi.fn().mockResolvedValue(commandContent)

const result = await getCommand("/test/cwd", "test")
const result = await getCommand("/test/cwd", "test", undefined)

expect(result?.argumentHint).toBeUndefined()
})
Expand All @@ -283,7 +283,7 @@ Test content.`
mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
mockFs.readFile = vi.fn().mockResolvedValue(commandContent)

const result = await getCommand("/test/cwd", "test")
const result = await getCommand("/test/cwd", "test", undefined)

expect(result?.argumentHint).toBeUndefined()
})
Expand Down Expand Up @@ -324,7 +324,7 @@ Build instructions without frontmatter.`
.mockResolvedValueOnce(deployContent)
.mockResolvedValueOnce(buildContent)

const result = await getCommands("/test/cwd")
const result = await getCommands("/test/cwd", undefined)

expect(result).toHaveLength(3)
expect(result).toEqual(
Expand Down Expand Up @@ -374,7 +374,7 @@ Deploy the app.`
])
mockFs.readFile = vi.fn().mockResolvedValueOnce(releaseContent).mockResolvedValueOnce(deployContent)

const result = await getCommands("/test/cwd")
const result = await getCommands("/test/cwd", undefined)

expect(result).toHaveLength(2)
expect(result).toEqual(
Expand Down
Loading
Loading