import { asSchema } from "ai"; import type { ZodType } from "zod"; import type { ToolSet } from "ai"; import type { JSONSchema7 } from "json-schema"; import { sanitizeToolName, toPascalCase, escapeJsDoc } from "./utils"; import { jsonSchemaToTypeString, type ConversionContext } from "./json-schema-types"; export interface ToolDescriptor { description?: string; inputSchema: ZodType; outputSchema?: ZodType; execute?: (args: unknown) => Promise; } export type ToolDescriptors = Record; /** * Check if a value is a Zod schema (has _zod property). */ function isZodSchema(value: unknown): value is ZodType { return ( value !== null && typeof value === "object" && "_zod" in value && (value as { _zod?: unknown })._zod !== undefined ); } /** * Check if a value conforms to the Standard Schema protocol (~standard). * This catches Zod v3 schemas (which expose ~standard but not _zod). */ function isStandardSchema(value: unknown): boolean { return ( value !== null && typeof value === "object" && "~standard" in value && (value as Record)["~standard"] !== undefined ); } /** * Check if a value is an AI SDK jsonSchema wrapper. * The jsonSchema wrapper has a [Symbol] with jsonSchema property. */ function isJsonSchemaWrapper( value: unknown ): value is { jsonSchema: JSONSchema7 } { if (value === null || typeof value !== "object") return false; if ("jsonSchema" in value) { return true; } const symbols = Object.getOwnPropertySymbols(value); for (const sym of symbols) { const symValue = (value as Record)[sym]; if (symValue && typeof symValue === "object" && "jsonSchema" in symValue) { return true; } } return false; } /** * Extract JSON schema from an AI SDK jsonSchema wrapper. */ function extractJsonSchema(wrapper: unknown): JSONSchema7 | null { if (wrapper === null || typeof wrapper !== "object") return null; if ("jsonSchema" in wrapper) { return (wrapper as { jsonSchema: JSONSchema7 }).jsonSchema; } const symbols = Object.getOwnPropertySymbols(wrapper); for (const sym of symbols) { const symValue = (wrapper as Record)[sym]; if (symValue && typeof symValue === "object" && "jsonSchema" in symValue) { return (symValue as { jsonSchema: JSONSchema7 }).jsonSchema; } } return null; } /** * Extract field descriptions from a schema. * Works with Zod schemas (via .shape) and jsonSchema wrappers (via .properties). */ function extractDescriptions(schema: unknown): Record { const descriptions: Record = {}; const shape = (schema as { shape?: Record }).shape; if (shape && typeof shape === "object") { for (const [fieldName, fieldSchema] of Object.entries(shape)) { let s = fieldSchema as { description?: string; unwrap?: () => unknown }; while (!s.description && typeof s.unwrap === "function") { s = s.unwrap() as typeof s; } if (s.description) { descriptions[fieldName] = s.description; } } return descriptions; } if (isJsonSchemaWrapper(schema)) { const jsonSchema = extractJsonSchema(schema); if (jsonSchema?.properties) { for (const [fieldName, propSchema] of Object.entries( jsonSchema.properties )) { if ( propSchema && typeof propSchema === "object" && propSchema.description ) { descriptions[fieldName] = propSchema.description; } } } } return descriptions; } /** * Extract field descriptions from a schema and format as @param lines. */ function extractParamDescriptions(schema: unknown): string[] { const descriptions = extractDescriptions(schema); return Object.entries(descriptions).map( ([fieldName, desc]) => `@param input.${fieldName} - ${desc}` ); } /** * Safely convert a schema to TypeScript type string. * Handles Zod schemas (v3/v4) and AI SDK jsonSchema wrappers. */ function safeSchemaToTs(schema: unknown, typeName: string): string { try { if (isZodSchema(schema) || isStandardSchema(schema)) { const wrapped = asSchema(schema as ZodType); const jsonSchema = wrapped.jsonSchema as JSONSchema7; if (jsonSchema) { const ctx: ConversionContext = { root: jsonSchema, depth: 0, seen: new Set(), maxDepth: 20 }; const typeBody = jsonSchemaToTypeString(jsonSchema, "", ctx); return `type ${typeName} = ${typeBody}`; } } if (isJsonSchemaWrapper(schema)) { const jsonSchema = extractJsonSchema(schema); if (jsonSchema) { const ctx: ConversionContext = { root: jsonSchema, depth: 0, seen: new Set(), maxDepth: 20 }; const typeBody = jsonSchemaToTypeString(jsonSchema, "", ctx); return `type ${typeName} = ${typeBody}`; } } return `type ${typeName} = unknown`; } catch { return `type ${typeName} = unknown`; } } /** * Generate TypeScript type definitions from tool descriptors or an AI SDK ToolSet. * These types can be included in tool descriptions to help LLMs write correct code. * * Requires the `ai` peer dependency. For a version that works with plain JSON Schema * objects (no AI SDK), use `generateTypesFromJsonSchema` from the main entry point. */ export function generateTypes( tools: ToolDescriptors | ToolSet, namespace = "codemode" ): string { let availableTools = ""; let availableTypes = ""; for (const [toolName, tool] of Object.entries(tools)) { const safeName = sanitizeToolName(toolName); const typeName = toPascalCase(safeName); try { const inputSchema = "inputSchema" in tool ? tool.inputSchema : tool.parameters; const outputSchema = "outputSchema" in tool ? tool.outputSchema : undefined; const description = tool.description; const inputType = safeSchemaToTs(inputSchema, `${typeName}Input`); const outputType = outputSchema ? safeSchemaToTs(outputSchema, `${typeName}Output`) : `type ${typeName}Output = unknown`; availableTypes += `\n${inputType.trim()}`; availableTypes += `\n${outputType.trim()}`; const paramDescs = (() => { try { return inputSchema ? extractParamDescriptions(inputSchema) : []; } catch { return []; } })(); const jsdocLines: string[] = []; if (description?.trim()) { jsdocLines.push(escapeJsDoc(description.trim().replace(/\r?\n/g, " "))); } else { jsdocLines.push(escapeJsDoc(toolName)); } for (const pd of paramDescs) { jsdocLines.push(escapeJsDoc(pd.replace(/\r?\n/g, " "))); } const jsdocBody = jsdocLines.map((l) => `\t * ${l}`).join("\n"); availableTools += `\n\t/**\n${jsdocBody}\n\t */`; availableTools += `\n\t${safeName}: (input: ${typeName}Input) => Promise<${typeName}Output>;`; availableTools += "\n"; } catch { availableTypes += `\ntype ${typeName}Input = unknown`; availableTypes += `\ntype ${typeName}Output = unknown`; availableTools += `\n\t/**\n\t * ${escapeJsDoc(toolName)}\n\t */`; availableTools += `\n\t${safeName}: (input: ${typeName}Input) => Promise<${typeName}Output>;`; availableTools += "\n"; } } availableTools = `\ndeclare const ${namespace}: {${availableTools}}`; return ` ${availableTypes} ${availableTools} `.trim(); }