2025-11-15 01:51:22 +05:30
import { z , ZodType } from "zod" ;
import * as fs from "fs/promises" ;
import * as path from "path" ;
2025-12-02 13:24:58 +05:30
import { WorkDir as BASE_DIR } from "../../config/config.js" ;
2025-11-15 01:51:22 +05:30
import { executeCommand } from "./command-executor.js" ;
2025-11-16 11:36:50 +05:30
import { resolveSkill , availableSkills } from "../assistant/skills/index.js" ;
2025-12-02 13:24:58 +05:30
import { executeTool , listServers , listTools } from "../../mcp/mcp.js" ;
import container from "../../di/container.js" ;
import { IMcpConfigRepo } from "../..//mcp/repo.js" ;
2025-12-16 14:48:04 +05:30
import { McpServerDefinition } from "../../mcp/schema.js" ;
2025-11-15 01:51:22 +05:30
const BuiltinToolsSchema = z . record ( z . string ( ) , z . object ( {
description : z.string ( ) ,
inputSchema : z.custom < ZodType > ( ) ,
execute : z.function ( {
input : z.any ( ) ,
output : z.promise ( z . any ( ) ) ,
} ) ,
} ) ) ;
export const BuiltinTools : z.infer < typeof BuiltinToolsSchema > = {
2025-11-16 11:36:50 +05:30
loadSkill : {
description : "Load a Rowboat skill definition into context by fetching its guidance string" ,
inputSchema : z.object ( {
skillName : z.string ( ) . describe ( "Skill identifier or path (e.g., 'workflow-run-ops' or 'src/application/assistant/skills/workflow-run-ops/skill.ts')" ) ,
} ) ,
execute : async ( { skillName } : { skillName : string } ) = > {
const resolved = resolveSkill ( skillName ) ;
if ( ! resolved ) {
return {
success : false ,
message : ` Skill ' ${ skillName } ' not found. Available skills: ${ availableSkills . join ( ", " ) } ` ,
} ;
}
return {
success : true ,
skillName : resolved.id ,
path : resolved.catalogPath ,
content : resolved.content ,
} ;
} ,
} ,
2025-11-15 01:51:22 +05:30
exploreDirectory : {
2025-11-17 23:27:00 +05:30
description : 'Recursively explore directory structure to understand existing agents and file organization' ,
2025-11-15 01:51:22 +05:30
inputSchema : z.object ( {
subdirectory : z.string ( ) . optional ( ) . describe ( 'Subdirectory to explore (optional, defaults to root)' ) ,
maxDepth : z.number ( ) . optional ( ) . describe ( 'Maximum depth to traverse (default: 3)' ) ,
} ) ,
execute : async ( { subdirectory , maxDepth = 3 } : { subdirectory? : string , maxDepth? : number } ) = > {
async function explore ( dir : string , depth : number = 0 ) : Promise < any > {
if ( depth > maxDepth ) return null ;
try {
const entries = await fs . readdir ( dir , { withFileTypes : true } ) ;
const result : any = { files : [ ] , directories : { } } ;
for ( const entry of entries ) {
const fullPath = path . join ( dir , entry . name ) ;
if ( entry . isFile ( ) ) {
const ext = path . extname ( entry . name ) ;
const size = ( await fs . stat ( fullPath ) ) . size ;
result . files . push ( {
name : entry.name ,
type : ext || 'no-extension' ,
size : size ,
relativePath : path.relative ( BASE_DIR , fullPath ) ,
} ) ;
} else if ( entry . isDirectory ( ) ) {
result . directories [ entry . name ] = await explore ( fullPath , depth + 1 ) ;
}
}
return result ;
} catch ( error ) {
return { error : error instanceof Error ? error . message : 'Unknown error' } ;
}
}
const dirPath = subdirectory ? path . join ( BASE_DIR , subdirectory ) : BASE_DIR ;
const structure = await explore ( dirPath ) ;
return {
success : true ,
basePath : path.relative ( BASE_DIR , dirPath ) || '.' ,
structure ,
} ;
} ,
} ,
readFile : {
description : 'Read and parse file contents. For JSON files, provides parsed structure.' ,
inputSchema : z.object ( {
filename : z.string ( ) . describe ( 'The name of the file to read (relative to .rowboat directory)' ) ,
} ) ,
execute : async ( { filename } : { filename : string } ) = > {
try {
const filePath = path . join ( BASE_DIR , filename ) ;
const content = await fs . readFile ( filePath , 'utf-8' ) ;
let parsed = null ;
let fileType = path . extname ( filename ) ;
if ( fileType === '.json' ) {
try {
parsed = JSON . parse ( content ) ;
} catch {
parsed = { error : 'Invalid JSON' } ;
}
}
return {
success : true ,
filename ,
fileType ,
content ,
parsed ,
path : filePath ,
size : content.length ,
} ;
} catch ( error ) {
return {
success : false ,
message : ` Failed to read file: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
} ;
}
} ,
} ,
createFile : {
description : 'Create a new file with content. Automatically creates parent directories if needed.' ,
inputSchema : z.object ( {
filename : z.string ( ) . describe ( 'The name of the file to create (relative to .rowboat directory)' ) ,
content : z.string ( ) . describe ( 'The content to write to the file' ) ,
description : z.string ( ) . optional ( ) . describe ( 'Optional description of why this file is being created' ) ,
} ) ,
execute : async ( { filename , content , description } : { filename : string , content : string , description? : string } ) = > {
try {
const filePath = path . join ( BASE_DIR , filename ) ;
const dir = path . dirname ( filePath ) ;
// Ensure directory exists
await fs . mkdir ( dir , { recursive : true } ) ;
// Write file
await fs . writeFile ( filePath , content , 'utf-8' ) ;
return {
success : true ,
message : ` File ' ${ filename } ' created successfully ` ,
description : description || 'No description provided' ,
path : filePath ,
size : content.length ,
} ;
} catch ( error ) {
return {
success : false ,
message : ` Failed to create file: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
} ;
}
} ,
} ,
updateFile : {
description : 'Update or overwrite the contents of an existing file' ,
inputSchema : z.object ( {
filename : z.string ( ) . describe ( 'The name of the file to update (relative to .rowboat directory)' ) ,
content : z.string ( ) . describe ( 'The new content to write to the file' ) ,
reason : z.string ( ) . optional ( ) . describe ( 'Optional reason for the update' ) ,
} ) ,
execute : async ( { filename , content , reason } : { filename : string , content : string , reason? : string } ) = > {
try {
const filePath = path . join ( BASE_DIR , filename ) ;
// Check if file exists
await fs . access ( filePath ) ;
// Update file
await fs . writeFile ( filePath , content , 'utf-8' ) ;
return {
success : true ,
message : ` File ' ${ filename } ' updated successfully ` ,
reason : reason || 'No reason provided' ,
path : filePath ,
size : content.length ,
} ;
} catch ( error ) {
return {
success : false ,
message : ` Failed to update file: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
} ;
}
} ,
} ,
deleteFile : {
description : 'Delete a file from the .rowboat directory' ,
inputSchema : z.object ( {
filename : z.string ( ) . describe ( 'The name of the file to delete (relative to .rowboat directory)' ) ,
} ) ,
execute : async ( { filename } : { filename : string } ) = > {
try {
const filePath = path . join ( BASE_DIR , filename ) ;
await fs . unlink ( filePath ) ;
return {
success : true ,
message : ` File ' ${ filename } ' deleted successfully ` ,
path : filePath ,
} ;
} catch ( error ) {
return {
success : false ,
message : ` Failed to delete file: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
} ;
}
} ,
} ,
listFiles : {
description : 'List all files and directories in the .rowboat directory or subdirectory' ,
inputSchema : z.object ( {
subdirectory : z.string ( ) . optional ( ) . describe ( 'Optional subdirectory to list (relative to .rowboat directory)' ) ,
} ) ,
execute : async ( { subdirectory } : { subdirectory? : string } ) = > {
try {
const dirPath = subdirectory ? path . join ( BASE_DIR , subdirectory ) : BASE_DIR ;
const entries = await fs . readdir ( dirPath , { withFileTypes : true } ) ;
const files = entries
. filter ( entry = > entry . isFile ( ) )
. map ( entry = > ( {
name : entry.name ,
type : path . extname ( entry . name ) || 'no-extension' ,
relativePath : path.relative ( BASE_DIR , path . join ( dirPath , entry . name ) ) ,
} ) ) ;
const directories = entries
. filter ( entry = > entry . isDirectory ( ) )
. map ( entry = > entry . name ) ;
return {
success : true ,
path : dirPath ,
relativePath : path.relative ( BASE_DIR , dirPath ) || '.' ,
files ,
directories ,
totalFiles : files.length ,
totalDirectories : directories.length ,
} ;
} catch ( error ) {
return {
success : false ,
message : ` Failed to list files: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
} ;
}
} ,
} ,
2025-11-17 23:27:00 +05:30
analyzeAgent : {
description : 'Read and analyze an agent file to understand its structure, tools, and configuration' ,
2025-11-15 01:51:22 +05:30
inputSchema : z.object ( {
2025-11-17 23:27:00 +05:30
agentName : z.string ( ) . describe ( 'Name of the agent file to analyze (with or without .json extension)' ) ,
2025-11-15 01:51:22 +05:30
} ) ,
2025-11-17 23:27:00 +05:30
execute : async ( { agentName } : { agentName : string } ) = > {
2025-11-15 01:51:22 +05:30
try {
2025-11-17 23:27:00 +05:30
const filename = agentName . endsWith ( '.json' ) ? agentName : ` ${ agentName } .json ` ;
const filePath = path . join ( BASE_DIR , 'agents' , filename ) ;
2025-11-15 01:51:22 +05:30
const content = await fs . readFile ( filePath , 'utf-8' ) ;
2025-11-17 23:27:00 +05:30
const agent = JSON . parse ( content ) ;
2025-11-15 01:51:22 +05:30
// Extract key information
2025-11-17 23:27:00 +05:30
const toolsList = agent . tools ? Object . keys ( agent . tools ) : [ ] ;
const agentTools = agent . tools ? Object . entries ( agent . tools ) . map ( ( [ key , tool ] : [ string , any ] ) = > ( {
key ,
type : tool . type ,
name : tool.name || key ,
} ) ) : [ ] ;
2025-11-15 01:51:22 +05:30
const analysis = {
2025-11-17 23:27:00 +05:30
name : agent.name ,
description : agent.description || 'No description' ,
model : agent.model || 'Not specified' ,
toolCount : toolsList.length ,
tools : agentTools ,
hasOtherAgents : agentTools.some ( ( t : any ) = > t . type === 'agent' ) ,
structure : agent ,
2025-11-15 01:51:22 +05:30
} ;
return {
success : true ,
filePath : path.relative ( BASE_DIR , filePath ) ,
analysis ,
} ;
} catch ( error ) {
return {
success : false ,
2025-11-17 23:27:00 +05:30
message : ` Failed to analyze agent: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
2025-11-15 01:51:22 +05:30
} ;
}
} ,
} ,
2025-11-24 10:44:05 +05:30
addMcpServer : {
description : 'Add or update an MCP server in the configuration with validation. This ensures the server definition is valid before saving.' ,
inputSchema : z.object ( {
serverName : z.string ( ) . describe ( 'Name/alias for the MCP server' ) ,
2025-12-02 13:24:58 +05:30
config : McpServerDefinition ,
2025-11-24 10:44:05 +05:30
} ) ,
2025-12-02 13:24:58 +05:30
execute : async ( { serverName , config } : {
2025-11-24 10:44:05 +05:30
serverName : string ;
2025-12-02 13:24:58 +05:30
config : z.infer < typeof McpServerDefinition > ;
2025-11-24 10:44:05 +05:30
} ) = > {
try {
2025-12-02 13:24:58 +05:30
const validationResult = McpServerDefinition . safeParse ( config ) ;
2025-11-24 10:44:05 +05:30
if ( ! validationResult . success ) {
return {
success : false ,
message : 'Server definition failed validation. Check the errors below.' ,
validationErrors : validationResult.error.issues.map ( ( e : any ) = > ` ${ e . path . join ( '.' ) } : ${ e . message } ` ) ,
2025-12-02 13:24:58 +05:30
providedDefinition : config ,
2025-11-24 10:44:05 +05:30
} ;
}
2025-12-02 13:24:58 +05:30
const repo = container . resolve < IMcpConfigRepo > ( 'mcpConfigRepo' ) ;
await repo . upsert ( serverName , config ) ;
2025-11-24 10:44:05 +05:30
return {
success : true ,
serverName ,
} ;
} catch ( error ) {
return {
2025-12-02 13:24:58 +05:30
error : ` Failed to update MCP server: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
2025-11-24 10:44:05 +05:30
} ;
}
} ,
} ,
2025-11-15 01:51:22 +05:30
listMcpServers : {
description : 'List all available MCP servers from the configuration' ,
inputSchema : z.object ( { } ) ,
2025-12-02 13:24:58 +05:30
execute : async ( ) = > {
2025-11-15 01:51:22 +05:30
try {
2025-12-02 13:24:58 +05:30
const result = await listServers ( ) ;
2025-11-15 01:51:22 +05:30
return {
2025-12-02 13:24:58 +05:30
result ,
count : Object.keys ( result . mcpServers ) . length ,
2025-11-15 01:51:22 +05:30
} ;
} catch ( error ) {
return {
2025-12-02 13:24:58 +05:30
error : ` Failed to list MCP servers: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
2025-11-15 01:51:22 +05:30
} ;
}
} ,
} ,
listMcpTools : {
description : 'List all available tools from a specific MCP server' ,
inputSchema : z.object ( {
serverName : z.string ( ) . describe ( 'Name of the MCP server to query' ) ,
2025-12-02 13:24:58 +05:30
cursor : z.string ( ) . optional ( ) ,
2025-11-15 01:51:22 +05:30
} ) ,
2025-12-02 13:24:58 +05:30
execute : async ( { serverName , cursor } : { serverName : string , cursor? : string } ) = > {
2025-11-15 01:51:22 +05:30
try {
2025-12-02 13:24:58 +05:30
const result = await listTools ( serverName , cursor ) ;
2025-11-15 01:51:22 +05:30
return {
serverName ,
2025-12-02 13:24:58 +05:30
result ,
count : result.tools.length ,
2025-11-15 01:51:22 +05:30
} ;
} catch ( error ) {
return {
2025-12-02 13:24:58 +05:30
error : ` Failed to list MCP tools: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
2025-11-15 01:51:22 +05:30
} ;
}
} ,
} ,
2025-12-02 10:34:11 +05:30
executeMcpTool : {
description : 'Execute a specific tool from an MCP server. Use this to run MCP tools on behalf of the user. IMPORTANT: Always use listMcpTools first to get the tool\'s inputSchema, then match the required parameters exactly in the arguments field.' ,
inputSchema : z.object ( {
serverName : z.string ( ) . describe ( 'Name of the MCP server that provides the tool' ) ,
toolName : z.string ( ) . describe ( 'Name of the tool to execute' ) ,
arguments : z.record ( z . string ( ) , z . any ( ) ) . optional ( ) . describe ( 'Arguments to pass to the tool (as key-value pairs matching the tool\'s input schema). MUST include all required parameters from the tool\'s inputSchema.' ) ,
} ) ,
execute : async ( { serverName , toolName , arguments : args = { } } : { serverName : string , toolName : string , arguments? : Record < string , any > } ) = > {
try {
2025-12-02 13:24:58 +05:30
const result = await executeTool ( serverName , toolName , args ) ;
2025-12-02 10:34:11 +05:30
return {
success : true ,
serverName ,
toolName ,
2025-12-02 13:24:58 +05:30
result ,
2025-12-02 10:34:11 +05:30
message : ` Successfully executed tool ' ${ toolName } ' from server ' ${ serverName } ' ` ,
} ;
} catch ( error ) {
return {
success : false ,
2025-12-02 13:24:58 +05:30
error : ` Failed to execute MCP tool: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
2025-12-02 10:34:11 +05:30
hint : 'Use listMcpTools to verify the tool exists and check its schema. Ensure all required parameters are provided in the arguments field.' ,
} ;
}
} ,
} ,
2025-11-15 01:51:22 +05:30
executeCommand : {
description : 'Execute a shell command and return the output. Use this to run bash/shell commands.' ,
inputSchema : z.object ( {
command : z.string ( ) . describe ( 'The shell command to execute (e.g., "ls -la", "cat file.txt")' ) ,
cwd : z.string ( ) . optional ( ) . describe ( 'Working directory to execute the command in (defaults to .rowboat directory)' ) ,
} ) ,
execute : async ( { command , cwd } : { command : string , cwd? : string } ) = > {
try {
const workingDir = cwd ? path . join ( BASE_DIR , cwd ) : BASE_DIR ;
const result = await executeCommand ( command , { cwd : workingDir } ) ;
return {
success : result.exitCode === 0 ,
stdout : result.stdout ,
stderr : result.stderr ,
exitCode : result.exitCode ,
command ,
workingDir ,
} ;
} catch ( error ) {
return {
success : false ,
message : ` Failed to execute command: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
command ,
} ;
}
} ,
} ,
2025-11-16 11:36:50 +05:30
} ;