import { defaultClientConditions, type Plugin, type ResolvedConfig, } from "vite"; import { createHash } from "node:crypto"; import path from "node:path"; import fs from "node:fs"; import * as recast from "recast"; import typescriptParser from "recast/parsers/typescript.js"; import { ApolloClient, ApolloLink, type DocumentNode } from "@apollo/client"; import { InMemoryCache } from "@apollo/client"; import { equal } from "@wry/equality"; import { gqlPluckFromCodeStringSync } from "@graphql-tools/graphql-tag-pluck"; import { glob } from "glob"; import { print } from "@apollo/client/utilities"; import { removeDirectivesFromDocument } from "@apollo/client/utilities/internal"; import { of } from "rxjs"; import { Kind, OperationTypeNode, parse, visit } from "graphql"; import { getArgumentValue, getDirectiveArgument, getTypeName, maybeGetArgumentValue, } from "./utilities/graphql.js"; import type { ApplicationManifest, ManifestOperation, } from "../types/application-manifest"; import { invariant } from "../utilities/invariant.js"; import { explorer } from "./utilities/config.js"; import type { ApolloClientAiAppsConfig } from "../config/index.js"; import { ApolloClientAiAppsConfigSchema } from "../config/schema.js"; import { z } from "zod"; import { createFragmentRegistry } from "@apollo/client/cache"; import { buildImportStatement, buildPropertySignature, buildKeywordLiteral, printRecast, type TSInterfaceBody, } from "./utilities/recast.js"; import type { TypeScriptDocumentsPluginConfig } from "@graphql-codegen/typescript-operations"; const b = recast.types.builders; export declare namespace apolloClientAiApps { export type Target = ApolloClientAiAppsConfig.AppTarget; export interface Options { targets: Target[]; devTarget?: Target | undefined; appsOutDir: string; schema?: string | undefined; } } const root = process.cwd(); const VALID_TARGETS: apolloClientAiApps.Target[] = ["openai", "mcp"]; function isValidTarget(target: unknown): target is apolloClientAiApps.Target { return VALID_TARGETS.includes(target as apolloClientAiApps.Target); } function buildExtensions(target: apolloClientAiApps.Target) { return [".mjs", ".js", ".mts", ".ts", ".jsx", ".tsx", ".json"].flatMap( (ext) => [`.${target}${ext}`, ext] ); } export function devTarget(target: string | undefined) { invariant( target === undefined || isValidTarget(target), `devTarget '${target}' is not a valid dev target. Must be one of ${VALID_TARGETS.join(", ")}.` ); return target; } interface FileCache { hash: string; sources: DocumentNode[]; } function md5(contents: string) { return createHash("md5").update(contents).digest("hex"); } /** * Builds the AST for the comment used at the top of a generated file from this * plugin. * * @remarks * This comment informs users that generated files from this plugin should not * be edited since it is autogenerated by this plugin. */ function buildHeaderComment() { return b.commentLine( " This file is auto-generated by @apollo/client-ai-apps. Do not edit manually.", true, false ); } /** * Builds the `@apollo/client-ai-apps` ambient module declaration for the * `register.d.ts` file. * * @param registerInterfaceBody - The interface body for the `Register` * interface * * @example * ```ts * buildAmbientModuleDeclaration(interfaceBody); * // => * // declare namespace "@apollo/client-ai-apps" { * // interface Register { * // // ...interfaceBody * // } * // } * ``` */ function buildAmbientModuleDeclaration(registerInterfaceBody: TSInterfaceBody) { const interfaceDeclaration = b.tsInterfaceDeclaration( b.identifier("Register"), b.tsInterfaceBody(registerInterfaceBody) ); const moduleDeclaration = b.tsModuleDeclaration( b.stringLiteral("@apollo/client-ai-apps"), b.tsModuleBlock([interfaceDeclaration]) ); moduleDeclaration.declare = true; return moduleDeclaration; } /** * Gets the variables type name for an operation. GraphQL Codegen creates * variables types with the combination of operationName + operationType + * "Variables" * * @example * ```ts * getVariablesTypeName({ name: "GetProduct", type: "query" }); * // => "GetProductQueryVariables" * ```` */ function getVariablesTypeName(operation: ManifestOperation) { const { name, type } = operation; return `${name}${type.charAt(0).toUpperCase()}${type.slice(1)}Variables`; } /** * Returns the code string written to the `.apollo-client-ai-apps/types/register.d.ts` files * for a given set of operations. */ function getRegisteredTypeContents({ operations, schema, flagSchemaBuildError = false, }: { operations: ManifestOperation[]; schema: string | undefined; /** * Used during dev to avoid failing the build and instead output the error * to the console. When providing an error, the error message is generated as * a literal type value in the `register.d.ts` file so that users can see the * error message when hovering over the type. */ flagSchemaBuildError?: false | Error; }) { if (flagSchemaBuildError) { const message = `[@apollo/client-ai-apps/vite]: There was an error building generated types. See the vite build output for more details.\n\n${flagSchemaBuildError.message}`; const importBaseStatement = buildImportStatement( [], "@apollo/client-ai-apps" ); importBaseStatement.comments = [buildHeaderComment()]; const typeAnnotation = b.tsLiteralType(b.stringLiteral(message)); return printRecast( b.program([ importBaseStatement, buildAmbientModuleDeclaration([ buildPropertySignature("toolName", typeAnnotation), buildPropertySignature( "toolInputs", b.tsTypeLiteral([buildPropertySignature(message, typeAnnotation)]) ), ]), ]) ); } const toolNames = operations.flatMap((op) => op.tools.map((t) => t.name)); if (toolNames.length === 0) { const emptyExport = b.exportNamedDeclaration(null); emptyExport.comments = [buildHeaderComment()]; return printRecast(b.program([emptyExport])); } const importBaseStatement = buildImportStatement( [], "@apollo/client-ai-apps" ); importBaseStatement.comments = [buildHeaderComment()]; const toolNameProp = buildPropertySignature( "toolName", b.tsUnionType(toolNames.map((n) => b.tsLiteralType(b.stringLiteral(n)))) ); if (!schema) { return printRecast( b.program([ importBaseStatement, buildAmbientModuleDeclaration([toolNameProp]), ]) ); } const importedVariableTypes = new Set(); const toolInputsValue: recast.types.namedTypes.TSPropertySignature[] = []; for (const operation of operations) { const variablesTypeName = getVariablesTypeName(operation); if (operation.tools.length) { importedVariableTypes.add(variablesTypeName); } for (const tool of operation.tools) { const variablesTypeRef = b.tsTypeReference( b.identifier(variablesTypeName) ); let typeExpression: | recast.types.namedTypes.TSTypeReference | recast.types.namedTypes.TSIntersectionType = variablesTypeRef; if (tool.extraInputs?.length) { const extraInputsType = tool.extraInputs.map((ei) => { return buildPropertySignature( ei.name, buildKeywordLiteral(ei.type), true ); }); typeExpression = b.tsIntersectionType([ variablesTypeRef, b.tsTypeLiteral(extraInputsType), ]); } toolInputsValue.push(buildPropertySignature(tool.name, typeExpression)); } } return printRecast( b.program([ importBaseStatement, buildImportStatement( Array.from(importedVariableTypes), "./operation-types.js", "type" ), buildAmbientModuleDeclaration([ toolNameProp, buildPropertySignature("toolInputs", b.tsTypeLiteral(toolInputsValue)), ]), ]) ); } /** * Generates and returns the code string written to the * `.apollo-client-ai-apps/types/operation-types.d.ts` file. Uses GraphQL * Codegen to introspect the given schema and extract variable types. */ async function generateOperationTypes( schema: string, documents: string[] ): Promise { const { generate } = await import("@graphql-codegen/cli"); if (documents.length === 0) { return `// Auto-generated by @apollo/client-ai-apps. Do not edit manually.\nexport {};\n`; } const output = await generate( { schema, documents, generates: { "operation-types.d.ts": { plugins: ["typescript-operations"], config: { nonOptionalTypename: true, skipTypeNameForRoot: true, namingConvention: "keep", } satisfies TypeScriptDocumentsPluginConfig, }, }, silent: true, } as Parameters[0], false ); const content = (output as { filename: string; content: string }[])[0] .content; return `// Auto-generated by @apollo/client-ai-apps. Do not edit manually.\n${content}`; } /** * Gets the name of an exported type (e.g. `export type x`) from a TypeScript * AST statement. */ function getExportedTypeAliasName( statement: recast.types.namedTypes.Statement ): string | undefined { if (statement.type !== "ExportNamedDeclaration") return; const declaration = ( statement as recast.types.namedTypes.ExportNamedDeclaration ).declaration; if (!declaration || declaration.type !== "TSTypeAliasDeclaration") return; const alias = declaration as recast.types.namedTypes.TSTypeAliasDeclaration; return typeof alias.id.name === "string" ? alias.id.name : undefined; } /** * Removes all non-`*Variables` types from the `operation-types.d.ts` file. * The `typescript` and `typescript-operations` codegen plugins add full schema * types and operation types. This function ensures those unused types are * removed. */ function filterOperationTypes( content: string, rootTypeNames: Set ): string { const ast = recast.parse(content, { parser: typescriptParser }); const statements: recast.types.namedTypes.Statement[] = ast.program.body; const typeMap = new Map< string, recast.types.namedTypes.TSTypeAliasDeclaration >(); for (const statement of statements) { const name = getExportedTypeAliasName(statement); if (name) { typeMap.set( name, (statement as recast.types.namedTypes.ExportNamedDeclaration) .declaration as recast.types.namedTypes.TSTypeAliasDeclaration ); } } const reachable = new Set(rootTypeNames); const queue = Array.from(rootTypeNames); while (queue.length > 0) { const name = queue.shift()!; const node = typeMap.get(name); if (!node) continue; recast.visit(node, { visitTSTypeReference(path) { const typeName = path.value.typeName?.name as string | undefined; if (typeName && !reachable.has(typeName)) { reachable.add(typeName); queue.push(typeName); } this.traverse(path); }, }); } ast.program.body = statements.filter((statement) => { const name = getExportedTypeAliasName(statement); return name === undefined || reachable.has(name); }); return printRecast(ast); } export function apolloClientAiApps( options: apolloClientAiApps.Options ): Plugin { const targets = Array.from(new Set(options.targets)); const { devTarget = targets.length === 1 ? targets[0] : undefined, appsOutDir, schema, } = options; let config!: ResolvedConfig; const fragments = createFragmentRegistry(); invariant( Array.isArray(targets) && targets.length > 0, "The `targets` option must be a non-empty array" ); invariant( targets.every(isValidTarget), `All targets must be one of: ${VALID_TARGETS.join(", ")}` ); invariant( path.basename(path.normalize(appsOutDir)) === "apps", "`appsOutDir` must end with `apps` as the final path segment (e.g. `path/to/apps`)." ); const client = new ApolloClient({ cache: new InMemoryCache({ fragments }), link: processQueryLink, }); let sources: DocumentNode[] = []; function recomputeSources(cache: Map) { sources = Array.from(cache.values()).flatMap((entry) => entry.sources); } async function getManifestOperations() { if (sources === getManifestOperations.cache.sources) { return getManifestOperations.cache.manifestOperations; } const manifestOperations = []; for (const source of sources) { const operationDef = source.definitions.find( (d) => d.kind === Kind.OPERATION_DEFINITION ); if (!operationDef) continue; switch (operationDef.operation) { case OperationTypeNode.QUERY: { const result = await client.query({ query: source, fetchPolicy: "no-cache", }); const data = result.data!; if (data.tools.length > 0 || data.prefetch) { manifestOperations.push(data); } break; } case OperationTypeNode.MUTATION: { const result = await client.mutate({ mutation: source, fetchPolicy: "no-cache", }); const data = result.data!; if (data.tools.length > 0 || data.prefetch) { manifestOperations.push(data); } break; } default: throw new Error( `Found unsupported operation type '${operationDef.operation}'. Only queries and mutations are supported.` ); } } getManifestOperations.cache = { sources, manifestOperations }; return manifestOperations; } getManifestOperations.cache = { sources, manifestOperations: [] as ManifestOperation[], }; async function processFile(file: string) { const code = fs.readFileSync(file, "utf-8"); if (!code.includes("gql")) return; const fileHash = md5(code); if (processFile.cache.get(file)?.hash === fileHash) return; const sources = gqlPluckFromCodeStringSync(file, code, { modules: [ { name: "graphql-tag", identifier: "gql" }, { name: "@apollo/client", identifier: "gql" }, ], }).map((source) => parse(source.body)); const previousSources = processFile.cache.get(file)?.sources; if (previousSources && equal(sources, previousSources)) { processFile.cache.set(file, { hash: fileHash, sources: previousSources }); return; } fragments.register(...sources); processFile.cache.set(file, { hash: fileHash, sources }); recomputeSources(processFile.cache); } processFile.cache = new Map(); async function generateManifest() { const appsConfig = await getAppsConfig(); const operations = await getManifestOperations(); invariant( operations.filter((o) => o.prefetch).length <= 1, "Found multiple operations marked as `@prefetch`. You can only mark 1 operation with `@prefetch`." ); function getBuildResourceForTarget(target: apolloClientAiApps.Target) { const entryPoint = getResourceFromConfig(appsConfig, config.mode, target); if (entryPoint) { return entryPoint; } if (config.mode === "production") { return `${target}/index.html`; } throw new Error( `No entry point found for mode "${config.mode}". Entry points other than "development" and "production" must be defined in package.json file.` ); } let resource: ApplicationManifest["resource"]; if (config.command === "serve") { // Dev mode: resource is a string (dev server URL) resource = getResourceFromConfig(appsConfig, config.mode, devTarget!) ?? `http${config.server.https ? "s" : ""}://${config.server.host ?? "localhost"}:${config.server.port}`; } else { resource = Object.fromEntries( targets.map((target) => [target, getBuildResourceForTarget(target)]) ) as { mcp?: string; openai?: string }; } const packageJson = readPackageJson(); const appName = appsConfig.name ?? packageJson.name; invariant( appName, "Error generating application manifest. Could not determine app name. Set `name` in your apollo-client-ai-apps config or `package.json`." ); const manifest: ApplicationManifest = { format: "apollo-ai-app-manifest", version: "1", appVersion: appsConfig.version ?? packageJson.version, name: appsConfig.name ?? packageJson.name, description: appsConfig.description ?? packageJson.description, hash: createHash("sha256").update(Date.now().toString()).digest("hex"), operations, resource, csp: { baseUriDomains: appsConfig.csp?.baseUriDomains ?? [], connectDomains: appsConfig.csp?.connectDomains ?? [], frameDomains: appsConfig.csp?.frameDomains ?? [], redirectDomains: appsConfig.csp?.redirectDomains ?? [], resourceDomains: appsConfig.csp?.resourceDomains ?? [], }, }; if (isNonEmptyObject(appsConfig.widgetSettings)) { manifest.widgetSettings = appsConfig.widgetSettings; } if (isNonEmptyObject(appsConfig.labels)) { manifest.labels = appsConfig.labels; } const manifestContents = JSON.stringify(manifest); // Always write to build directory so the MCP server picks it up writeFileSync( path.resolve(root, appsOutDir, appName, ".application-manifest.json"), manifestContents ); // Always write to the dev location so that the app can bundle the manifest content writeFileSync(".application-manifest.json", manifestContents); const manifestTypesFilepath = ".application-manifest.d.json.ts"; if (!fs.existsSync(manifestTypesFilepath)) { const manifestImport = b.importDeclaration( [b.importSpecifier(b.identifier("ApplicationManifest"))], b.stringLiteral("@apollo/client-ai-apps"), "type" ); const manifestId = b.identifier("manifest"); manifestId.typeAnnotation = b.tsTypeAnnotation( b.tsTypeReference(b.identifier("ApplicationManifest"), null) ); const manifestDeclaration = b.variableDeclaration("const", [ b.variableDeclarator(manifestId, null), ]) as recast.types.namedTypes.VariableDeclaration & { declare: boolean }; manifestDeclaration.declare = true; const exportDefault = b.exportDefaultDeclaration( b.identifier("manifest") ); const content = printRecast( b.program([manifestImport, manifestDeclaration, exportDefault]) ); writeFileSync(manifestTypesFilepath, content); } } async function generateTypesFiles() { let flagSchemaBuildError: false | Error = false; const operations = await getManifestOperations(); if (operations === generateTypesFiles.cache) { return; } generateTypesFiles.cache = operations; if (schema) { try { const opTypesContent = await generateOperationTypes( schema, operations.map((op) => op.body) ); const rootTypeNames = new Set( operations.flatMap((op) => op.tools.length > 0 ? [getVariablesTypeName(op)] : [] ) ); writeFileSync( path.resolve( root, ".apollo-client-ai-apps/types/operation-types.d.ts" ), filterOperationTypes(opTypesContent, rootTypeNames), { cache: true } ); } catch (e) { if (config.command === "build") { throw e; } flagSchemaBuildError = e as Error; console.error("[@apollo/client-ai-apps/vite]:", e); } } const typesFileContents = getRegisteredTypeContents({ operations, schema, flagSchemaBuildError, }); writeFileSync( path.resolve(root, ".apollo-client-ai-apps/types/register.d.ts"), typesFileContents, { cache: true } ); } generateTypesFiles.cache = [] as ManifestOperation[] | undefined; return { name: "@apollo/client-ai-apps/vite", async buildStart() { // Scan all files on startup const files = await glob("./src/**/*.{ts,tsx,js,jsx}", { fs }); for (const file of files) { const fullPath = path.resolve(root, file); await processFile(fullPath); } await Promise.all([generateManifest(), generateTypesFiles()]); }, configResolved(resolvedConfig) { config = resolvedConfig; }, async configEnvironment(name) { if (!targets.includes(name as any)) return; const appsConfig = await getAppsConfig(); const appName = appsConfig.name ?? readPackageJson().name; invariant( appName, "Could not determine app name. Set `name` in your apollo-client-ai-apps config or `package.json`." ); return { build: { outDir: path.join(appsOutDir, appName, name), }, }; }, configureServer(server) { server.watcher.on("change", async (file) => { if (file.endsWith("package.json")) { readPackageJson.resetCache(); await generateManifest(); } else if (file.match(/\.?apollo-client-ai-apps\.config\.\w+$/)) { explorer.clearCaches(); await generateManifest(); } else if (file.match(/\.(jsx?|tsx?)$/)) { await processFile(file); await Promise.all([generateManifest(), generateTypesFiles()]); } }); }, config(userConfig, { command }) { if (userConfig.build?.outDir) { console.warn( "[@apollo/client-ai-apps/vite] `build.outDir` is set in your Vite config but will be " + "ignored. Use `appsOutDir` in the plugin options to control the output location." ); } if (command === "serve") { invariant( isValidTarget(devTarget) || targets.length === 1, "`devTarget` must be set for development when using multiple targets." ); const target = devTarget ?? targets[0]; return { resolve: { extensions: buildExtensions(target), conditions: [target, ...defaultClientConditions], }, }; } return { environments: Object.fromEntries( targets.map((target) => [ target, { consumer: "client", webCompatible: true, resolve: { extensions: buildExtensions(target), conditions: [target, ...defaultClientConditions], }, }, ]) ), builder: { buildApp: async (builder) => { await Promise.all( targets.map((target) => builder.build(builder.environments[target]) ) ); }, }, }; }, transformIndexHtml(html, ctx) { if (!ctx.server) return html; let baseUrl = ( ctx.server.config?.server?.origin ?? ctx.server.resolvedUrls?.local[0] ?? "" ).replace(/\/$/, ""); baseUrl = baseUrl.replace(/\/$/, ""); return ( html // import "/@vite/..." or "/@react-refresh" .replace(/(from\s+["'])\/([^"']+)/g, `$1${baseUrl}/$2`) // src="/src/..." .replace(/(src=["'])\/([^"']+)/gi, `$1${baseUrl}/$2`) ); }, } satisfies Plugin; } const processQueryLink = new ApolloLink((operation) => { const body = print( removeOperationDescription( removeManifestDirectives(sortTopLevelDefinitions(operation.query)) ) ); const name = operation.operationName; const definition = operation.query.definitions.find( (d) => d.kind === "OperationDefinition" ); // Use `operation.query` so that the error reflects the end-user defined // document, not our sorted one invariant( definition, `Document does not contain an operation:\n${print(operation.query)}` ); const { directives, operation: type } = definition; const variables = definition.variableDefinitions?.reduce( (obj, varDef) => ({ ...obj, [varDef.variable.name.value]: getTypeName(varDef.type), }), {} ); const prefetch = directives?.some((d) => d.name.value === "prefetch"); const id = createHash("sha256").update(body).digest("hex"); // TODO: For now, you can only have 1 operation marked as prefetch. In the future, we'll likely support more than 1, and the "prefetchId" will be defined on the `@prefetch` itself as an argument const prefetchID = prefetch ? "__anonymous" : undefined; const toolDirectives = directives?.filter((d) => d.name.value === "tool") ?? []; const tools = toolDirectives.map((directive) => { const nameArg = getDirectiveArgument("name", directive); const descriptionArg = getDirectiveArgument("description", directive); let name: string; if (nameArg) { name = getArgumentValue(nameArg, Kind.STRING); } else { invariant( toolDirectives.length === 1, `Operations with multiple @tool directives must provide a 'name' argument on each @tool` ); invariant( definition.name?.value, `Anonymous operations cannot use @tool without providing a 'name' argument` ); name = definition.name.value; } let description: string; if (descriptionArg) { description = getArgumentValue(descriptionArg, Kind.STRING); } else { invariant( toolDirectives.length === 1, `Operations with multiple @tool directives must provide a 'description' argument on each @tool` ); invariant( definition.description?.value, `Operations using @tool without a 'description' argument must have a description on the operation definition` ); description = definition.description.value; } const result = ToolDirectiveSchema.safeParse({ name, description, extraInputs: maybeGetArgumentValue( getDirectiveArgument("extraInputs", directive), Kind.LIST ), extraOutputs: maybeGetArgumentValue( getDirectiveArgument("extraOutputs", directive), Kind.OBJECT ), labels: maybeGetArgumentValue( getDirectiveArgument("labels", directive), Kind.OBJECT ), }); if (result.error) { throw z.prettifyError(result.error); } return result.data; }); // TODO: Make this object satisfy the `ManifestOperation` type. Currently // it errors because we need more validation on a few of these fields return of({ data: { id, name, type, body, variables, prefetch, prefetchID, tools }, }); }); function removeOperationDescription(doc: DocumentNode): DocumentNode { return visit(doc, { OperationDefinition(node) { return { ...node, description: undefined }; }, }); } function removeManifestDirectives(doc: DocumentNode) { return removeDirectivesFromDocument( [{ name: "prefetch" }, { name: "tool" }], doc )!; } // Sort the definitions in this document so that operations come before fragments, // and so that each kind of definition is sorted by name. export function sortTopLevelDefinitions(query: DocumentNode): DocumentNode { const definitions = [...query.definitions]; // We want to avoid unnecessary dependencies, so write out a comparison // function instead of using _.orderBy. definitions.sort((a, b) => { // This is a reverse sort by kind, so that OperationDefinition precedes FragmentDefinition. if (a.kind > b.kind) { return -1; } if (a.kind < b.kind) { return 1; } // Extract the name from each definition. Jump through some hoops because // non-executable definitions don't have to have names (even though any // DocumentNode actually passed here should only have executable // definitions). const aName = a.kind === "OperationDefinition" || a.kind === "FragmentDefinition" ? (a.name?.value ?? "") : ""; const bName = b.kind === "OperationDefinition" || b.kind === "FragmentDefinition" ? (b.name?.value ?? "") : ""; // Sort by name ascending. if (aName < bName) { return -1; } if (aName > bName) { return 1; } // Assuming that the document is "valid", no operation or fragment name can appear // more than once, so we don't need to differentiate further to have a deterministic // sort. return 0; }); return { ...query, definitions, }; } function isNonEmptyObject( obj: T | null | undefined ): obj is T { return !!obj && Object.keys(obj).length > 0; } async function getAppsConfig() { const result = await explorer.search(); const config = (result?.config ?? {}) as Partial; const parsed = ApolloClientAiAppsConfigSchema.safeParse(config); if (parsed.error) { throw z.prettifyError(parsed.error); } return parsed.data; } function getResourceFromConfig( appsConfig: z.infer, mode: string, target: apolloClientAiApps.Target ) { if (!appsConfig.entry || !appsConfig.entry[mode]) { return; } const config = appsConfig.entry[mode]; return typeof config === "string" ? config : config[target]; } function readPackageJson(): Record { if (readPackageJson.cache) { return readPackageJson.cache; } return (readPackageJson.cache = JSON.parse( fs.readFileSync("package.json", "utf-8") )); } readPackageJson.cache = undefined as Record | undefined; readPackageJson.resetCache = () => { readPackageJson.cache = undefined; }; function writeFileSync( filepath: string, content: string, options: { cache?: boolean } = {} ) { function writeFile() { fs.mkdirSync(path.dirname(filepath), { recursive: true }); fs.writeFileSync(filepath, content, "utf-8"); } if (!options.cache) { return writeFile(); } const hash = md5(content); const cachedHash = writeFileSync.cache.get(filepath); if (hash !== cachedHash || !fs.existsSync(filepath)) { writeFileSync.cache.set(filepath, hash); writeFile(); } } writeFileSync.cache = new Map(); const ToolDirectiveSchema = z.strictObject({ name: z.stringFormat("toolName", (value) => value.indexOf(" ") === -1, { error: (iss) => `Tool with name "${iss.input}" must not contain spaces`, }), description: z.string(), extraInputs: z.optional( z.array( z.strictObject({ name: z.string(), description: z.string(), type: z.literal(["string", "boolean", "number"]), }) ) ), extraOutputs: z.optional(z.record(z.string(), z.unknown())), labels: ApolloClientAiAppsConfigSchema.shape.labels.optional(), });