diff --git a/src/index.ts b/src/index.ts index aa0c6e73..c0c4c8e2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -86,6 +86,10 @@ import { createTaskGetTool, createTaskList, createTaskUpdateTool, + createGrepTools, + createGlobTools, + createAstGrepTools, + createSessionManagerTools, } from "./tools"; import { CATEGORY_DESCRIPTIONS, @@ -538,6 +542,10 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { const allTools: Record = { ...builtinTools, + ...createGrepTools(ctx), + ...createGlobTools(ctx), + ...createAstGrepTools(ctx), + ...createSessionManagerTools(ctx), ...backgroundTools, call_omo_agent: callOmoAgent, ...(lookAt ? { look_at: lookAt } : {}), diff --git a/src/tools/ast-grep/index.ts b/src/tools/ast-grep/index.ts index 8a02587d..7356d000 100644 --- a/src/tools/ast-grep/index.ts +++ b/src/tools/ast-grep/index.ts @@ -1,12 +1,4 @@ -import type { ToolDefinition } from "@opencode-ai/plugin" -import { ast_grep_search, ast_grep_replace } from "./tools" - -export const builtinTools: Record = { - ast_grep_search, - ast_grep_replace, -} - -export { ast_grep_search, ast_grep_replace } +export { createAstGrepTools } from "./tools" export { ensureAstGrepBinary, getCachedBinaryPath, getCacheDir } from "./downloader" export { getAstGrepPath, isCliAvailable, ensureCliAvailable, startBackgroundInit } from "./cli" export { checkEnvironment, formatEnvironmentCheck } from "./constants" diff --git a/src/tools/ast-grep/tools.ts b/src/tools/ast-grep/tools.ts index 11de9184..57c1d6a9 100644 --- a/src/tools/ast-grep/tools.ts +++ b/src/tools/ast-grep/tools.ts @@ -1,3 +1,4 @@ +import type { PluginInput } from "@opencode-ai/plugin" import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool" import { CLI_LANGUAGES } from "./constants" import { runSg } from "./cli" @@ -34,80 +35,83 @@ function getEmptyResultHint(pattern: string, lang: CliLanguage): string | null { return null } -export const ast_grep_search: ToolDefinition = tool({ - description: - "Search code patterns across filesystem using AST-aware matching. Supports 25 languages. " + - "Use meta-variables: $VAR (single node), $$$ (multiple nodes). " + - "IMPORTANT: Patterns must be complete AST nodes (valid code). " + - "For functions, include params and body: 'export async function $NAME($$$) { $$$ }' not 'export async function $NAME'. " + - "Examples: 'console.log($MSG)', 'def $FUNC($$$):', 'async function $NAME($$$)'", - args: { - pattern: tool.schema.string().describe("AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."), - lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"), - paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search (default: ['.'])"), - globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"), - context: tool.schema.number().optional().describe("Context lines around match"), - }, - execute: async (args, context) => { - try { - const result = await runSg({ - pattern: args.pattern, - lang: args.lang as CliLanguage, - paths: args.paths, - globs: args.globs, - context: args.context, - }) +export function createAstGrepTools(ctx: PluginInput): Record { + const ast_grep_search: ToolDefinition = tool({ + description: + "Search code patterns across filesystem using AST-aware matching. Supports 25 languages. " + + "Use meta-variables: $VAR (single node), $$$ (multiple nodes). " + + "IMPORTANT: Patterns must be complete AST nodes (valid code). " + + "For functions, include params and body: 'export async function $NAME($$$) { $$$ }' not 'export async function $NAME'. " + + "Examples: 'console.log($MSG)', 'def $FUNC($$$):', 'async function $NAME($$$)'", + args: { + pattern: tool.schema.string().describe("AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."), + lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"), + paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search (default: ['.'])"), + globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"), + context: tool.schema.number().optional().describe("Context lines around match"), + }, + execute: async (args, context) => { + try { + const result = await runSg({ + pattern: args.pattern, + lang: args.lang as CliLanguage, + paths: args.paths ?? [ctx.directory], + globs: args.globs, + context: args.context, + }) - let output = formatSearchResult(result) + let output = formatSearchResult(result) - if (result.matches.length === 0 && !result.error) { - const hint = getEmptyResultHint(args.pattern, args.lang as CliLanguage) - if (hint) { - output += `\n\n${hint}` + if (result.matches.length === 0 && !result.error) { + const hint = getEmptyResultHint(args.pattern, args.lang as CliLanguage) + if (hint) { + output += `\n\n${hint}` + } } + + await showOutputToUser(context, output) + return output + } catch (e) { + const output = `Error: ${e instanceof Error ? e.message : String(e)}` + await showOutputToUser(context, output) + return output } + }, + }) - await showOutputToUser(context, output) - return output - } catch (e) { - const output = `Error: ${e instanceof Error ? e.message : String(e)}` - await showOutputToUser(context, output) - return output - } - }, -}) - -export const ast_grep_replace: ToolDefinition = tool({ - description: - "Replace code patterns across filesystem with AST-aware rewriting. " + - "Dry-run by default. Use meta-variables in rewrite to preserve matched content. " + - "Example: pattern='console.log($MSG)' rewrite='logger.info($MSG)'", - args: { - pattern: tool.schema.string().describe("AST pattern to match"), - rewrite: tool.schema.string().describe("Replacement pattern (can use $VAR from pattern)"), - lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"), - paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search"), - globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs"), - dryRun: tool.schema.boolean().optional().describe("Preview changes without applying (default: true)"), - }, - execute: async (args, context) => { - try { - const result = await runSg({ - pattern: args.pattern, - rewrite: args.rewrite, - lang: args.lang as CliLanguage, - paths: args.paths, - globs: args.globs, - updateAll: args.dryRun === false, - }) - const output = formatReplaceResult(result, args.dryRun !== false) - await showOutputToUser(context, output) - return output - } catch (e) { - const output = `Error: ${e instanceof Error ? e.message : String(e)}` - await showOutputToUser(context, output) - return output - } - }, -}) + const ast_grep_replace: ToolDefinition = tool({ + description: + "Replace code patterns across filesystem with AST-aware rewriting. " + + "Dry-run by default. Use meta-variables in rewrite to preserve matched content. " + + "Example: pattern='console.log($MSG)' rewrite='logger.info($MSG)'", + args: { + pattern: tool.schema.string().describe("AST pattern to match"), + rewrite: tool.schema.string().describe("Replacement pattern (can use $VAR from pattern)"), + lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"), + paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search"), + globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs"), + dryRun: tool.schema.boolean().optional().describe("Preview changes without applying (default: true)"), + }, + execute: async (args, context) => { + try { + const result = await runSg({ + pattern: args.pattern, + rewrite: args.rewrite, + lang: args.lang as CliLanguage, + paths: args.paths ?? [ctx.directory], + globs: args.globs, + updateAll: args.dryRun === false, + }) + const output = formatReplaceResult(result, args.dryRun !== false) + await showOutputToUser(context, output) + return output + } catch (e) { + const output = `Error: ${e instanceof Error ? e.message : String(e)}` + await showOutputToUser(context, output) + return output + } + }, + }) + return { ast_grep_search, ast_grep_replace } +} diff --git a/src/tools/glob/index.ts b/src/tools/glob/index.ts index 96c79646..2004321f 100644 --- a/src/tools/glob/index.ts +++ b/src/tools/glob/index.ts @@ -1,3 +1 @@ -import { glob } from "./tools" - -export { glob } +export { createGlobTools } from "./tools" diff --git a/src/tools/glob/tools.ts b/src/tools/glob/tools.ts index 2d8b491e..318d178d 100644 --- a/src/tools/glob/tools.ts +++ b/src/tools/glob/tools.ts @@ -1,43 +1,47 @@ +import type { PluginInput } from "@opencode-ai/plugin" import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool" import { runRgFiles } from "./cli" import { resolveGrepCliWithAutoInstall } from "./constants" import { formatGlobResult } from "./utils" -export const glob: ToolDefinition = tool({ - description: - "Fast file pattern matching tool with safety limits (60s timeout, 100 file limit). " + - "Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\". " + - "Returns matching file paths sorted by modification time. " + - "Use this tool when you need to find files by name patterns.", - args: { - pattern: tool.schema.string().describe("The glob pattern to match files against"), - path: tool.schema - .string() - .optional() - .describe( - "The directory to search in. If not specified, the current working directory will be used. " + - "IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - " + - "simply omit it for the default behavior. Must be a valid directory path if provided." - ), - }, - execute: async (args) => { - try { - const cli = await resolveGrepCliWithAutoInstall() - // Use process.cwd() as the default search path when no path is provided - const searchPath = args.path ?? process.cwd() - const paths = [searchPath] +export function createGlobTools(ctx: PluginInput): Record { + const glob: ToolDefinition = tool({ + description: + "Fast file pattern matching tool with safety limits (60s timeout, 100 file limit). " + + "Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\". " + + "Returns matching file paths sorted by modification time. " + + "Use this tool when you need to find files by name patterns.", + args: { + pattern: tool.schema.string().describe("The glob pattern to match files against"), + path: tool.schema + .string() + .optional() + .describe( + "The directory to search in. If not specified, the current working directory will be used. " + + "IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - " + + "simply omit it for the default behavior. Must be a valid directory path if provided." + ), + }, + execute: async (args) => { + try { + const cli = await resolveGrepCliWithAutoInstall() + const searchPath = args.path ?? ctx.directory + const paths = [searchPath] - const result = await runRgFiles( - { - pattern: args.pattern, - paths, - }, - cli - ) + const result = await runRgFiles( + { + pattern: args.pattern, + paths, + }, + cli + ) - return formatGlobResult(result) - } catch (e) { - return `Error: ${e instanceof Error ? e.message : String(e)}` - } - }, -}) + return formatGlobResult(result) + } catch (e) { + return `Error: ${e instanceof Error ? e.message : String(e)}` + } + }, + }) + + return { glob } +} diff --git a/src/tools/grep/index.ts b/src/tools/grep/index.ts index a457feac..f5979b73 100644 --- a/src/tools/grep/index.ts +++ b/src/tools/grep/index.ts @@ -1,3 +1 @@ -import { grep } from "./tools" - -export { grep } +export { createGrepTools } from "./tools" diff --git a/src/tools/grep/tools.ts b/src/tools/grep/tools.ts index 901764ce..dd55e3c0 100644 --- a/src/tools/grep/tools.ts +++ b/src/tools/grep/tools.ts @@ -1,42 +1,46 @@ +import type { PluginInput } from "@opencode-ai/plugin" import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool" import { runRg } from "./cli" import { formatGrepResult } from "./utils" -export const grep: ToolDefinition = tool({ - description: - "Fast content search tool with safety limits (60s timeout, 10MB output). " + - "Searches file contents using regular expressions. " + - "Supports full regex syntax (eg. \"log.*Error\", \"function\\s+\\w+\", etc.). " + - "Filter files by pattern with the include parameter (eg. \"*.js\", \"*.{ts,tsx}\"). " + - "Returns file paths with matches sorted by modification time.", - args: { - pattern: tool.schema.string().describe("The regex pattern to search for in file contents"), - include: tool.schema - .string() - .optional() - .describe("File pattern to include in the search (e.g. \"*.js\", \"*.{ts,tsx}\")"), - path: tool.schema - .string() - .optional() - .describe("The directory to search in. Defaults to the current working directory."), - }, - execute: async (args) => { - try { - const globs = args.include ? [args.include] : undefined - // Use process.cwd() as the default search path when no path is provided - const searchPath = args.path ?? process.cwd() - const paths = [searchPath] +export function createGrepTools(ctx: PluginInput): Record { + const grep: ToolDefinition = tool({ + description: + "Fast content search tool with safety limits (60s timeout, 10MB output). " + + "Searches file contents using regular expressions. " + + "Supports full regex syntax (eg. \"log.*Error\", \"function\\s+\\w+\", etc.). " + + "Filter files by pattern with the include parameter (eg. \"*.js\", \"*.{ts,tsx}\"). " + + "Returns file paths with matches sorted by modification time.", + args: { + pattern: tool.schema.string().describe("The regex pattern to search for in file contents"), + include: tool.schema + .string() + .optional() + .describe("File pattern to include in the search (e.g. \"*.js\", \"*.{ts,tsx}\")"), + path: tool.schema + .string() + .optional() + .describe("The directory to search in. Defaults to the current working directory."), + }, + execute: async (args) => { + try { + const globs = args.include ? [args.include] : undefined + const searchPath = args.path ?? ctx.directory + const paths = [searchPath] - const result = await runRg({ - pattern: args.pattern, - paths, - globs, - context: 0, - }) + const result = await runRg({ + pattern: args.pattern, + paths, + globs, + context: 0, + }) - return formatGrepResult(result) - } catch (e) { - return `Error: ${e instanceof Error ? e.message : String(e)}` - } - }, -}) + return formatGrepResult(result) + } catch (e) { + return `Error: ${e instanceof Error ? e.message : String(e)}` + } + }, + }) + + return { grep } +} diff --git a/src/tools/index.ts b/src/tools/index.ts index f1437d3c..a38a6c74 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -10,21 +10,11 @@ import { export { lspManager } -import { - ast_grep_search, - ast_grep_replace, -} from "./ast-grep" - -import { grep } from "./grep" -import { glob } from "./glob" +export { createAstGrepTools } from "./ast-grep" +export { createGrepTools } from "./grep" +export { createGlobTools } from "./glob" export { createSlashcommandTool, discoverCommandsSync } from "./slashcommand" - -import { - session_list, - session_read, - session_search, - session_info, -} from "./session-manager" +export { createSessionManagerTools } from "./session-manager" export { sessionExists } from "./session-manager/storage" @@ -70,12 +60,4 @@ export const builtinTools: Record = { lsp_diagnostics, lsp_prepare_rename, lsp_rename, - ast_grep_search, - ast_grep_replace, - grep, - glob, - session_list, - session_read, - session_search, - session_info, } diff --git a/src/tools/session-manager/index.ts b/src/tools/session-manager/index.ts index 5dcd143d..8e9f2806 100644 --- a/src/tools/session-manager/index.ts +++ b/src/tools/session-manager/index.ts @@ -1,3 +1,3 @@ -export * from "./tools" +export { createSessionManagerTools } from "./tools" export * from "./types" export * from "./constants" diff --git a/src/tools/session-manager/tools.ts b/src/tools/session-manager/tools.ts index 1ef917c0..5da95a1a 100644 --- a/src/tools/session-manager/tools.ts +++ b/src/tools/session-manager/tools.ts @@ -1,3 +1,4 @@ +import type { PluginInput } from "@opencode-ai/plugin" import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool" import { SESSION_LIST_DESCRIPTION, @@ -26,121 +27,125 @@ function withTimeout(promise: Promise, ms: number, operation: string): Pro ]) } -export const session_list: ToolDefinition = tool({ - description: SESSION_LIST_DESCRIPTION, - args: { - limit: tool.schema.number().optional().describe("Maximum number of sessions to return"), - from_date: tool.schema.string().optional().describe("Filter sessions from this date (ISO 8601 format)"), - to_date: tool.schema.string().optional().describe("Filter sessions until this date (ISO 8601 format)"), - project_path: tool.schema.string().optional().describe("Filter sessions by project path (default: current working directory)"), - }, - execute: async (args: SessionListArgs, _context) => { - try { - const directory = args.project_path ?? process.cwd() - let sessions = await getMainSessions({ directory }) - let sessionIDs = sessions.map((s) => s.id) +export function createSessionManagerTools(ctx: PluginInput): Record { + const session_list: ToolDefinition = tool({ + description: SESSION_LIST_DESCRIPTION, + args: { + limit: tool.schema.number().optional().describe("Maximum number of sessions to return"), + from_date: tool.schema.string().optional().describe("Filter sessions from this date (ISO 8601 format)"), + to_date: tool.schema.string().optional().describe("Filter sessions until this date (ISO 8601 format)"), + project_path: tool.schema.string().optional().describe("Filter sessions by project path (default: current working directory)"), + }, + execute: async (args: SessionListArgs, _context) => { + try { + const directory = args.project_path ?? ctx.directory + let sessions = await getMainSessions({ directory }) + let sessionIDs = sessions.map((s) => s.id) - if (args.from_date || args.to_date) { - sessionIDs = await filterSessionsByDate(sessionIDs, args.from_date, args.to_date) - } - - if (args.limit && args.limit > 0) { - sessionIDs = sessionIDs.slice(0, args.limit) - } - - return await formatSessionList(sessionIDs) - } catch (e) { - return `Error: ${e instanceof Error ? e.message : String(e)}` - } - }, -}) - -export const session_read: ToolDefinition = tool({ - description: SESSION_READ_DESCRIPTION, - args: { - session_id: tool.schema.string().describe("Session ID to read"), - include_todos: tool.schema.boolean().optional().describe("Include todo list if available (default: false)"), - include_transcript: tool.schema.boolean().optional().describe("Include transcript log if available (default: false)"), - limit: tool.schema.number().optional().describe("Maximum number of messages to return (default: all)"), - }, - execute: async (args: SessionReadArgs, _context) => { - try { - if (!sessionExists(args.session_id)) { - return `Session not found: ${args.session_id}` - } - - let messages = await readSessionMessages(args.session_id) - - if (args.limit && args.limit > 0) { - messages = messages.slice(0, args.limit) - } - - const todos = args.include_todos ? await readSessionTodos(args.session_id) : undefined - - return formatSessionMessages(messages, args.include_todos, todos) - } catch (e) { - return `Error: ${e instanceof Error ? e.message : String(e)}` - } - }, -}) - -export const session_search: ToolDefinition = tool({ - description: SESSION_SEARCH_DESCRIPTION, - args: { - query: tool.schema.string().describe("Search query string"), - session_id: tool.schema.string().optional().describe("Search within specific session only (default: all sessions)"), - case_sensitive: tool.schema.boolean().optional().describe("Case-sensitive search (default: false)"), - limit: tool.schema.number().optional().describe("Maximum number of results to return (default: 20)"), - }, - execute: async (args: SessionSearchArgs, _context) => { - try { - const resultLimit = args.limit && args.limit > 0 ? args.limit : 20 - - const searchOperation = async (): Promise => { - if (args.session_id) { - return searchInSession(args.session_id, args.query, args.case_sensitive, resultLimit) + if (args.from_date || args.to_date) { + sessionIDs = await filterSessionsByDate(sessionIDs, args.from_date, args.to_date) } - const allSessions = await getAllSessions() - const sessionsToScan = allSessions.slice(0, MAX_SESSIONS_TO_SCAN) - - const allResults: SearchResult[] = [] - for (const sid of sessionsToScan) { - if (allResults.length >= resultLimit) break - - const remaining = resultLimit - allResults.length - const sessionResults = await searchInSession(sid, args.query, args.case_sensitive, remaining) - allResults.push(...sessionResults) + if (args.limit && args.limit > 0) { + sessionIDs = sessionIDs.slice(0, args.limit) } - return allResults.slice(0, resultLimit) + return await formatSessionList(sessionIDs) + } catch (e) { + return `Error: ${e instanceof Error ? e.message : String(e)}` } + }, + }) - const results = await withTimeout(searchOperation(), SEARCH_TIMEOUT_MS, "Search") + const session_read: ToolDefinition = tool({ + description: SESSION_READ_DESCRIPTION, + args: { + session_id: tool.schema.string().describe("Session ID to read"), + include_todos: tool.schema.boolean().optional().describe("Include todo list if available (default: false)"), + include_transcript: tool.schema.boolean().optional().describe("Include transcript log if available (default: false)"), + limit: tool.schema.number().optional().describe("Maximum number of messages to return (default: all)"), + }, + execute: async (args: SessionReadArgs, _context) => { + try { + if (!sessionExists(args.session_id)) { + return `Session not found: ${args.session_id}` + } - return formatSearchResults(results) - } catch (e) { - return `Error: ${e instanceof Error ? e.message : String(e)}` - } - }, -}) + let messages = await readSessionMessages(args.session_id) -export const session_info: ToolDefinition = tool({ - description: SESSION_INFO_DESCRIPTION, - args: { - session_id: tool.schema.string().describe("Session ID to inspect"), - }, - execute: async (args: SessionInfoArgs, _context) => { - try { - const info = await getSessionInfo(args.session_id) + if (args.limit && args.limit > 0) { + messages = messages.slice(0, args.limit) + } - if (!info) { - return `Session not found: ${args.session_id}` + const todos = args.include_todos ? await readSessionTodos(args.session_id) : undefined + + return formatSessionMessages(messages, args.include_todos, todos) + } catch (e) { + return `Error: ${e instanceof Error ? e.message : String(e)}` } + }, + }) - return formatSessionInfo(info) - } catch (e) { - return `Error: ${e instanceof Error ? e.message : String(e)}` - } - }, -}) + const session_search: ToolDefinition = tool({ + description: SESSION_SEARCH_DESCRIPTION, + args: { + query: tool.schema.string().describe("Search query string"), + session_id: tool.schema.string().optional().describe("Search within specific session only (default: all sessions)"), + case_sensitive: tool.schema.boolean().optional().describe("Case-sensitive search (default: false)"), + limit: tool.schema.number().optional().describe("Maximum number of results to return (default: 20)"), + }, + execute: async (args: SessionSearchArgs, _context) => { + try { + const resultLimit = args.limit && args.limit > 0 ? args.limit : 20 + + const searchOperation = async (): Promise => { + if (args.session_id) { + return searchInSession(args.session_id, args.query, args.case_sensitive, resultLimit) + } + + const allSessions = await getAllSessions() + const sessionsToScan = allSessions.slice(0, MAX_SESSIONS_TO_SCAN) + + const allResults: SearchResult[] = [] + for (const sid of sessionsToScan) { + if (allResults.length >= resultLimit) break + + const remaining = resultLimit - allResults.length + const sessionResults = await searchInSession(sid, args.query, args.case_sensitive, remaining) + allResults.push(...sessionResults) + } + + return allResults.slice(0, resultLimit) + } + + const results = await withTimeout(searchOperation(), SEARCH_TIMEOUT_MS, "Search") + + return formatSearchResults(results) + } catch (e) { + return `Error: ${e instanceof Error ? e.message : String(e)}` + } + }, + }) + + const session_info: ToolDefinition = tool({ + description: SESSION_INFO_DESCRIPTION, + args: { + session_id: tool.schema.string().describe("Session ID to inspect"), + }, + execute: async (args: SessionInfoArgs, _context) => { + try { + const info = await getSessionInfo(args.session_id) + + if (!info) { + return `Session not found: ${args.session_id}` + } + + return formatSessionInfo(info) + } catch (e) { + return `Error: ${e instanceof Error ? e.message : String(e)}` + } + }, + }) + + return { session_list, session_read, session_search, session_info } +}