Better client

This commit is contained in:
Jack Amadeo
2026-02-18 15:44:32 -05:00
parent e7742a132b
commit 94d07012e1
6 changed files with 291 additions and 2 deletions
+140
View File
@@ -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<string>();
const zodImports = new Set<string>();
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<string, unknown>";
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<string, unknown>): Promise<Record<string, unknown>>;
}
${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);
}
+14
View File
@@ -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": {
+4
View File
@@ -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",
+129
View File
@@ -0,0 +1,129 @@
// This file is auto-generated — do not edit manually.
export interface ExtMethodProvider {
extMethod(
method: string,
params: Record<string, unknown>,
): Promise<Record<string, unknown>>;
}
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<void> {
await this.conn.extMethod("_goose/extensions/add", params);
}
async extensionsRemove(params: RemoveExtensionRequest): Promise<void> {
await this.conn.extMethod("_goose/extensions/remove", params);
}
async tools(params: GetToolsRequest): Promise<GetToolsResponse> {
const raw = await this.conn.extMethod("_goose/tools", params);
return zGetToolsResponse.parse(raw) as GetToolsResponse;
}
async resourceRead(
params: ReadResourceRequest,
): Promise<ReadResourceResponse> {
const raw = await this.conn.extMethod("_goose/resource/read", params);
return zReadResourceResponse.parse(raw) as ReadResourceResponse;
}
async workingDirUpdate(params: UpdateWorkingDirRequest): Promise<void> {
await this.conn.extMethod("_goose/working_dir/update", params);
}
async sessionList(): Promise<ListSessionsResponse> {
const raw = await this.conn.extMethod("_goose/session/list", {});
return zListSessionsResponse.parse(raw) as ListSessionsResponse;
}
async sessionGet(params: GetSessionRequest): Promise<GetSessionResponse> {
const raw = await this.conn.extMethod("_goose/session/get", params);
return zGetSessionResponse.parse(raw) as GetSessionResponse;
}
async sessionDelete(params: DeleteSessionRequest): Promise<void> {
await this.conn.extMethod("_goose/session/delete", params);
}
async sessionExport(
params: ExportSessionRequest,
): Promise<ExportSessionResponse> {
const raw = await this.conn.extMethod("_goose/session/export", params);
return zExportSessionResponse.parse(raw) as ExportSessionResponse;
}
async sessionImport(
params: ImportSessionRequest,
): Promise<ImportSessionResponse> {
const raw = await this.conn.extMethod("_goose/session/import", params);
return zImportSessionResponse.parse(raw) as ImportSessionResponse;
}
async configExtensions(): Promise<GetExtensionsResponse> {
const raw = await this.conn.extMethod("_goose/config/extensions", {});
return zGetExtensionsResponse.parse(raw) as GetExtensionsResponse;
}
async toolCall(): Promise<Record<string, unknown>> {
return await this.conn.extMethod("_goose/tool/call", {});
}
async providerUpdate(): Promise<Record<string, unknown>> {
return await this.conn.extMethod("_goose/provider/update", {});
}
async containerSet(): Promise<Record<string, unknown>> {
return await this.conn.extMethod("_goose/container/set", {});
}
async appsList(): Promise<Record<string, unknown>> {
return await this.conn.extMethod("_goose/apps/list", {});
}
async appsExport(): Promise<Record<string, unknown>> {
return await this.conn.extMethod("_goose/apps/export", {});
}
async appsImport(): Promise<Record<string, unknown>> {
return await this.conn.extMethod("_goose/apps/import", {});
}
async configProviders(): Promise<Record<string, unknown>> {
return await this.conn.extMethod("_goose/config/providers", {});
}
}
+1 -1
View File
@@ -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 = [
{
+3 -1
View File
@@ -1 +1,3 @@
export * from "./generated";
export * from "./generated/index.js";
export * from "./generated/zod.gen.js";
export { GooseClient } from "./generated/client.gen.js";