diff --git a/ui/acp/generate-schema.ts b/ui/acp/generate-schema.ts index 3a5fe238ef..08cd402a4c 100644 --- a/ui/acp/generate-schema.ts +++ b/ui/acp/generate-schema.ts @@ -79,6 +79,9 @@ async function main() { await postProcessTypes(); await postProcessIndex(meta); + // 5. Generate typed client wrapper + await generateClient(meta); + console.log(`\nGenerated Goose extension schema in ${OUTPUT_DIR}`); } @@ -101,6 +104,9 @@ async function postProcessIndex(meta: { methods: unknown[] }) { return ""; }); + // Fix bare relative imports to use .js extensions (required by nodenext consumers) + src = fixRelativeImports(src); + // Append method constants const methodConstants = await prettier.format( ` @@ -112,4 +118,138 @@ export type GooseExtMethod = (typeof GOOSE_EXT_METHODS)[number]; ); await fs.writeFile(indexPath, `${src}\n${methodConstants}`); + + // Also fix imports in zod.gen.ts (it may import from types.gen) + for (const file of ["zod.gen.ts", "types.gen.ts"]) { + const filePath = resolve(OUTPUT_DIR, file); + try { + const content = await fs.readFile(filePath, "utf8"); + const fixed = fixRelativeImports(content); + if (fixed !== content) { + await fs.writeFile(filePath, fixed); + } + } catch { + // File may not exist + } + } +} + +function fixRelativeImports(src: string): string { + return src.replace( + /from\s+['"](\.[^'"]+)['"]/g, + (_match, importPath: string) => { + if (importPath.endsWith(".js") || importPath.endsWith(".json")) { + return `from '${importPath}'`; + } + return `from '${importPath}.js'`; + }, + ); +} + +interface MethodMeta { + method: string; + requestType: string | null; + responseType: string | null; +} + +/** + * Convert a method path like "session/list" or "working_dir/update" to camelCase "sessionList", "workingDirUpdate". + */ +function methodToCamelCase(method: string): string { + return method + .split(/[/_]/) + .map((part, i) => + i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1), + ) + .join(""); +} + +/** + * Generate a typed GooseClient class that wraps ClientSideConnection.extMethod() + * with proper TypeScript types and Zod runtime validation. + */ +async function generateClient(meta: { methods: MethodMeta[] }) { + const typeImports = new Set(); + const zodImports = new Set(); + + const methodDefs: string[] = []; + + for (const m of meta.methods) { + const fnName = methodToCamelCase(m.method); + const fullMethod = `_goose/${m.method}`; + + // Build param type and arg + let paramType = ""; + let paramArg = ""; + let callParams = "{}"; + if (m.requestType) { + typeImports.add(m.requestType); + paramType = m.requestType; + paramArg = `params: ${paramType}`; + callParams = "params"; + } + + // Build return type and validation + let returnType: string; + let bodyLines: string[]; + + if (m.responseType && m.responseType !== "EmptyResponse") { + typeImports.add(m.responseType); + const zodName = `z${m.responseType}`; + zodImports.add(zodName); + returnType = m.responseType; + bodyLines = [ + `const raw = await this.conn.extMethod("${fullMethod}", ${callParams});`, + `return ${zodName}.parse(raw) as ${returnType};`, + ]; + } else if (m.responseType === "EmptyResponse") { + returnType = "void"; + bodyLines = [ + `await this.conn.extMethod("${fullMethod}", ${callParams});`, + ]; + } else { + // Both request and response are untyped (serde_json::Value) + returnType = "Record"; + bodyLines = [ + `return await this.conn.extMethod("${fullMethod}", ${callParams ? callParams : "{}"});`, + ]; + } + + methodDefs.push(` + async ${fnName}(${paramArg}): Promise<${returnType}> { + ${bodyLines.join("\n ")} + }`); + } + + const typeImportLine = typeImports.size + ? `import type { ${[...typeImports].sort().join(", ")} } from "./types.gen.js";` + : ""; + const zodImportLine = zodImports.size + ? `import { ${[...zodImports].sort().join(", ")} } from "./zod.gen.js";` + : ""; + + let src = `// This file is auto-generated — do not edit manually. + +export interface ExtMethodProvider { + extMethod(method: string, params: Record): Promise>; +} + +${typeImportLine} +${zodImportLine} + +/** + * Typed client for Goose custom extension methods. + * Wraps an ExtMethodProvider (e.g. ClientSideConnection) with proper types and Zod validation. + */ +export class GooseClient { + constructor(private conn: ExtMethodProvider) {} +${methodDefs.join("\n")} +} +`; + + src = await prettier.format(src, { parser: "typescript" }); + src = fixRelativeImports(src); + + const clientPath = resolve(OUTPUT_DIR, "client.gen.ts"); + await fs.writeFile(clientPath, src); } diff --git a/ui/acp/package-lock.json b/ui/acp/package-lock.json index 6bcc518309..3eb6dc181f 100644 --- a/ui/acp/package-lock.json +++ b/ui/acp/package-lock.json @@ -11,10 +11,24 @@ "zod": "^3.25.76" }, "devDependencies": { + "@agentclientprotocol/sdk": "^0.14.1", "@hey-api/openapi-ts": "^0.92.3", "prettier": "^3.8.1", "tsx": "^4.21.0", "typescript": "~5.9.3" + }, + "peerDependencies": { + "@agentclientprotocol/sdk": "*" + } + }, + "node_modules/@agentclientprotocol/sdk": { + "version": "0.14.1", + "resolved": "https://global.block-artifacts.com/artifactory/api/npm/square-npm/@agentclientprotocol/sdk/-/sdk-0.14.1.tgz", + "integrity": "sha512-b6r3PS3Nly+Wyw9U+0nOr47bV8tfS476EgyEMhoKvJCZLbgqoDFN7DJwkxL88RR0aiOqOYV1ZnESHqb+RmdH8w==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" } }, "node_modules/@esbuild/aix-ppc64": { diff --git a/ui/acp/package.json b/ui/acp/package.json index e1f1f2de98..fc258fd648 100644 --- a/ui/acp/package.json +++ b/ui/acp/package.json @@ -14,7 +14,11 @@ "dependencies": { "zod": "^3.25.76" }, + "peerDependencies": { + "@agentclientprotocol/sdk": "*" + }, "devDependencies": { + "@agentclientprotocol/sdk": "^0.14.1", "@hey-api/openapi-ts": "^0.92.3", "prettier": "^3.8.1", "tsx": "^4.21.0", diff --git a/ui/acp/src/generated/client.gen.ts b/ui/acp/src/generated/client.gen.ts new file mode 100644 index 0000000000..db0ae5eb21 --- /dev/null +++ b/ui/acp/src/generated/client.gen.ts @@ -0,0 +1,129 @@ +// This file is auto-generated — do not edit manually. + +export interface ExtMethodProvider { + extMethod( + method: string, + params: Record, + ): Promise>; +} + +import type { + AddExtensionRequest, + DeleteSessionRequest, + ExportSessionRequest, + ExportSessionResponse, + GetExtensionsResponse, + GetSessionRequest, + GetSessionResponse, + GetToolsRequest, + GetToolsResponse, + ImportSessionRequest, + ImportSessionResponse, + ListSessionsResponse, + ReadResourceRequest, + ReadResourceResponse, + RemoveExtensionRequest, + UpdateWorkingDirRequest, +} from './types.gen.js'; +import { + zExportSessionResponse, + zGetExtensionsResponse, + zGetSessionResponse, + zGetToolsResponse, + zImportSessionResponse, + zListSessionsResponse, + zReadResourceResponse, +} from './zod.gen.js'; + +/** + * Typed client for Goose custom extension methods. + * Wraps an ExtMethodProvider (e.g. ClientSideConnection) with proper types and Zod validation. + */ +export class GooseClient { + constructor(private conn: ExtMethodProvider) {} + + async extensionsAdd(params: AddExtensionRequest): Promise { + await this.conn.extMethod("_goose/extensions/add", params); + } + + async extensionsRemove(params: RemoveExtensionRequest): Promise { + await this.conn.extMethod("_goose/extensions/remove", params); + } + + async tools(params: GetToolsRequest): Promise { + const raw = await this.conn.extMethod("_goose/tools", params); + return zGetToolsResponse.parse(raw) as GetToolsResponse; + } + + async resourceRead( + params: ReadResourceRequest, + ): Promise { + const raw = await this.conn.extMethod("_goose/resource/read", params); + return zReadResourceResponse.parse(raw) as ReadResourceResponse; + } + + async workingDirUpdate(params: UpdateWorkingDirRequest): Promise { + await this.conn.extMethod("_goose/working_dir/update", params); + } + + async sessionList(): Promise { + const raw = await this.conn.extMethod("_goose/session/list", {}); + return zListSessionsResponse.parse(raw) as ListSessionsResponse; + } + + async sessionGet(params: GetSessionRequest): Promise { + const raw = await this.conn.extMethod("_goose/session/get", params); + return zGetSessionResponse.parse(raw) as GetSessionResponse; + } + + async sessionDelete(params: DeleteSessionRequest): Promise { + await this.conn.extMethod("_goose/session/delete", params); + } + + async sessionExport( + params: ExportSessionRequest, + ): Promise { + const raw = await this.conn.extMethod("_goose/session/export", params); + return zExportSessionResponse.parse(raw) as ExportSessionResponse; + } + + async sessionImport( + params: ImportSessionRequest, + ): Promise { + const raw = await this.conn.extMethod("_goose/session/import", params); + return zImportSessionResponse.parse(raw) as ImportSessionResponse; + } + + async configExtensions(): Promise { + const raw = await this.conn.extMethod("_goose/config/extensions", {}); + return zGetExtensionsResponse.parse(raw) as GetExtensionsResponse; + } + + async toolCall(): Promise> { + return await this.conn.extMethod("_goose/tool/call", {}); + } + + async providerUpdate(): Promise> { + return await this.conn.extMethod("_goose/provider/update", {}); + } + + async containerSet(): Promise> { + return await this.conn.extMethod("_goose/container/set", {}); + } + + async appsList(): Promise> { + return await this.conn.extMethod("_goose/apps/list", {}); + } + + async appsExport(): Promise> { + return await this.conn.extMethod("_goose/apps/export", {}); + } + + async appsImport(): Promise> { + return await this.conn.extMethod("_goose/apps/import", {}); + } + + async configProviders(): Promise> { + return await this.conn.extMethod("_goose/config/providers", {}); + } +} diff --git a/ui/acp/src/generated/index.ts b/ui/acp/src/generated/index.ts index c1b3caff80..b44b568427 100644 --- a/ui/acp/src/generated/index.ts +++ b/ui/acp/src/generated/index.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -export type { AddExtensionRequest, DeleteSessionRequest, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExtRequest, ExtResponse, GetExtensionsResponse, GetSessionRequest, GetSessionResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ListSessionsResponse, ReadResourceRequest, ReadResourceResponse, RemoveExtensionRequest, UpdateWorkingDirRequest } from './types.gen'; +export type { AddExtensionRequest, DeleteSessionRequest, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExtRequest, ExtResponse, GetExtensionsResponse, GetSessionRequest, GetSessionResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ListSessionsResponse, ReadResourceRequest, ReadResourceResponse, RemoveExtensionRequest, UpdateWorkingDirRequest } from './types.gen.js'; export const GOOSE_EXT_METHODS = [ { diff --git a/ui/acp/src/index.ts b/ui/acp/src/index.ts index e84c86c641..04ae78458e 100644 --- a/ui/acp/src/index.ts +++ b/ui/acp/src/index.ts @@ -1 +1,3 @@ -export * from "./generated"; +export * from "./generated/index.js"; +export * from "./generated/zod.gen.js"; +export { GooseClient } from "./generated/client.gen.js";