import { FileOperationsService } from './file-operations'
export interface ToolCall {
id: string
type: 'function'
function: {
name: string
arguments: string
}
}
export interface ToolResult {
role: 'tool'
tool_call_id: string
name: string
content: string
}
export class ToolExecutor {
private fileService: FileOperationsService
constructor(customSupabaseClient?: any) {
this.fileService = new FileOperationsService(customSupabaseClient)
}
getToolDefinitions() {
return [
{
type: "function",
function: {
name: "search_files",
description: "๐ PRIMARY DISCOVERY TOOL: Use this FIRST for any user request involving specific files, characters, chapters, or content. Searches by name or content and returns file IDs needed for other tools. Essential for: questions about existing content, edit requests, file references. TIP: If this doesn't find what you're looking for, try list_files to see what's available, or try variations with extensions like 'overview.md' or partial matches.",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query to find files (e.g., 'Kris', 'chapter 1', 'character notes', 'outline'). Try different variations if first search fails."
}
},
required: ["query"]
}
}
},
{
type: "function",
function: {
name: "smart_search",
description: "๐ฏ INTELLIGENT SEARCH: Use when regular search_files fails or you need to find files with variations. Tries multiple search strategies: exact match, partial match, with extensions, similar names, etc. Great for finding files when you know part of the name but search_files returned empty.",
parameters: {
type: "object",
properties: {
base_name: {
type: "string",
description: "Base name to search for (e.g., 'book-overview', 'character notes')"
}
},
required: ["base_name"]
}
}
},
{
type: "function",
function: {
name: "list_files",
description: "๐ EXPLORATION TOOL: Use to see project structure and discover what files exist. Good for understanding organization before working with specific files, or when user asks 'what do I have' or wants to see their project.",
parameters: {
type: "object",
properties: {
folder_id: {
type: "string",
description: "Optional: ID of a specific folder to list. If not provided, lists root level."
}
},
required: []
}
}
},
{
type: "function",
function: {
name: "read_file",
description: "๐ CONTENT READER: Get complete file content for analysis, editing, or answering questions. ALWAYS use after search_files to get the actual content. CRITICAL: Only use file_id from search_files or list_files results, NEVER use file names as IDs.",
parameters: {
type: "object",
properties: {
file_id: {
type: "string",
description: "The UUID file ID from search_files or list_files results (NOT the file name)"
}
},
required: ["file_id"]
}
}
},
{
type: "function",
function: {
name: "update_file",
description: "โ๏ธ CONTENT EDITOR: Update/edit existing files with new content. Use after reading current content. CRITICAL: Only use file_id from search_files or list_files results, NEVER use file names as IDs.",
parameters: {
type: "object",
properties: {
file_id: {
type: "string",
description: "The UUID file ID from search_files or list_files results (NOT the file name)"
},
new_content: {
type: "string",
description: "The new content for the file"
},
change_summary: {
type: "string",
description: "Brief summary of what changes were made"
}
},
required: ["file_id", "new_content", "change_summary"]
}
}
},
{
type: "function",
function: {
name: "create_file",
description: "๐ CONTENT CREATOR: Create new files when user wants to add content that doesn't exist yet. Use when user asks to create something new.",
parameters: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the new file (e.g., 'anya.md', 'chapter-2.md', 'character-notes.md'). Do NOT include folder paths."
},
content: {
type: "string",
description: "Initial content for the new file"
},
parent_folder_id: {
type: "string",
description: "Optional: ID of the parent folder. If not provided, creates in root."
}
},
required: ["name", "content"]
}
}
},
{
type: "function",
function: {
name: "create_folder",
description: "๐ FOLDER CREATOR: Create new folders to organize files. Use before creating files that should be organized in folders. Essential for creating proper folder structure.",
parameters: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the new folder (e.g., 'characters', 'chapters', 'research')"
},
parent_folder_id: {
type: "string",
description: "Optional: ID of the parent folder. If not provided, creates in root."
}
},
required: ["name"]
}
}
},
{
type: "function",
function: {
name: "delete_file",
description: "๐๏ธ CONTENT REMOVER: Delete files or folders that are no longer needed. Use carefully and only when explicitly requested by user.",
parameters: {
type: "object",
properties: {
file_id: {
type: "string",
description: "The UUID file ID from search_files or list_files results (NOT the file name)"
}
},
required: ["file_id"]
}
}
},
{
type: "function",
function: {
name: "grep_files",
description: "๐ TEXT SEARCH TOOL: Search for specific text patterns within file contents. Use to find exact phrases, keywords, or patterns across your project. More precise than search_files for finding specific content within files.",
parameters: {
type: "object",
properties: {
pattern: {
type: "string",
description: "The text pattern or phrase to search for within files"
},
file_filter: {
type: "string",
description: "Optional: Filter to specific file types or names (e.g., '*.md', 'chapter', etc.)"
},
case_sensitive: {
type: "boolean",
description: "Optional: Whether the search should be case sensitive (default: false)"
}
},
required: ["pattern"]
}
}
}
]
}
async executeToolCall(bookId: string, toolCall: ToolCall, userId?: string): Promise<any> {
const { name: toolName, arguments: toolArgs } = toolCall.function
// Handle empty or invalid JSON arguments
let args: any = {}
if (toolArgs && toolArgs.trim()) {
try {
args = JSON.parse(toolArgs)
} catch (error) {
console.error('Failed to parse tool arguments:', toolArgs, error)
const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error'
throw new Error(`Invalid tool arguments: ${errorMessage}`)
}
}
// Execute the tool directly
return await this.executeTool(toolName, args, bookId)
}
private async executeTool(toolName: string, args: any, bookId: string): Promise<any> {
let result: any
switch (toolName) {
case 'search_files':
result = await this.fileService.searchFiles(bookId, args.query || '')
break
case 'smart_search':
result = await this.smartSearch(bookId, args.base_name || '')
break
case 'read_file':
result = await this.fileService.readFile(bookId, args.file_id)
break
case 'update_file':
result = await this.fileService.updateFile(bookId, args.file_id, args.new_content, args.change_summary || 'Updated via AI')
break
case 'create_file':
result = await this.fileService.createFile(bookId, args.name, args.content || '', args.parent_folder_id)
break
case 'create_folder':
result = await this.fileService.createFolder(bookId, args.name, args.parent_folder_id)
break
case 'delete_file':
result = await this.fileService.deleteFile(bookId, args.file_id)
break
case 'list_files':
result = await this.fileService.listFiles(bookId, args.folder_id)
break
case 'grep_files':
result = await this.fileService.grepFiles(bookId, args.pattern || '', args.file_filter || '', args.case_sensitive || false)
break
default:
throw new Error(`Unknown tool: ${toolName}`)
}
// Dispatch event for file-modifying operations to trigger UI refresh
if (this.isFileModifyingOperation(toolName)) {
console.log(`๐ ToolExecutor: Dispatching file operation completed event for ${toolName}`)
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('ai-file-operation-completed', {
detail: {
operation: toolName,
bookId: bookId,
result: result
}
}))
}
}
return result
}
private isFileModifyingOperation(toolName: string): boolean {
return ['update_file', 'create_file', 'create_folder', 'delete_file'].includes(toolName)
}
private async smartSearch(bookId: string, baseName: string): Promise<any> {
// Try multiple search strategies in order of specificity
const searchVariations = [
baseName, // exact match
`${baseName}.md`, // with .md extension
`${baseName}.txt`, // with .txt extension
baseName.replace(/-/g, ' '), // replace hyphens with spaces
baseName.replace(/_/g, ' '), // replace underscores with spaces
baseName.replace(/[-_]/g, ''), // remove separators
...baseName.split(/[-_\s]+/), // individual words
]
let allResults: any[] = []
const foundFiles = new Set<string>() // to avoid duplicates
// Try each variation
for (const query of searchVariations) {
if (query.length < 2) continue // skip very short queries
try {
const results = await this.fileService.searchFiles(bookId, query)
// Add new unique results
for (const result of results) {
if (!foundFiles.has(result.id)) {
foundFiles.add(result.id)
allResults.push({
...result,
search_query: query, // track which query found this
match_type: query === baseName ? 'exact' : 'variation'
})
}
}
// If we found exact matches, prioritize them
if (query === baseName && results.length > 0) {
break
}
} catch (error) {
console.warn(`Search variation "${query}" failed:`, error)
}
}
// If still no results, try listing files and fuzzy matching
if (allResults.length === 0) {
try {
const allFiles = await this.fileService.listFiles(bookId)
const fuzzyMatches = allFiles.filter((file: any) => {
const fileName = file.name.toLowerCase()
const searchTerm = baseName.toLowerCase()
return fileName.includes(searchTerm) ||
searchTerm.split(/[-_\s]+/).some((word: string) => fileName.includes(word))
})
allResults = fuzzyMatches.map((file: any) => ({
...file,
search_query: 'fuzzy_match',
match_type: 'fuzzy'
}))
} catch (error) {
console.warn('Fuzzy matching failed:', error)
}
}
return {
original_query: baseName,
total_found: allResults.length,
files: allResults.slice(0, 10), // limit to 10 results
search_strategies_used: searchVariations.slice(0, 5)
}
}
getToolDescription(toolName: string, toolArgs: any): string {
switch (toolName) {
case 'search_files':
return `๐ Looking for: *"${toolArgs.query}"*`
case 'smart_search':
return `๐ฏ Intelligent search for: *"${toolArgs.base_name}"* (trying multiple variations)`
case 'read_file':
return `๐ Reading file...`
case 'update_file':
return `โ๏ธ Updating file...`
case 'create_file':
return `๐ Creating: *"${toolArgs.name}"*`
case 'create_folder':
return `๐ Creating folder: *"${toolArgs.name}"*`
case 'list_files':
return `๐ Checking files...`
case 'delete_file':
return `๐๏ธ Deleting file...`
case 'grep_files':
return `๐ Searching for: *"${toolArgs.pattern}"*`
default:
return `๐ ๏ธ Executing ${toolName}...`
}
}
formatToolResult(toolName: string, toolResult: any): string {
switch (toolName) {
case 'search_files':
if (Array.isArray(toolResult) && toolResult.length > 0) {
const fileLinks = toolResult.map((f: any) => `[๐ ${f.name}](file://${f.id})`).join(', ')
return `Found: ${fileLinks}`
} else {
return 'No files found.'
}
case 'smart_search':
if (toolResult.total_found > 0) {
const fileLinks = toolResult.files.map((f: any) => {
const matchInfo = f.match_type === 'exact' ? '๐ฏ' : f.match_type === 'fuzzy' ? '๐' : '๐'
return `${matchInfo} [๐ ${f.name}](file://${f.id})`
}).join(', ')
return `Found ${toolResult.total_found} file(s): ${fileLinks}`
} else {
return `No files found for "${toolResult.original_query}". Tried variations: ${toolResult.search_strategies_used.join(', ')}`
}
case 'read_file':
if (toolResult.content) {
const preview = toolResult.content.length > 100
? toolResult.content.substring(0, 100) + '...'
: toolResult.content
return `**[๐ ${toolResult.name}](file://${toolResult.id})**\n\`\`\`\n${preview}\n\`\`\``
} else {
return `**[๐ ${toolResult.name}](file://${toolResult.id})** (empty)`
}
case 'update_file':
return `โ
Updated [๐ ${toolResult.name}](file://${toolResult.id})`
case 'create_file':
return `โ
Created [๐ ${toolResult.name}](file://${toolResult.id})`
case 'create_folder':
return `โ
Created [๐ ${toolResult.name}](folder://${toolResult.id})`
case 'list_files':
if (Array.isArray(toolResult) && toolResult.length > 0) {
const folders = toolResult.filter((item: any) => item.type === 'folder')
const files = toolResult.filter((item: any) => item.type === 'file')
let message = ''
if (folders.length > 0) {
const folderLinks = folders.map((f: any) => `[๐ ${f.name}](folder://${f.id})`).join(', ')
message += `${folderLinks}\n`
}
if (files.length > 0) {
const fileLinks = files.map((f: any) => `[๐ ${f.name}](file://${f.id})`).join(', ')
message += `${fileLinks}`
}
return message
} else {
return 'Empty folder'
}
case 'delete_file':
return `โ
Deleted ${toolResult.name}`
case 'grep_files':
if (Array.isArray(toolResult) && toolResult.length > 0) {
const summary = toolResult.map((f: any) =>
`**[๐ ${f.name}](file://${f.id})** (${f.total_matches} match${f.total_matches > 1 ? 'es' : ''})`
).join(', ')
let details = ''
toolResult.forEach((file: any) => {
details += `\n\n**[๐ ${file.name}](file://${file.id}):**\n`
file.matches.slice(0, 3).forEach((match: any) => {
details += `Line ${match.line_number}: \`${match.content}\`\n`
})
if (file.matches.length > 3) {
details += `... and ${file.matches.length - 3} more matches\n`
}
})
return `Found in: ${summary}${details}`
} else {
return 'No matches found.'
}
default:
return `**Tool completed successfully**`
}
}
}