feat: implement hooks, plugins, session forks, and vim motions
Add 4 major features to codetyper-cli:
- Hooks System: Lifecycle hooks (PreToolUse, PostToolUse, SessionStart,
SessionEnd, UserPromptSubmit, Stop) with exit code control flow
- Plugin System: Custom tools, commands, and hooks via plugin manifest
- Session Forking: Snapshots, rewind, fork, and switch between branches
- Vim Motions: Normal/Insert/Command/Visual modes with keyboard navigation
New files:
- src/types/{hooks,plugin,session-fork,vim}.ts
- src/constants/{hooks,plugin,session-fork,vim}.ts
- src/services/{hooks-service,plugin-loader,plugin-service,session-fork-service}.ts
- src/stores/vim-store.ts (vanilla)
- src/tui/hooks/{useVimMode,useVimStore,useTodoStore,useThemeStore}.ts
- src/tui/components/VimStatusLine.tsx
Modified:
- src/services/agent.ts (hook integration)
- src/tools/index.ts (plugin tool registration)
- src/stores/{todo-store,theme-store}.ts (converted to vanilla)
- TUI components (updated hook imports)
This commit is contained in:
@@ -11,6 +11,8 @@ export { todoWriteTool } from "@tools/todo-write";
|
||||
export { todoReadTool } from "@tools/todo-read";
|
||||
export { globToolDefinition } from "@tools/glob/definition";
|
||||
export { grepToolDefinition } from "@tools/grep/definition";
|
||||
export { webSearchTool } from "@tools/web-search";
|
||||
export { lspTool } from "@tools/lsp";
|
||||
|
||||
import type { ToolDefinition, FunctionDefinition } from "@tools/types";
|
||||
import { toolToFunction } from "@tools/types";
|
||||
@@ -22,11 +24,18 @@ import { todoWriteTool } from "@tools/todo-write";
|
||||
import { todoReadTool } from "@tools/todo-read";
|
||||
import { globToolDefinition } from "@tools/glob/definition";
|
||||
import { grepToolDefinition } from "@tools/grep/definition";
|
||||
import { webSearchTool } from "@tools/web-search";
|
||||
import { lspTool } from "@tools/lsp";
|
||||
import {
|
||||
isMCPTool,
|
||||
executeMCPTool,
|
||||
getMCPToolsForApi,
|
||||
} from "@services/mcp/tools";
|
||||
import {
|
||||
isPluginTool,
|
||||
getPluginTool,
|
||||
getPluginToolsForApi,
|
||||
} from "@services/plugin-service";
|
||||
import { z } from "zod";
|
||||
|
||||
// All available tools
|
||||
@@ -39,6 +48,8 @@ export const tools: ToolDefinition[] = [
|
||||
grepToolDefinition,
|
||||
todoWriteTool,
|
||||
todoReadTool,
|
||||
webSearchTool,
|
||||
lspTool,
|
||||
];
|
||||
|
||||
// Tools that are read-only (allowed in chat mode)
|
||||
@@ -47,6 +58,8 @@ const READ_ONLY_TOOLS = new Set([
|
||||
"glob",
|
||||
"grep",
|
||||
"todo_read",
|
||||
"web_search",
|
||||
"lsp",
|
||||
]);
|
||||
|
||||
// Map of tools by name
|
||||
@@ -58,7 +71,7 @@ export const toolMap: Map<string, ToolDefinition> = new Map(
|
||||
let mcpToolsCache: Awaited<ReturnType<typeof getMCPToolsForApi>> | null = null;
|
||||
|
||||
/**
|
||||
* Get tool by name (including MCP tools)
|
||||
* Get tool by name (including MCP tools and plugin tools)
|
||||
*/
|
||||
export function getTool(name: string): ToolDefinition | undefined {
|
||||
// Check built-in tools first
|
||||
@@ -67,6 +80,11 @@ export function getTool(name: string): ToolDefinition | undefined {
|
||||
return builtInTool;
|
||||
}
|
||||
|
||||
// Check if it's a plugin tool
|
||||
if (isPluginTool(name)) {
|
||||
return getPluginTool(name);
|
||||
}
|
||||
|
||||
// Check if it's an MCP tool
|
||||
if (isMCPTool(name)) {
|
||||
// Return a wrapper tool definition for MCP tools
|
||||
@@ -132,13 +150,15 @@ export async function getToolsForApiAsync(
|
||||
return builtInTools;
|
||||
}
|
||||
|
||||
// Get MCP tools (uses cache if available)
|
||||
// Get MCP tools and plugin tools
|
||||
try {
|
||||
mcpToolsCache = await getMCPToolsForApi();
|
||||
return [...builtInTools, ...mcpToolsCache];
|
||||
const pluginTools = getPluginToolsForApi();
|
||||
return [...builtInTools, ...pluginTools, ...mcpToolsCache];
|
||||
} catch {
|
||||
// If MCP tools fail to load, just return built-in tools
|
||||
return builtInTools;
|
||||
// If MCP tools fail to load, still include plugin tools
|
||||
const pluginTools = getPluginToolsForApi();
|
||||
return [...builtInTools, ...pluginTools];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,12 +183,15 @@ export function getToolsForApi(
|
||||
return builtInTools;
|
||||
}
|
||||
|
||||
// Include plugin tools
|
||||
const pluginTools = getPluginToolsForApi();
|
||||
|
||||
// Include cached MCP tools if available
|
||||
if (mcpToolsCache) {
|
||||
return [...builtInTools, ...mcpToolsCache];
|
||||
return [...builtInTools, ...pluginTools, ...mcpToolsCache];
|
||||
}
|
||||
|
||||
return builtInTools;
|
||||
return [...builtInTools, ...pluginTools];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
253
src/tools/lsp.ts
Normal file
253
src/tools/lsp.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* LSP Tool - Provides code intelligence capabilities to the agent
|
||||
*
|
||||
* Operations:
|
||||
* - hover: Get hover information at a position
|
||||
* - definition: Jump to definition
|
||||
* - references: Find all references
|
||||
* - symbols: Get document symbols
|
||||
* - diagnostics: Get file diagnostics
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import {
|
||||
lspService,
|
||||
type Diagnostic,
|
||||
type Location,
|
||||
type DocumentSymbol,
|
||||
type Hover,
|
||||
} from "@services/lsp/index";
|
||||
import type { ToolDefinition } from "@tools/types";
|
||||
import fs from "fs/promises";
|
||||
|
||||
const PositionSchema = z.object({
|
||||
line: z.number().describe("Zero-based line number"),
|
||||
character: z.number().describe("Zero-based character offset"),
|
||||
});
|
||||
|
||||
const parametersSchema = z.object({
|
||||
operation: z
|
||||
.enum(["hover", "definition", "references", "symbols", "diagnostics"])
|
||||
.describe("The LSP operation to perform"),
|
||||
file: z.string().describe("Path to the file"),
|
||||
position: PositionSchema.optional().describe(
|
||||
"Position in the file (required for hover, definition, references)",
|
||||
),
|
||||
});
|
||||
|
||||
type LSPParams = z.infer<typeof parametersSchema>;
|
||||
|
||||
const formatDiagnostics = (diagnostics: Diagnostic[]): string => {
|
||||
if (diagnostics.length === 0) {
|
||||
return "No diagnostics found.";
|
||||
}
|
||||
|
||||
const severityNames = ["", "Error", "Warning", "Info", "Hint"];
|
||||
|
||||
return diagnostics
|
||||
.map((d) => {
|
||||
const severity = severityNames[d.severity ?? 1];
|
||||
const location = `${d.range.start.line + 1}:${d.range.start.character + 1}`;
|
||||
const source = d.source ? `[${d.source}] ` : "";
|
||||
return `${severity} at ${location}: ${source}${d.message}`;
|
||||
})
|
||||
.join("\n");
|
||||
};
|
||||
|
||||
const formatLocation = (loc: Location): string => {
|
||||
const file = loc.uri.replace("file://", "");
|
||||
const line = loc.range.start.line + 1;
|
||||
const char = loc.range.start.character + 1;
|
||||
return `${file}:${line}:${char}`;
|
||||
};
|
||||
|
||||
const formatLocations = (locations: Location | Location[] | null): string => {
|
||||
if (!locations) {
|
||||
return "No locations found.";
|
||||
}
|
||||
|
||||
const locs = Array.isArray(locations) ? locations : [locations];
|
||||
if (locs.length === 0) {
|
||||
return "No locations found.";
|
||||
}
|
||||
|
||||
return locs.map(formatLocation).join("\n");
|
||||
};
|
||||
|
||||
const formatSymbols = (symbols: DocumentSymbol[], indent = 0): string => {
|
||||
const kindNames: Record<number, string> = {
|
||||
1: "File",
|
||||
2: "Module",
|
||||
3: "Namespace",
|
||||
4: "Package",
|
||||
5: "Class",
|
||||
6: "Method",
|
||||
7: "Property",
|
||||
8: "Field",
|
||||
9: "Constructor",
|
||||
10: "Enum",
|
||||
11: "Interface",
|
||||
12: "Function",
|
||||
13: "Variable",
|
||||
14: "Constant",
|
||||
15: "String",
|
||||
16: "Number",
|
||||
17: "Boolean",
|
||||
18: "Array",
|
||||
19: "Object",
|
||||
20: "Key",
|
||||
21: "Null",
|
||||
22: "EnumMember",
|
||||
23: "Struct",
|
||||
24: "Event",
|
||||
25: "Operator",
|
||||
26: "TypeParameter",
|
||||
};
|
||||
|
||||
const lines: string[] = [];
|
||||
const prefix = " ".repeat(indent);
|
||||
|
||||
for (const symbol of symbols) {
|
||||
const kind = kindNames[symbol.kind] ?? "Unknown";
|
||||
const line = symbol.range.start.line + 1;
|
||||
lines.push(`${prefix}${kind}: ${symbol.name} (line ${line})`);
|
||||
|
||||
if (symbol.children && symbol.children.length > 0) {
|
||||
lines.push(formatSymbols(symbol.children, indent + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
};
|
||||
|
||||
const formatHover = (hover: Hover | null): string => {
|
||||
if (!hover) {
|
||||
return "No hover information available.";
|
||||
}
|
||||
|
||||
const contents = hover.contents;
|
||||
|
||||
if (typeof contents === "string") {
|
||||
return contents;
|
||||
}
|
||||
|
||||
if (Array.isArray(contents)) {
|
||||
return contents
|
||||
.map((c) => (typeof c === "string" ? c : c.value))
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
return contents.value;
|
||||
};
|
||||
|
||||
export const lspTool: ToolDefinition = {
|
||||
name: "lsp",
|
||||
description: `Get code intelligence information using Language Server Protocol.
|
||||
|
||||
Operations:
|
||||
- hover: Get type information and documentation at a position
|
||||
- definition: Find where a symbol is defined
|
||||
- references: Find all references to a symbol
|
||||
- symbols: Get all symbols in a document (classes, functions, etc.)
|
||||
- diagnostics: Get errors and warnings for a file
|
||||
|
||||
Examples:
|
||||
- Get hover info: { "operation": "hover", "file": "src/app.ts", "position": { "line": 10, "character": 5 } }
|
||||
- Find definition: { "operation": "definition", "file": "src/app.ts", "position": { "line": 10, "character": 5 } }
|
||||
- Get symbols: { "operation": "symbols", "file": "src/app.ts" }
|
||||
- Get diagnostics: { "operation": "diagnostics", "file": "src/app.ts" }`,
|
||||
parameters: parametersSchema,
|
||||
execute: async (args: LSPParams) => {
|
||||
const { operation, file, position } = args;
|
||||
|
||||
// Check if file exists
|
||||
try {
|
||||
await fs.access(file);
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
title: "File not found",
|
||||
output: `File not found: ${file}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if LSP support is available
|
||||
if (!lspService.hasSupport(file)) {
|
||||
return {
|
||||
success: false,
|
||||
title: "No LSP support",
|
||||
output: `No language server available for this file type.`,
|
||||
};
|
||||
}
|
||||
|
||||
// Open file in LSP
|
||||
await lspService.openFile(file);
|
||||
|
||||
const operationHandlers: Record<string, () => Promise<{ title: string; output: string }>> = {
|
||||
hover: async () => {
|
||||
if (!position) {
|
||||
return { title: "Error", output: "Position required for hover operation" };
|
||||
}
|
||||
const hover = await lspService.getHover(file, position);
|
||||
return { title: "Hover Info", output: formatHover(hover) };
|
||||
},
|
||||
|
||||
definition: async () => {
|
||||
if (!position) {
|
||||
return { title: "Error", output: "Position required for definition operation" };
|
||||
}
|
||||
const definition = await lspService.getDefinition(file, position);
|
||||
return { title: "Definition", output: formatLocations(definition) };
|
||||
},
|
||||
|
||||
references: async () => {
|
||||
if (!position) {
|
||||
return { title: "Error", output: "Position required for references operation" };
|
||||
}
|
||||
const references = await lspService.getReferences(file, position);
|
||||
return {
|
||||
title: `References (${references.length})`,
|
||||
output: formatLocations(references.length > 0 ? references : null),
|
||||
};
|
||||
},
|
||||
|
||||
symbols: async () => {
|
||||
const symbols = await lspService.getDocumentSymbols(file);
|
||||
if (symbols.length === 0) {
|
||||
return { title: "Document Symbols", output: "No symbols found." };
|
||||
}
|
||||
return {
|
||||
title: `Document Symbols (${symbols.length})`,
|
||||
output: formatSymbols(symbols),
|
||||
};
|
||||
},
|
||||
|
||||
diagnostics: async () => {
|
||||
const diagnosticsMap = lspService.getDiagnostics(file);
|
||||
const allDiagnostics: Diagnostic[] = [];
|
||||
for (const diags of diagnosticsMap.values()) {
|
||||
allDiagnostics.push(...diags);
|
||||
}
|
||||
return {
|
||||
title: `Diagnostics (${allDiagnostics.length})`,
|
||||
output: formatDiagnostics(allDiagnostics),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const handler = operationHandlers[operation];
|
||||
if (!handler) {
|
||||
return {
|
||||
success: false,
|
||||
title: "Unknown operation",
|
||||
output: `Unknown LSP operation: ${operation}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await handler();
|
||||
return {
|
||||
success: true,
|
||||
...result,
|
||||
};
|
||||
},
|
||||
};
|
||||
7
src/tools/web-search.ts
Normal file
7
src/tools/web-search.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Web Search tool for searching the web
|
||||
*/
|
||||
|
||||
export { webSearchParams, type WebSearchParamsSchema } from "@tools/web-search/params";
|
||||
export { executeWebSearch, webSearchTool } from "@tools/web-search/execute";
|
||||
export type { SearchResult } from "@tools/web-search/execute";
|
||||
242
src/tools/web-search/execute.ts
Normal file
242
src/tools/web-search/execute.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Web Search Tool Execution
|
||||
*
|
||||
* Uses DuckDuckGo HTML search (no API key required)
|
||||
*/
|
||||
|
||||
import {
|
||||
WEB_SEARCH_DEFAULTS,
|
||||
WEB_SEARCH_MESSAGES,
|
||||
WEB_SEARCH_TITLES,
|
||||
WEB_SEARCH_DESCRIPTION,
|
||||
} from "@constants/web-search";
|
||||
import { webSearchParams } from "@tools/web-search/params";
|
||||
import type { ToolDefinition, ToolContext, ToolResult } from "@/types/tools";
|
||||
import type { WebSearchParams } from "@tools/web-search/params";
|
||||
|
||||
export interface SearchResult {
|
||||
title: string;
|
||||
url: string;
|
||||
snippet: string;
|
||||
}
|
||||
|
||||
const createErrorResult = (error: string): ToolResult => ({
|
||||
success: false,
|
||||
title: WEB_SEARCH_TITLES.FAILED,
|
||||
output: "",
|
||||
error,
|
||||
});
|
||||
|
||||
const createNoResultsResult = (query: string): ToolResult => ({
|
||||
success: true,
|
||||
title: WEB_SEARCH_TITLES.NO_RESULTS,
|
||||
output: `No results found for: "${query}"`,
|
||||
});
|
||||
|
||||
const createSuccessResult = (
|
||||
results: SearchResult[],
|
||||
query: string,
|
||||
): ToolResult => {
|
||||
const formattedResults = results
|
||||
.map(
|
||||
(r, i) =>
|
||||
`${i + 1}. **${r.title}**\n ${r.url}\n ${r.snippet}`,
|
||||
)
|
||||
.join("\n\n");
|
||||
|
||||
return {
|
||||
success: true,
|
||||
title: WEB_SEARCH_TITLES.RESULTS(results.length),
|
||||
output: `Search results for "${query}":\n\n${formattedResults}`,
|
||||
metadata: {
|
||||
query,
|
||||
resultCount: results.length,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse DuckDuckGo HTML search results
|
||||
*/
|
||||
const parseSearchResults = (html: string, maxResults: number): SearchResult[] => {
|
||||
const results: SearchResult[] = [];
|
||||
|
||||
// DuckDuckGo lite HTML structure parsing
|
||||
// Look for result links and snippets
|
||||
const resultPattern =
|
||||
/<a[^>]+class="result-link"[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>[\s\S]*?<td[^>]*class="result-snippet"[^>]*>([^<]+)/gi;
|
||||
|
||||
// Alternative pattern for standard DuckDuckGo HTML
|
||||
const altPattern =
|
||||
/<a[^>]+rel="nofollow"[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>[\s\S]*?<span[^>]*>([^<]{20,})/gi;
|
||||
|
||||
// Try result-link pattern first
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = resultPattern.exec(html)) !== null && results.length < maxResults) {
|
||||
const [, url, title, snippet] = match;
|
||||
if (url && title && !url.includes("duckduckgo.com")) {
|
||||
results.push({
|
||||
title: decodeHtmlEntities(title.trim()),
|
||||
url: decodeUrl(url),
|
||||
snippet: decodeHtmlEntities(snippet.trim()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If no results, try alternative pattern
|
||||
if (results.length === 0) {
|
||||
while ((match = altPattern.exec(html)) !== null && results.length < maxResults) {
|
||||
const [, url, title, snippet] = match;
|
||||
if (url && title && !url.includes("duckduckgo.com")) {
|
||||
results.push({
|
||||
title: decodeHtmlEntities(title.trim()),
|
||||
url: decodeUrl(url),
|
||||
snippet: decodeHtmlEntities(snippet.trim()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: extract any external links with reasonable text
|
||||
if (results.length === 0) {
|
||||
const linkPattern = /<a[^>]+href="(https?:\/\/(?!duckduckgo)[^"]+)"[^>]*>([^<]{10,100})<\/a>/gi;
|
||||
const seenUrls = new Set<string>();
|
||||
|
||||
while ((match = linkPattern.exec(html)) !== null && results.length < maxResults) {
|
||||
const [, url, title] = match;
|
||||
if (!seenUrls.has(url) && !url.includes("duckduckgo")) {
|
||||
seenUrls.add(url);
|
||||
results.push({
|
||||
title: decodeHtmlEntities(title.trim()),
|
||||
url: decodeUrl(url),
|
||||
snippet: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* Decode HTML entities
|
||||
*/
|
||||
const decodeHtmlEntities = (text: string): string => {
|
||||
const entities: Record<string, string> = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
""": '"',
|
||||
"'": "'",
|
||||
" ": " ",
|
||||
"'": "'",
|
||||
"/": "/",
|
||||
};
|
||||
|
||||
let decoded = text;
|
||||
for (const [entity, char] of Object.entries(entities)) {
|
||||
decoded = decoded.replace(new RegExp(entity, "g"), char);
|
||||
}
|
||||
|
||||
// Handle numeric entities
|
||||
decoded = decoded.replace(/&#(\d+);/g, (_, code) =>
|
||||
String.fromCharCode(parseInt(code, 10)),
|
||||
);
|
||||
|
||||
return decoded;
|
||||
};
|
||||
|
||||
/**
|
||||
* Decode DuckDuckGo redirect URLs
|
||||
*/
|
||||
const decodeUrl = (url: string): string => {
|
||||
// DuckDuckGo often wraps URLs in redirects
|
||||
if (url.includes("uddg=")) {
|
||||
const match = url.match(/uddg=([^&]+)/);
|
||||
if (match) {
|
||||
return decodeURIComponent(match[1]);
|
||||
}
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform web search using DuckDuckGo
|
||||
*/
|
||||
const performSearch = async (
|
||||
query: string,
|
||||
maxResults: number,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SearchResult[]> => {
|
||||
const encodedQuery = encodeURIComponent(query);
|
||||
|
||||
// Use DuckDuckGo HTML search (lite version for easier parsing)
|
||||
const searchUrl = `https://lite.duckduckgo.com/lite/?q=${encodedQuery}`;
|
||||
|
||||
const response = await fetch(searchUrl, {
|
||||
headers: {
|
||||
"User-Agent": WEB_SEARCH_DEFAULTS.USER_AGENT,
|
||||
Accept: "text/html",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
},
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Search request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
return parseSearchResults(html, maxResults);
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute web search
|
||||
*/
|
||||
export const executeWebSearch = async (
|
||||
args: WebSearchParams,
|
||||
ctx: ToolContext,
|
||||
): Promise<ToolResult> => {
|
||||
const { query, maxResults = 5 } = args;
|
||||
|
||||
if (!query || query.trim().length === 0) {
|
||||
return createErrorResult("Search query is required");
|
||||
}
|
||||
|
||||
ctx.onMetadata?.({
|
||||
title: WEB_SEARCH_TITLES.SEARCHING(query),
|
||||
status: "running",
|
||||
});
|
||||
|
||||
try {
|
||||
// Create timeout with abort signal
|
||||
const timeoutId = setTimeout(
|
||||
() => ctx.abort.abort(),
|
||||
WEB_SEARCH_DEFAULTS.TIMEOUT_MS,
|
||||
);
|
||||
|
||||
const results = await performSearch(query, maxResults, ctx.abort.signal);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (results.length === 0) {
|
||||
return createNoResultsResult(query);
|
||||
}
|
||||
|
||||
return createSuccessResult(results, query);
|
||||
} catch (error) {
|
||||
if (ctx.abort.signal.aborted) {
|
||||
return createErrorResult(WEB_SEARCH_MESSAGES.TIMEOUT);
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return createErrorResult(WEB_SEARCH_MESSAGES.SEARCH_ERROR(message));
|
||||
}
|
||||
};
|
||||
|
||||
export const webSearchTool: ToolDefinition<typeof webSearchParams> = {
|
||||
name: "web_search",
|
||||
description: WEB_SEARCH_DESCRIPTION,
|
||||
parameters: webSearchParams,
|
||||
execute: executeWebSearch,
|
||||
};
|
||||
17
src/tools/web-search/params.ts
Normal file
17
src/tools/web-search/params.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Web Search Tool Parameters
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export const webSearchParams = z.object({
|
||||
query: z.string().describe("The search query"),
|
||||
maxResults: z
|
||||
.number()
|
||||
.optional()
|
||||
.default(5)
|
||||
.describe("Maximum number of results to return (default: 5)"),
|
||||
});
|
||||
|
||||
export type WebSearchParamsSchema = typeof webSearchParams;
|
||||
export type WebSearchParams = z.infer<typeof webSearchParams>;
|
||||
Reference in New Issue
Block a user