chore: move acp to sdk (#8556)

This commit is contained in:
Alex Hancock
2026-04-15 13:00:12 -04:00
committed by GitHub
parent 78b5b5209a
commit a7396a7bd0
17 changed files with 14 additions and 14 deletions
+6
View File
@@ -0,0 +1,6 @@
dist/
node_modules/
# Generated schema files (generated from Rust during build)
acp-schema.json
acp-meta.json
+131
View File
@@ -0,0 +1,131 @@
# @aaif/goose-sdk
TypeScript client library for the Goose Agent Client Protocol (ACP).
This package provides:
- TypeScript types and Zod validators for Goose ACP extension methods
- A client for communicating with the Goose ACP server
## Installation
```bash
npm install @aaif/goose-sdk
```
The native `goose` binaries are distributed as optional dependencies
and will be automatically installed for your platform.
## Development
### Prerequisites
- Node.js 18+
- Rust toolchain
- (Optional) Cross-compilation toolchains for building all platforms
### Building
```bash
# Build everything (schema + TypeScript)
npm run build
# Build just the schema (requires Rust)
npm run build:schema
# Build just the TypeScript
npm run build:ts
# Build native binary for current platform
npm run build:native
# Build native binaries for all platforms
npm run build:native:all
```
### Local Development with npm link
To use this package locally in another project (e.g., `@aaif/goose`):
```bash
# In ui/sdk
npm run build
npm link
# In ui/text (or another project)
npm link @aaif/goose-sdk
```
### Schema Generation
The TypeScript types are generated from Rust schemas defined in `crates/goose-acp`.
The build process:
1. Builds the `generate-acp-schema` Rust binary
2. Runs it to generate `acp-schema.json` and `acp-meta.json`
3. Uses `@hey-api/openapi-ts` to generate TypeScript types and Zod validators
4. Generates a typed client in `src/generated/client.gen.ts`
To regenerate schemas after changing Rust types:
```bash
npm run build:schema
```
## Native Binary Packages
Platform-specific npm packages for the `goose` binary are located in
`ui/goose-binary/`:
| Package | Platform |
|---------|----------|
| `@aaif/goose-binary-darwin-arm64` | macOS Apple Silicon |
| `@aaif/goose-binary-darwin-x64` | macOS Intel |
| `@aaif/goose-binary-linux-arm64` | Linux ARM64 |
| `@aaif/goose-binary-linux-x64` | Linux x64 |
| `@aaif/goose-binary-win32-x64` | Windows x64 |
These are published separately from `@aaif/goose-sdk`.
### Building Native Binaries
```bash
# Build for current platform
npm run build:native
# Build for all platforms (requires cross-compilation toolchains)
npm run build:native:all
# Build for specific platform(s)
npx tsx scripts/build-native.ts darwin-arm64 linux-x64
```
## Publishing
Publishing is handled by GitHub Actions. See `.github/workflows/publish-npm.yml`.
For manual publishing:
```bash
# From repository root
./ui/scripts/publish.sh --real
```
This will:
1. Build and publish `@aaif/goose-sdk`
2. Publish all native binary packages
3. Publish `@aaif/goose` (which depends on the above)
## Usage
```typescript
import { GooseClient } from "@aaif/goose-sdk";
const client = new GooseClient({
// ... configuration
});
// Use the client
const result = await client.someMethod({ ... });
```
See the [main documentation](../../README.md) for more details.
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env node
/**
* Generates TypeScript types + Zod validators for Goose custom extension methods.
*
* Usage:
* npm run generate # build Rust schema, then generate TS
*/
import { createClient } from "@hey-api/openapi-ts";
import { execSync } from "child_process";
import * as fs from "fs/promises";
import { dirname, resolve } from "path";
import { fileURLToPath } from "url";
import * as prettier from "prettier";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT = resolve(__dirname, "../..");
const SCHEMA_PATH = resolve(ROOT, "crates/goose-acp/acp-schema.json");
const META_PATH = resolve(ROOT, "crates/goose-acp/acp-meta.json");
const OUTPUT_DIR = resolve(__dirname, "src/generated");
// Export the main function so it can be imported by build-schema.ts
export default async function main() {
const schemaSrc = await fs.readFile(SCHEMA_PATH, "utf8");
const jsonSchema = JSON.parse(
schemaSrc.replaceAll("#/$defs/", "#/components/schemas/"),
);
const metaSrc = await fs.readFile(META_PATH, "utf8");
const meta = JSON.parse(metaSrc);
await createClient({
input: {
openapi: "3.1.0",
info: {
title: "Goose Extensions",
version: "1.0.0",
},
components: {
schemas: jsonSchema.$defs,
},
},
output: {
path: OUTPUT_DIR,
},
plugins: ["zod", "@hey-api/typescript"],
});
await postProcessTypes();
await postProcessIndex(meta);
await generateClient(meta);
console.log(`\nGenerated Goose extension schema in ${OUTPUT_DIR}`);
}
async function postProcessTypes() {
const tsPath = resolve(OUTPUT_DIR, "types.gen.ts");
let src = await fs.readFile(tsPath, "utf8");
src = src.replace(/\nexport type ClientOptions =[\s\S]*?^};\n/m, "\n");
await fs.writeFile(tsPath, src);
}
async function postProcessIndex(meta: { methods: unknown[] }) {
const indexPath = resolve(OUTPUT_DIR, "index.ts");
let src = await fs.readFile(indexPath, "utf8");
src = src.replace(/,?\s*ClientOptions\s*,?/g, (match) => {
if (match.startsWith(",") && match.endsWith(",")) return ",";
if (match.startsWith(",")) return "";
return "";
});
src = fixRelativeImports(src);
const methodConstants = await prettier.format(
`
export const GOOSE_EXT_METHODS = ${JSON.stringify(meta.methods, null, 2)} as const;
export type GooseExtMethod = (typeof GOOSE_EXT_METHODS)[number];
`,
{ parser: "typescript" },
);
await fs.writeFile(indexPath, `${src}\n${methodConstants}`);
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;
}
function methodToCamelCase(method: string): string {
return method
.split(/[/_]/)
.map((part, i) =>
i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1),
)
.join("");
}
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 = m.method;
let paramType = "";
let paramArg = "";
let callParams = "{}";
if (m.requestType) {
typeImports.add(m.requestType);
paramType = m.requestType;
paramArg = `params: ${paramType}`;
callParams = "params";
}
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 {
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}
export class GooseExtClient {
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);
}
// Run main if this file is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}
+46
View File
@@ -0,0 +1,46 @@
{
"name": "@aaif/goose-sdk",
"version": "0.16.0",
"description": "Agent Client Protocol (ACP) SDK for Goose AI agent",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/aaif-goose/goose.git"
},
"keywords": [
"goose",
"ai",
"agent",
"acp",
"agent-client-protocol"
],
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "npm run generate && npm run build:ts",
"build:ts": "tsc",
"build:native": "tsx scripts/build-native.ts",
"build:native:all": "tsx scripts/build-native.ts --all",
"generate": "tsx generate-schema.ts",
"lint": "tsc --noEmit",
"format": "prettier --write src/"
},
"dependencies": {
"zod": "^3.25.76"
},
"peerDependencies": {
"@agentclientprotocol/sdk": "*"
},
"devDependencies": {
"@agentclientprotocol/sdk": "^0.14.1",
"@hey-api/openapi-ts": "^0.92.3",
"@types/node": "^20.0.0",
"prettier": "^3.8.1",
"tsx": "^4.21.0",
"typescript": "~5.9.3"
}
}
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env node
/**
* Builds the goose binary for target platforms and places them
* into the corresponding npm package directories under ui/goose-binary/.
*
* Usage:
* npm run build:native # build for current platform only
* npm run build:native:all # build for all platforms
* tsx scripts/build-native.ts darwin-arm64 # build specific platform
*
* Prerequisites:
* - Rust cross-compilation toolchains installed for each target
*/
import { execSync } from "child_process";
import { dirname, resolve } from "path";
import { fileURLToPath } from "url";
import { mkdirSync, copyFileSync, chmodSync, existsSync } from "fs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT = resolve(__dirname, "../../..");
const NATIVE_DIR = resolve(ROOT, "ui/goose-binary");
const RUST_TARGETS: Record<string, string> = {
"darwin-arm64": "aarch64-apple-darwin",
"darwin-x64": "x86_64-apple-darwin",
"linux-arm64": "aarch64-unknown-linux-gnu",
"linux-x64": "x86_64-unknown-linux-gnu",
"win32-x64": "x86_64-pc-windows-msvc",
};
const PLATFORM_MAP: Record<string, string> = {
"darwin-arm64": "darwin-arm64",
"darwin-x64": "darwin-x64",
"linux-arm64": "linux-arm64",
"linux-x64": "linux-x64",
"win32-x64": "win32-x64",
};
function getCurrentPlatform(): string | null {
const platform = process.platform;
const arch = process.arch;
const key = `${platform}-${arch}`;
return PLATFORM_MAP[key] || null;
}
function buildTarget(platform: string): void {
const rustTarget = RUST_TARGETS[platform];
if (!rustTarget) {
throw new Error(`Unknown platform: ${platform}`);
}
const pkgDir = resolve(NATIVE_DIR, `goose-binary-${platform}`);
const binDir = resolve(pkgDir, "bin");
console.log(`==> Building goose for ${platform} (${rustTarget})`);
try {
execSync(`cargo build --release --target ${rustTarget} --bin goose`, {
cwd: ROOT,
stdio: "inherit",
});
} catch (err) {
console.error(`Failed to build for ${platform}`);
throw err;
}
mkdirSync(binDir, { recursive: true });
const ext = platform.startsWith("win32") ? ".exe" : "";
const binaryName = `goose${ext}`;
const srcPath = resolve(ROOT, "target", rustTarget, "release", binaryName);
const destPath = resolve(binDir, binaryName);
if (!existsSync(srcPath)) {
throw new Error(`Binary not found at ${srcPath}`);
}
copyFileSync(srcPath, destPath);
chmodSync(destPath, 0o755);
console.log(` ✅ Placed binary at ${destPath}`);
}
async function main() {
const args = process.argv.slice(2);
const buildAll = args.includes("--all");
if (buildAll) {
console.log("==> Building for all platforms");
for (const platform of Object.keys(RUST_TARGETS)) {
try {
buildTarget(platform);
} catch (err) {
console.error(`Failed to build ${platform}:`, err);
process.exit(1);
}
}
} else if (args.length > 0 && !args[0].startsWith("--")) {
// Build specific platforms
for (const platform of args) {
if (!RUST_TARGETS[platform]) {
console.error(`Unknown platform: ${platform}`);
console.error(
`Valid platforms: ${Object.keys(RUST_TARGETS).join(", ")}`,
);
process.exit(1);
}
buildTarget(platform);
}
} else {
// Build for current platform only
const currentPlatform = getCurrentPlatform();
if (!currentPlatform) {
console.error(
`Unsupported platform: ${process.platform}-${process.arch}`,
);
console.error(`Valid platforms: ${Object.keys(RUST_TARGETS).join(", ")}`);
console.error(`Use --all to build for all platforms`);
process.exit(1);
}
console.log(`==> Building for current platform: ${currentPlatform}`);
buildTarget(currentPlatform);
}
console.log("==> Done. Native packages staged in ui/goose-binary/");
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
+178
View File
@@ -0,0 +1,178 @@
// 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,
ArchiveSessionRequest,
CheckSecretRequest,
CheckSecretResponse,
DeleteSessionRequest,
ExportSessionRequest,
ExportSessionResponse,
GetExtensionsRequest,
GetExtensionsResponse,
GetProviderDetailsRequest,
GetProviderDetailsResponse,
GetProviderModelsRequest,
GetProviderModelsResponse,
GetToolsRequest,
GetToolsResponse,
ImportSessionRequest,
ImportSessionResponse,
ListProvidersRequest,
ListProvidersResponse,
ReadConfigRequest,
ReadConfigResponse,
ReadResourceRequest,
ReadResourceResponse,
RemoveConfigRequest,
RemoveExtensionRequest,
RemoveSecretRequest,
UnarchiveSessionRequest,
UpdateProviderRequest,
UpdateProviderResponse,
UpdateWorkingDirRequest,
UpsertConfigRequest,
UpsertSecretRequest,
} from './types.gen.js';
import {
zCheckSecretResponse,
zExportSessionResponse,
zGetExtensionsResponse,
zGetProviderDetailsResponse,
zGetProviderModelsResponse,
zGetToolsResponse,
zImportSessionResponse,
zListProvidersResponse,
zReadConfigResponse,
zReadResourceResponse,
zUpdateProviderResponse,
} from './zod.gen.js';
export class GooseExtClient {
constructor(private conn: ExtMethodProvider) {}
async GooseExtensionsAdd(params: AddExtensionRequest): Promise<void> {
await this.conn.extMethod("_goose/extensions/add", params);
}
async GooseExtensionsRemove(params: RemoveExtensionRequest): Promise<void> {
await this.conn.extMethod("_goose/extensions/remove", params);
}
async GooseTools(params: GetToolsRequest): Promise<GetToolsResponse> {
const raw = await this.conn.extMethod("_goose/tools", params);
return zGetToolsResponse.parse(raw) as GetToolsResponse;
}
async GooseResourceRead(
params: ReadResourceRequest,
): Promise<ReadResourceResponse> {
const raw = await this.conn.extMethod("_goose/resource/read", params);
return zReadResourceResponse.parse(raw) as ReadResourceResponse;
}
async GooseWorkingDirUpdate(params: UpdateWorkingDirRequest): Promise<void> {
await this.conn.extMethod("_goose/working_dir/update", params);
}
async sessionDelete(params: DeleteSessionRequest): Promise<void> {
await this.conn.extMethod("session/delete", params);
}
async GooseConfigExtensions(
params: GetExtensionsRequest,
): Promise<GetExtensionsResponse> {
const raw = await this.conn.extMethod("_goose/config/extensions", params);
return zGetExtensionsResponse.parse(raw) as GetExtensionsResponse;
}
async GooseSessionProviderUpdate(
params: UpdateProviderRequest,
): Promise<UpdateProviderResponse> {
const raw = await this.conn.extMethod(
"_goose/session/provider/update",
params,
);
return zUpdateProviderResponse.parse(raw) as UpdateProviderResponse;
}
async GooseProvidersList(
params: ListProvidersRequest,
): Promise<ListProvidersResponse> {
const raw = await this.conn.extMethod("_goose/providers/list", params);
return zListProvidersResponse.parse(raw) as ListProvidersResponse;
}
async GooseProvidersDetails(
params: GetProviderDetailsRequest,
): Promise<GetProviderDetailsResponse> {
const raw = await this.conn.extMethod("_goose/providers/details", params);
return zGetProviderDetailsResponse.parse(raw) as GetProviderDetailsResponse;
}
async GooseProvidersModels(
params: GetProviderModelsRequest,
): Promise<GetProviderModelsResponse> {
const raw = await this.conn.extMethod("_goose/providers/models", params);
return zGetProviderModelsResponse.parse(raw) as GetProviderModelsResponse;
}
async GooseConfigRead(
params: ReadConfigRequest,
): Promise<ReadConfigResponse> {
const raw = await this.conn.extMethod("_goose/config/read", params);
return zReadConfigResponse.parse(raw) as ReadConfigResponse;
}
async GooseConfigUpsert(params: UpsertConfigRequest): Promise<void> {
await this.conn.extMethod("_goose/config/upsert", params);
}
async GooseConfigRemove(params: RemoveConfigRequest): Promise<void> {
await this.conn.extMethod("_goose/config/remove", params);
}
async GooseSecretCheck(
params: CheckSecretRequest,
): Promise<CheckSecretResponse> {
const raw = await this.conn.extMethod("_goose/secret/check", params);
return zCheckSecretResponse.parse(raw) as CheckSecretResponse;
}
async GooseSecretUpsert(params: UpsertSecretRequest): Promise<void> {
await this.conn.extMethod("_goose/secret/upsert", params);
}
async GooseSecretRemove(params: RemoveSecretRequest): Promise<void> {
await this.conn.extMethod("_goose/secret/remove", params);
}
async GooseSessionExport(
params: ExportSessionRequest,
): Promise<ExportSessionResponse> {
const raw = await this.conn.extMethod("_goose/session/export", params);
return zExportSessionResponse.parse(raw) as ExportSessionResponse;
}
async GooseSessionImport(
params: ImportSessionRequest,
): Promise<ImportSessionResponse> {
const raw = await this.conn.extMethod("_goose/session/import", params);
return zImportSessionResponse.parse(raw) as ImportSessionResponse;
}
async GooseSessionArchive(params: ArchiveSessionRequest): Promise<void> {
await this.conn.extMethod("_goose/session/archive", params);
}
async GooseSessionUnarchive(params: UnarchiveSessionRequest): Promise<void> {
await this.conn.extMethod("_goose/session/unarchive", params);
}
}
+113
View File
@@ -0,0 +1,113 @@
// This file is auto-generated by @hey-api/openapi-ts
export type { AddExtensionRequest, ArchiveSessionRequest, CheckSecretRequest, CheckSecretResponse, DeleteSessionRequest, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetProviderDetailsRequest, GetProviderDetailsResponse, GetProviderModelsRequest, GetProviderModelsResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ListProvidersRequest, ListProvidersResponse, ModelEntry, ProviderConfigKey, ProviderDetailEntry, ProviderListEntry, ReadConfigRequest, ReadConfigResponse, ReadResourceRequest, ReadResourceResponse, RemoveConfigRequest, RemoveExtensionRequest, RemoveSecretRequest, UnarchiveSessionRequest, UpdateProviderRequest, UpdateProviderResponse, UpdateWorkingDirRequest, UpsertConfigRequest, UpsertSecretRequest } from './types.gen.js';
export const GOOSE_EXT_METHODS = [
{
method: "_goose/extensions/add",
requestType: "AddExtensionRequest",
responseType: "EmptyResponse",
},
{
method: "_goose/extensions/remove",
requestType: "RemoveExtensionRequest",
responseType: "EmptyResponse",
},
{
method: "_goose/tools",
requestType: "GetToolsRequest",
responseType: "GetToolsResponse",
},
{
method: "_goose/resource/read",
requestType: "ReadResourceRequest",
responseType: "ReadResourceResponse",
},
{
method: "_goose/working_dir/update",
requestType: "UpdateWorkingDirRequest",
responseType: "EmptyResponse",
},
{
method: "session/delete",
requestType: "DeleteSessionRequest",
responseType: "EmptyResponse",
},
{
method: "_goose/config/extensions",
requestType: "GetExtensionsRequest",
responseType: "GetExtensionsResponse",
},
{
method: "_goose/session/provider/update",
requestType: "UpdateProviderRequest",
responseType: "UpdateProviderResponse",
},
{
method: "_goose/providers/list",
requestType: "ListProvidersRequest",
responseType: "ListProvidersResponse",
},
{
method: "_goose/providers/details",
requestType: "GetProviderDetailsRequest",
responseType: "GetProviderDetailsResponse",
},
{
method: "_goose/providers/models",
requestType: "GetProviderModelsRequest",
responseType: "GetProviderModelsResponse",
},
{
method: "_goose/config/read",
requestType: "ReadConfigRequest",
responseType: "ReadConfigResponse",
},
{
method: "_goose/config/upsert",
requestType: "UpsertConfigRequest",
responseType: "EmptyResponse",
},
{
method: "_goose/config/remove",
requestType: "RemoveConfigRequest",
responseType: "EmptyResponse",
},
{
method: "_goose/secret/check",
requestType: "CheckSecretRequest",
responseType: "CheckSecretResponse",
},
{
method: "_goose/secret/upsert",
requestType: "UpsertSecretRequest",
responseType: "EmptyResponse",
},
{
method: "_goose/secret/remove",
requestType: "RemoveSecretRequest",
responseType: "EmptyResponse",
},
{
method: "_goose/session/export",
requestType: "ExportSessionRequest",
responseType: "ExportSessionResponse",
},
{
method: "_goose/session/import",
requestType: "ImportSessionRequest",
responseType: "ImportSessionResponse",
},
{
method: "_goose/session/archive",
requestType: "ArchiveSessionRequest",
responseType: "EmptyResponse",
},
{
method: "_goose/session/unarchive",
requestType: "UnarchiveSessionRequest",
responseType: "EmptyResponse",
},
] as const;
export type GooseExtMethod = (typeof GOOSE_EXT_METHODS)[number];
+317
View File
@@ -0,0 +1,317 @@
// This file is auto-generated by @hey-api/openapi-ts
/**
* Add an extension to an active session.
*/
export type AddExtensionRequest = {
sessionId: string;
/**
* Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform).
*/
config?: unknown;
};
/**
* Empty success response for operations that return no data.
*/
export type EmptyResponse = {
[key: string]: unknown;
};
/**
* Remove an extension from an active session.
*/
export type RemoveExtensionRequest = {
sessionId: string;
name: string;
};
/**
* List all tools available in a session.
*/
export type GetToolsRequest = {
sessionId: string;
};
/**
* Tools response.
*/
export type GetToolsResponse = {
/**
* Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`.
*/
tools: Array<unknown>;
};
/**
* Read a resource from an extension.
*/
export type ReadResourceRequest = {
sessionId: string;
uri: string;
extensionName: string;
};
/**
* Resource read response.
*/
export type ReadResourceResponse = {
/**
* The resource result from the extension (MCP ReadResourceResult).
*/
result?: unknown;
};
/**
* Update the working directory for a session.
*/
export type UpdateWorkingDirRequest = {
sessionId: string;
workingDir: string;
};
/**
* Delete a session.
*/
export type DeleteSessionRequest = {
sessionId: string;
};
/**
* List configured extensions and any warnings.
*/
export type GetExtensionsRequest = {
[key: string]: unknown;
};
/**
* List configured extensions and any warnings.
*/
export type GetExtensionsResponse = {
/**
* Array of ExtensionEntry objects with `enabled` flag and config details.
*/
extensions: Array<unknown>;
warnings: Array<string>;
};
/**
* Atomically update the provider for a live session.
*/
export type UpdateProviderRequest = {
sessionId: string;
provider: string;
model?: string | null;
contextLimit?: number | null;
requestParams?: {
[key: string]: unknown;
} | null;
};
/**
* Provider update response.
*/
export type UpdateProviderResponse = {
/**
* Refreshed session config options after the provider/model change.
*/
configOptions: Array<unknown>;
};
/**
* List providers available through goose, including the config-default sentinel.
*/
export type ListProvidersRequest = {
[key: string]: unknown;
};
/**
* Provider list response.
*/
export type ListProvidersResponse = {
providers: Array<ProviderListEntry>;
};
export type ProviderListEntry = {
id: string;
label: string;
};
/**
* List providers with full metadata (config keys, setup steps, etc.).
*/
export type GetProviderDetailsRequest = {
[key: string]: unknown;
};
/**
* Provider details response.
*/
export type GetProviderDetailsResponse = {
providers: Array<ProviderDetailEntry>;
};
export type ProviderDetailEntry = {
name: string;
displayName: string;
description: string;
defaultModel: string;
isConfigured: boolean;
providerType: string;
configKeys: Array<ProviderConfigKey>;
setupSteps?: Array<string>;
knownModels?: Array<ModelEntry>;
};
export type ProviderConfigKey = {
name: string;
required: boolean;
secret: boolean;
default?: string | null;
oauthFlow?: boolean;
deviceCodeFlow?: boolean;
primary?: boolean;
};
export type ModelEntry = {
name: string;
contextLimit: number;
};
/**
* Fetch the full list of models available for a specific provider.
*/
export type GetProviderModelsRequest = {
providerName: string;
};
/**
* Provider models response.
*/
export type GetProviderModelsResponse = {
models: Array<string>;
};
/**
* Read a single non-secret config value.
*/
export type ReadConfigRequest = {
key: string;
};
/**
* Config read response.
*/
export type ReadConfigResponse = {
value?: unknown;
};
/**
* Upsert a single non-secret config value.
*/
export type UpsertConfigRequest = {
key: string;
value: unknown;
};
/**
* Remove a single non-secret config value.
*/
export type RemoveConfigRequest = {
key: string;
};
/**
* Check whether a secret exists. Never returns the actual value.
*/
export type CheckSecretRequest = {
key: string;
};
/**
* Secret check response.
*/
export type CheckSecretResponse = {
exists: boolean;
};
/**
* Set a secret value (write-only).
*/
export type UpsertSecretRequest = {
key: string;
value: unknown;
};
/**
* Remove a secret.
*/
export type RemoveSecretRequest = {
key: string;
};
/**
* Export a session as a JSON string.
*/
export type ExportSessionRequest = {
sessionId: string;
};
/**
* Export session response — raw JSON of the goose session with `conversation`.
*/
export type ExportSessionResponse = {
data: string;
};
/**
* Import a session from a JSON string.
*/
export type ImportSessionRequest = {
data: string;
};
/**
* Import session response — metadata about the newly created session.
*/
export type ImportSessionResponse = {
sessionId: string;
title?: string | null;
updatedAt?: string | null;
messageCount: number;
};
/**
* Archive a session (soft delete).
*/
export type ArchiveSessionRequest = {
sessionId: string;
};
/**
* Unarchive a previously archived session.
*/
export type UnarchiveSessionRequest = {
sessionId: string;
};
export type ExtRequest = {
id: string;
method: string;
params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | ReadResourceRequest | UpdateWorkingDirRequest | DeleteSessionRequest | GetExtensionsRequest | UpdateProviderRequest | ListProvidersRequest | GetProviderDetailsRequest | GetProviderModelsRequest | ReadConfigRequest | UpsertConfigRequest | RemoveConfigRequest | CheckSecretRequest | UpsertSecretRequest | RemoveSecretRequest | ExportSessionRequest | ImportSessionRequest | ArchiveSessionRequest | UnarchiveSessionRequest | {
[key: string]: unknown;
} | null;
};
export type ExtResponse = {
id: string;
result?: EmptyResponse | GetToolsResponse | ReadResourceResponse | GetExtensionsResponse | UpdateProviderResponse | ListProvidersResponse | GetProviderDetailsResponse | GetProviderModelsResponse | ReadConfigResponse | CheckSecretResponse | ExportSessionResponse | ImportSessionResponse | unknown;
} | {
error: {
code: number;
message: string;
data?: unknown;
};
id: string;
};
+356
View File
@@ -0,0 +1,356 @@
// This file is auto-generated by @hey-api/openapi-ts
import { z } from 'zod';
/**
* Add an extension to an active session.
*/
export const zAddExtensionRequest = z.object({
sessionId: z.string(),
config: z.unknown().optional().default(null)
});
/**
* Empty success response for operations that return no data.
*/
export const zEmptyResponse = z.record(z.unknown());
/**
* Remove an extension from an active session.
*/
export const zRemoveExtensionRequest = z.object({
sessionId: z.string(),
name: z.string()
});
/**
* List all tools available in a session.
*/
export const zGetToolsRequest = z.object({
sessionId: z.string()
});
/**
* Tools response.
*/
export const zGetToolsResponse = z.object({
tools: z.array(z.unknown())
});
/**
* Read a resource from an extension.
*/
export const zReadResourceRequest = z.object({
sessionId: z.string(),
uri: z.string(),
extensionName: z.string()
});
/**
* Resource read response.
*/
export const zReadResourceResponse = z.object({
result: z.unknown().optional().default(null)
});
/**
* Update the working directory for a session.
*/
export const zUpdateWorkingDirRequest = z.object({
sessionId: z.string(),
workingDir: z.string()
});
/**
* Delete a session.
*/
export const zDeleteSessionRequest = z.object({
sessionId: z.string()
});
/**
* List configured extensions and any warnings.
*/
export const zGetExtensionsRequest = z.record(z.unknown());
/**
* List configured extensions and any warnings.
*/
export const zGetExtensionsResponse = z.object({
extensions: z.array(z.unknown()),
warnings: z.array(z.string())
});
/**
* Atomically update the provider for a live session.
*/
export const zUpdateProviderRequest = z.object({
sessionId: z.string(),
provider: z.string(),
model: z.union([
z.string(),
z.null()
]).optional(),
contextLimit: z.union([
z.number().int().gte(0),
z.null()
]).optional(),
requestParams: z.union([
z.record(z.unknown()),
z.null()
]).optional()
});
/**
* Provider update response.
*/
export const zUpdateProviderResponse = z.object({
configOptions: z.array(z.unknown())
});
/**
* List providers available through goose, including the config-default sentinel.
*/
export const zListProvidersRequest = z.record(z.unknown());
export const zProviderListEntry = z.object({
id: z.string(),
label: z.string()
});
/**
* Provider list response.
*/
export const zListProvidersResponse = z.object({
providers: z.array(zProviderListEntry)
});
/**
* List providers with full metadata (config keys, setup steps, etc.).
*/
export const zGetProviderDetailsRequest = z.record(z.unknown());
export const zProviderConfigKey = z.object({
name: z.string(),
required: z.boolean(),
secret: z.boolean(),
default: z.union([
z.string(),
z.null()
]).optional().default(null),
oauthFlow: z.boolean().optional().default(false),
deviceCodeFlow: z.boolean().optional().default(false),
primary: z.boolean().optional().default(false)
});
export const zModelEntry = z.object({
name: z.string(),
contextLimit: z.number().int().gte(0)
});
export const zProviderDetailEntry = z.object({
name: z.string(),
displayName: z.string(),
description: z.string(),
defaultModel: z.string(),
isConfigured: z.boolean(),
providerType: z.string(),
configKeys: z.array(zProviderConfigKey),
setupSteps: z.array(z.string()).optional().default([]),
knownModels: z.array(zModelEntry).optional().default([])
});
/**
* Provider details response.
*/
export const zGetProviderDetailsResponse = z.object({
providers: z.array(zProviderDetailEntry)
});
/**
* Fetch the full list of models available for a specific provider.
*/
export const zGetProviderModelsRequest = z.object({
providerName: z.string()
});
/**
* Provider models response.
*/
export const zGetProviderModelsResponse = z.object({
models: z.array(z.string())
});
/**
* Read a single non-secret config value.
*/
export const zReadConfigRequest = z.object({
key: z.string()
});
/**
* Config read response.
*/
export const zReadConfigResponse = z.object({
value: z.unknown().optional().default(null)
});
/**
* Upsert a single non-secret config value.
*/
export const zUpsertConfigRequest = z.object({
key: z.string(),
value: z.unknown()
});
/**
* Remove a single non-secret config value.
*/
export const zRemoveConfigRequest = z.object({
key: z.string()
});
/**
* Check whether a secret exists. Never returns the actual value.
*/
export const zCheckSecretRequest = z.object({
key: z.string()
});
/**
* Secret check response.
*/
export const zCheckSecretResponse = z.object({
exists: z.boolean()
});
/**
* Set a secret value (write-only).
*/
export const zUpsertSecretRequest = z.object({
key: z.string(),
value: z.unknown()
});
/**
* Remove a secret.
*/
export const zRemoveSecretRequest = z.object({
key: z.string()
});
/**
* Export a session as a JSON string.
*/
export const zExportSessionRequest = z.object({
sessionId: z.string()
});
/**
* Export session response — raw JSON of the goose session with `conversation`.
*/
export const zExportSessionResponse = z.object({
data: z.string()
});
/**
* Import a session from a JSON string.
*/
export const zImportSessionRequest = z.object({
data: z.string()
});
/**
* Import session response — metadata about the newly created session.
*/
export const zImportSessionResponse = z.object({
sessionId: z.string(),
title: z.union([
z.string(),
z.null()
]).optional(),
updatedAt: z.union([
z.string(),
z.null()
]).optional(),
messageCount: z.number().int().gte(0)
});
/**
* Archive a session (soft delete).
*/
export const zArchiveSessionRequest = z.object({
sessionId: z.string()
});
/**
* Unarchive a previously archived session.
*/
export const zUnarchiveSessionRequest = z.object({
sessionId: z.string()
});
export const zExtRequest = z.object({
id: z.string(),
method: z.string(),
params: z.union([
z.union([
zAddExtensionRequest,
zRemoveExtensionRequest,
zGetToolsRequest,
zReadResourceRequest,
zUpdateWorkingDirRequest,
zDeleteSessionRequest,
zGetExtensionsRequest,
zUpdateProviderRequest,
zListProvidersRequest,
zGetProviderDetailsRequest,
zGetProviderModelsRequest,
zReadConfigRequest,
zUpsertConfigRequest,
zRemoveConfigRequest,
zCheckSecretRequest,
zUpsertSecretRequest,
zRemoveSecretRequest,
zExportSessionRequest,
zImportSessionRequest,
zArchiveSessionRequest,
zUnarchiveSessionRequest
]),
z.union([
z.record(z.unknown()),
z.null()
])
]).optional()
});
export const zExtResponse = z.union([
z.object({
id: z.string(),
result: z.union([
z.union([
zEmptyResponse,
zGetToolsResponse,
zReadResourceResponse,
zGetExtensionsResponse,
zUpdateProviderResponse,
zListProvidersResponse,
zGetProviderDetailsResponse,
zGetProviderModelsResponse,
zReadConfigResponse,
zCheckSecretResponse,
zExportSessionResponse,
zImportSessionResponse
]),
z.unknown()
]).optional()
}),
z.object({
error: z.object({
code: z.number().int(),
message: z.string(),
data: z.unknown().optional()
}),
id: z.string()
})
]);
+123
View File
@@ -0,0 +1,123 @@
import {
ClientSideConnection,
type Client,
type Stream,
type InitializeRequest,
type InitializeResponse,
type NewSessionRequest,
type NewSessionResponse,
type LoadSessionRequest,
type LoadSessionResponse,
type PromptRequest,
type PromptResponse,
type CancelNotification,
type AuthenticateRequest,
type AuthenticateResponse,
type SetSessionModeRequest,
type SetSessionModeResponse,
type SetSessionConfigOptionRequest,
type SetSessionConfigOptionResponse,
type ForkSessionRequest,
type ForkSessionResponse,
type ListSessionsRequest,
type ListSessionsResponse,
type ResumeSessionRequest,
type ResumeSessionResponse,
type SetSessionModelRequest,
type SetSessionModelResponse,
} from "@agentclientprotocol/sdk";
import { GooseExtClient } from "./generated/client.gen.js";
import { createHttpStream } from "./http-stream.js";
export class GooseClient {
private conn: ClientSideConnection;
private ext: GooseExtClient;
constructor(toClient: () => Client, streamOrUrl: Stream | string) {
const stream =
typeof streamOrUrl === "string"
? createHttpStream(streamOrUrl)
: streamOrUrl;
this.conn = new ClientSideConnection(toClient, stream);
this.ext = new GooseExtClient(this.conn);
}
get signal(): AbortSignal {
return this.conn.signal;
}
get closed(): Promise<void> {
return this.conn.closed;
}
initialize(params: InitializeRequest): Promise<InitializeResponse> {
return this.conn.initialize(params);
}
newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
return this.conn.newSession(params);
}
loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
return this.conn.loadSession(params);
}
prompt(params: PromptRequest): Promise<PromptResponse> {
return this.conn.prompt(params);
}
cancel(params: CancelNotification): Promise<void> {
return this.conn.cancel(params);
}
authenticate(params: AuthenticateRequest): Promise<AuthenticateResponse> {
return this.conn.authenticate(params);
}
setSessionMode(
params: SetSessionModeRequest,
): Promise<SetSessionModeResponse> {
return this.conn.setSessionMode(params);
}
setSessionConfigOption(
params: SetSessionConfigOptionRequest,
): Promise<SetSessionConfigOptionResponse> {
return this.conn.setSessionConfigOption(params);
}
unstable_forkSession(
params: ForkSessionRequest,
): Promise<ForkSessionResponse> {
return this.conn.unstable_forkSession(params);
}
unstable_listSessions(
params: ListSessionsRequest,
): Promise<ListSessionsResponse> {
return this.conn.unstable_listSessions(params);
}
unstable_resumeSession(
params: ResumeSessionRequest,
): Promise<ResumeSessionResponse> {
return this.conn.unstable_resumeSession(params);
}
unstable_setSessionModel(
params: SetSessionModelRequest,
): Promise<SetSessionModelResponse> {
return this.conn.unstable_setSessionModel(params);
}
extMethod(
method: string,
params: Record<string, unknown>,
): Promise<Record<string, unknown>> {
return this.conn.extMethod(method, params);
}
get goose(): GooseExtClient {
return this.ext;
}
}
+120
View File
@@ -0,0 +1,120 @@
import type { AnyMessage, Stream } from "@agentclientprotocol/sdk";
const ACP_SESSION_HEADER = "Acp-Session-Id";
export function createHttpStream(serverUrl: string): Stream {
let sessionId: string | null = null;
const incoming: AnyMessage[] = [];
const waiters: Array<() => void> = [];
const sseAbort = new AbortController();
function pushMessage(msg: AnyMessage) {
incoming.push(msg);
const w = waiters.shift();
if (w) w();
}
function waitForMessage(): Promise<void> {
if (incoming.length > 0) return Promise.resolve();
return new Promise<void>((r) => waiters.push(r));
}
async function consumeSSE(response: Response) {
if (!response.body) return;
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split("\n\n");
buffer = parts.pop() || "";
for (const part of parts) {
for (const line of part.split("\n")) {
if (line.startsWith("data: ")) {
try {
const msg = JSON.parse(line.slice(6)) as AnyMessage;
pushMessage(msg);
} catch {
// ignore malformed JSON
}
}
}
}
}
} catch (e: unknown) {
if (e instanceof DOMException && e.name === "AbortError") return;
}
}
let isFirstRequest = true;
const readable = new ReadableStream<AnyMessage>({
async pull(controller) {
await waitForMessage();
while (incoming.length > 0) {
controller.enqueue(incoming.shift()!);
}
},
});
const writable = new WritableStream<AnyMessage>({
async write(msg) {
const isRequest =
"method" in msg &&
"id" in msg &&
msg.id !== undefined &&
msg.id !== null;
const headers: Record<string, string> = {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
};
if (sessionId) {
headers[ACP_SESSION_HEADER] = sessionId;
}
if (isFirstRequest && isRequest) {
isFirstRequest = false;
const response = await fetch(`${serverUrl}/acp`, {
method: "POST",
headers,
body: JSON.stringify(msg),
signal: sseAbort.signal,
});
const sid = response.headers.get(ACP_SESSION_HEADER);
if (sid) sessionId = sid;
consumeSSE(response);
} else if (isRequest) {
const abort = new AbortController();
fetch(`${serverUrl}/acp`, {
method: "POST",
headers,
body: JSON.stringify(msg),
signal: abort.signal,
}).catch(() => {});
setTimeout(() => abort.abort(), 200);
} else {
await fetch(`${serverUrl}/acp`, {
method: "POST",
headers,
body: JSON.stringify(msg),
});
}
},
close() {
sseAbort.abort();
},
});
return { readable, writable };
}
+10
View File
@@ -0,0 +1,10 @@
export * from "./generated/types.gen.js";
export * from "./generated/zod.gen.js";
export { GooseClient } from "./goose-client.js";
export { createHttpStream } from "./http-stream.js";
export {
ClientSideConnection,
type Client,
type Stream,
} from "@agentclientprotocol/sdk";
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "nodenext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src",
},
"include": ["src"],
}