diff --git a/packages/redrob/src/daemon/connect.ts b/packages/redrob/src/daemon/connect.ts index 059a3ff377..3027ea0dee 100644 --- a/packages/redrob/src/daemon/connect.ts +++ b/packages/redrob/src/daemon/connect.ts @@ -87,6 +87,8 @@ export async function connectOrStart(options: StartOptions = {}): Promise = { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".webp": "image/webp", + ".gif": "image/gif", +} + +/** MCP Apps limits, as Cowork's mcp-app-host had them. */ +const MAX_APP_HTML_BYTES = 2 * 1024 * 1024 +const APP_CALL_TIMEOUT_MS = 120_000 + +/** `1536x1024` -> `3:2`, the aspect the gateway's image models take. */ +function aspectOf(size: unknown): string | undefined { + const match = typeof size === "string" ? /^(\d+)x(\d+)$/.exec(size.trim()) : null + if (!match) return undefined + const w = Number(match[1]) + const h = Number(match[2]) + if (!w || !h) return undefined + const gcd = (a: number, b: number): number => (b ? gcd(b, a % b) : a) + const d = gcd(w, h) + return `${w / d}:${h / d}` +} + +function message(error: unknown): string { + if (error instanceof Error) return error.message + if (error && typeof error === "object" && "message" in error && typeof error.message === "string") return error.message + return String(error) +} + +/** A reference image as a data URL: a `file://` path the desktop sent, or a data URL as is. */ +async function referenceUrl(ref: string): Promise { + if (ref.startsWith("data:image/")) return ref + const file = ref.startsWith("file://") ? ref.slice("file://".length) : ref + const mime = REFERENCE_MIME[path.extname(file).toLowerCase()] + if (!mime) throw new Error(`Not an image this engine can send: ${path.basename(file)}`) + const bytes = await readFile(file) + if (bytes.byteLength === 0 || bytes.byteLength > MAX_REFERENCE_BYTES) + throw new Error(`${path.basename(file)} is empty or larger than 20 MB.`) + return `data:${mime};base64,${bytes.toString("base64")}` +} + +function imagePrompt(action: string, prompt: string, references: number): string { + if (action === "edit") return `Edit the attached image. ${prompt}` + if (action === "blend") return `Blend the ${references} attached images into one new image. ${prompt}` + return prompt +} + +export const extensionsHandlers = HttpApiBuilder.group(InstanceHttpApi, "extensions", (handlers) => + Effect.gen(function* () { + const http = yield* HttpClient.HttpClient + const auth = yield* Auth.Service + const mcp = yield* MCP.Service + + const image = Effect.fnUntraced(function* (action: string, args: Record) { + if (!IMAGE_ACTIONS.has(action)) return { ok: false, error: `Unknown image action: ${action}` } + const prompt = typeof args.prompt === "string" ? args.prompt.trim() : "" + if (!prompt) return { ok: false, error: "Describe the image to make." } + const refs = Array.isArray(args.images) ? args.images.filter((x): x is string => typeof x === "string") : [] + if (action === "edit" && refs.length !== 1) return { ok: false, error: "Edit takes exactly one image." } + if (action === "blend" && refs.length < 2) return { ok: false, error: "Blend takes two or more images." } + if (refs.length > MAX_REFERENCES) return { ok: false, error: `At most ${MAX_REFERENCES} reference images.` } + + const stored = yield* auth.get("redrob").pipe(Effect.orElseSucceed(() => undefined)) + const key = redrobKey(stored) ?? (yield* Effect.promise(() => SharedAccount.apiKey())) + if (!key) return { ok: false, error: "Sign in to Redrob to make images: no Redrob API key is available." } + + const model = typeof args.model === "string" && args.model.trim() ? args.model.trim() : DEFAULT_IMAGE_MODEL + const urls = yield* Effect.tryPromise({ + try: () => Promise.all(refs.map(referenceUrl)), + catch: (error) => new Error(message(error)), + }) + const text = imagePrompt(action, prompt, urls.length) + const aspect = aspectOf(args.size) + const request = yield* HttpClientRequest.post(`${gatewayUrl()}/chat/completions`).pipe( + HttpClientRequest.bearerToken(key), + HttpClientRequest.acceptJson, + HttpClientRequest.bodyJson({ + model, + messages: [ + { + role: "user", + content: urls.length + ? [{ type: "text", text }, ...urls.map((url) => ({ type: "image_url", image_url: { url } }))] + : text, + }, + ], + modalities: ["image", "text"], + ...(aspect ? { image_config: { aspect_ratio: aspect } } : {}), + }), + ) + const response = yield* http + .execute(request) + .pipe(Effect.timeoutOrElse({ duration: IMAGE_TIMEOUT_MS, orElse: () => Effect.fail(new Error("Image generation timed out.")) })) + const body = yield* response.text + if (response.status < 200 || response.status >= 300) + return { ok: false, error: `The Redrob gateway answered ${response.status}: ${gatewayMessage(body)}` } + const payload = yield* decodeResponse(body) + const reply = payload.choices[0]?.message + const picture = (reply?.images ?? []).map((item) => decodeImageDataUrl(item.image_url.url)).find(Boolean) + if (!picture) { + const said = reply?.content?.trim() + return { ok: false, error: `${model} returned no image${said ? `. It said: ${said.slice(0, 500)}` : "."}` } + } + const costUsd = payload.redrob?.costUsd + return { + ok: true, + result: { + b64: Buffer.from(picture.bytes).toString("base64"), + mime: picture.mime, + model, + ...(costUsd === undefined ? {} : { costUsd }), + }, + } + }) + + const call = Effect.fn("ExtensionsHttpApi.call")(function* (ctx: { + payload: Schema.Schema.Type + }) { + const { extensionId, action, args } = ctx.payload + if (!IMAGE_EXTENSIONS.has(extensionId)) return { ok: false, error: `Unknown extension: ${extensionId}` } + return yield* image(action, args ?? {}).pipe( + Effect.catch((error) => Effect.succeed({ ok: false, error: message(error) })), + ) + }) + + /** The connected client a host means: its exact name, its sanitised name, or the prefix of its tool names. */ + const client = Effect.fnUntraced(function* (server: string) { + const clients = yield* mcp.clients() + if (clients[server]) return { name: server, client: clients[server] } + const names = Object.keys(clients) + const exact = names.find((name) => McpCatalog.sanitize(name) === server) + const prefixed = names.filter((name) => McpCatalog.sanitize(name).startsWith(`${server}_`)) + const name = exact ?? (prefixed.length === 1 ? prefixed[0] : undefined) + if (!name) + return yield* new InvalidRequestError({ message: `No connected MCP server named ${server}`, field: "server" }) + return { name, client: clients[name] } + }) + + const mcpAppResolve = Effect.fn("ExtensionsHttpApi.mcpAppResolve")(function* (ctx: { + payload: Schema.Schema.Type + }) { + const { uri } = ctx.payload + if (!uri.startsWith("ui://")) + return yield* new InvalidRequestError({ message: "An MCP App view is a ui:// resource.", field: "uri" }) + const { name } = yield* client(ctx.payload.server) + const read = yield* mcp.readResource(name, uri) + if (!read) return yield* new UpstreamError({ message: `${name} did not return ${uri}`, service: name }) + const item = read.contents.find((c) => c.uri === uri) ?? read.contents[0] + if (!item) return yield* new UpstreamError({ message: `${name} returned ${uri} empty`, service: name }) + if (item.mimeType && !item.mimeType.startsWith("text/html")) + return yield* new InvalidRequestError({ message: `${uri} is ${item.mimeType}, not an HTML view.`, field: "uri" }) + const html = + "text" in item && typeof item.text === "string" + ? item.text + : "blob" in item && typeof item.blob === "string" + ? Buffer.from(item.blob, "base64").toString("utf8") + : "" + if (!html) return yield* new UpstreamError({ message: `${name} returned ${uri} empty`, service: name }) + if (Buffer.byteLength(html) > MAX_APP_HTML_BYTES) + return yield* new UpstreamError({ message: `${uri} is larger than 2 MB`, service: name }) + return { html, mimeType: item.mimeType, server: name } + }) + + const mcpAppCall = Effect.fn("ExtensionsHttpApi.mcpAppCall")(function* (ctx: { + payload: Schema.Schema.Type + }) { + const target = yield* client(ctx.payload.server) + const result: unknown = yield* Effect.tryPromise({ + try: () => + target.client.callTool({ name: ctx.payload.name, arguments: ctx.payload.arguments ?? {} }, undefined, { + timeout: APP_CALL_TIMEOUT_MS, + }), + catch: (error) => new UpstreamError({ message: message(error), service: target.name }), + }) + return result + }) + + return handlers.handle("call", call).handle("mcpAppResolve", mcpAppResolve).handle("mcpAppCall", mcpAppCall) + }), +) diff --git a/packages/redrob/src/server/routes/instance/httpapi/server.ts b/packages/redrob/src/server/routes/instance/httpapi/server.ts index d2915064f1..862c1741ce 100644 --- a/packages/redrob/src/server/routes/instance/httpapi/server.ts +++ b/packages/redrob/src/server/routes/instance/httpapi/server.ts @@ -92,6 +92,7 @@ import { fileHandlers } from "./handlers/file" import { globalHandlers } from "./handlers/global" import { instanceHandlers } from "./handlers/instance" import { mcpHandlers } from "./handlers/mcp" +import { extensionsHandlers } from "./handlers/extensions" import { memoryHandlers } from "./handlers/memory" import { permissionHandlers } from "./handlers/permission" import { projectHandlers } from "./handlers/project" @@ -158,6 +159,7 @@ const instanceApiRoutes = HttpApiBuilder.layer(InstanceHttpApi).pipe( Layer.provide([ configHandlers, experimentalHandlers, + extensionsHandlers, fileHandlers, instanceHandlers, mcpHandlers, diff --git a/packages/redrob/src/tool/image_generate.ts b/packages/redrob/src/tool/image_generate.ts index 12ff427f09..5af3b09a6f 100644 --- a/packages/redrob/src/tool/image_generate.ts +++ b/packages/redrob/src/tool/image_generate.ts @@ -68,7 +68,7 @@ const GatewayResponse = Schema.Struct({ redrob: Schema.optional(Schema.Struct({ costUsd: Schema.optional(Schema.Number) })), }) -const decodeResponse = Schema.decodeUnknownEffect(Schema.fromJsonString(GatewayResponse)) +export const decodeResponse = Schema.decodeUnknownEffect(Schema.fromJsonString(GatewayResponse)) /** The Redrob key: the engine's stored credential first, then the environment, as the provider resolves it. */ export function redrobKey(stored: Auth.Info | undefined): string | undefined { diff --git a/packages/sdk/js/src/server.ts b/packages/sdk/js/src/server.ts index 5249d4f064..140c12f5e0 100644 --- a/packages/sdk/js/src/server.ts +++ b/packages/sdk/js/src/server.ts @@ -33,6 +33,7 @@ export async function createRedrobServer(options?: ServerOptions) { if (options.config?.logLevel) args.push(`--log-level=${options.config.logLevel}`) const proc = launch(`redrob`, args, { + windowsHide: true, env: { ...process.env, REDROB_CONFIG_CONTENT: JSON.stringify(options.config ?? {}), diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 3361cc54bf..6333bb0c9e 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -43,6 +43,8 @@ import type { ExperimentalConsoleSwitchOrgResponses, ExperimentalControlPlaneMoveSessionErrors, ExperimentalControlPlaneMoveSessionResponses, + ExperimentalExtensionsCallErrors, + ExperimentalExtensionsCallResponses, ExperimentalProjectCopyGenerateNameErrors, ExperimentalProjectCopyGenerateNameResponses, ExperimentalResourceListErrors, @@ -100,6 +102,10 @@ import type { LspStatusResponses, McpAddErrors, McpAddResponses, + McpAppCallErrors, + McpAppCallResponses, + McpAppResolveErrors, + McpAppResolveResponses, McpAuthAuthenticateErrors, McpAuthAuthenticateResponses, McpAuthCallbackErrors, @@ -1087,6 +1093,55 @@ export class Resource extends HeyApiClient { } } +export class Extensions extends HeyApiClient { + /** + * Call an extension + * + * Run one action of a built-in extension (image generation) with the engine's credential. + */ + public call( + parameters?: { + directory?: string + workspace?: string + extensionId?: string + action?: string + args?: { + [key: string]: unknown + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "extensionId" }, + { in: "body", key: "action" }, + { in: "body", key: "args" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ExperimentalExtensionsCallResponses, + ExperimentalExtensionsCallErrors, + ThrowOnError + >({ + url: "/experimental/extensions/call", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + export class ProjectCopy extends HeyApiClient { /** * Generate project copy name @@ -1431,6 +1486,11 @@ export class Experimental extends HeyApiClient { return (this._resource ??= new Resource({ client: this.client })) } + private _extensions?: Extensions + get extensions(): Extensions { + return (this._extensions ??= new Extensions({ client: this.client })) + } + private _projectCopy?: ProjectCopy get projectCopy(): ProjectCopy { return (this._projectCopy ??= new ProjectCopy({ client: this.client })) @@ -1887,17 +1947,18 @@ export class Worktree extends HeyApiClient { } } -export class Find extends HeyApiClient { +export class App2 extends HeyApiClient { /** - * Find text + * Resolve an MCP App view * - * Search for text patterns across files in the project using ripgrep. + * Read a connector's ui:// resource so a host can show it in a sandboxed frame. */ - public text( - parameters: { + public resolve( + parameters?: { directory?: string workspace?: string - pattern: string + server?: string + uri?: string }, options?: Options, ) { @@ -1908,31 +1969,38 @@ export class Find extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "pattern" }, + { in: "body", key: "server" }, + { in: "body", key: "uri" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/find", + return (options?.client ?? this.client).post({ + url: "/mcp-apps/resolve", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } /** - * Find files + * Call a tool from an MCP App view * - * Search for files or directories by name or pattern in the project directory. + * Call a connector's tool for its app view. The host confirms the call with the person first. */ - public files( - parameters: { + public call( + parameters?: { directory?: string workspace?: string - query: string - dirs?: "true" | "false" - type?: "file" | "directory" - limit?: number + server?: string + name?: string + arguments?: { + [key: string]: unknown + } }, options?: Options, ) { @@ -1943,31 +2011,37 @@ export class Find extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "query" }, - { in: "query", key: "dirs" }, - { in: "query", key: "type" }, - { in: "query", key: "limit" }, + { in: "body", key: "server" }, + { in: "body", key: "name" }, + { in: "body", key: "arguments" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/find/file", + return (options?.client ?? this.client).post({ + url: "/mcp-apps/call", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } +} +export class Auth2 extends HeyApiClient { /** - * Find symbols + * Remove MCP OAuth * - * Search for workspace symbols like functions, classes, and variables using LSP. + * Remove OAuth credentials for an MCP server. */ - public symbols( + public remove( parameters: { + name: string directory?: string workspace?: string - query: string }, options?: Options, ) { @@ -1976,32 +2050,30 @@ export class Find extends HeyApiClient { [ { args: [ + { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "query" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/find/symbol", + return (options?.client ?? this.client).delete({ + url: "/mcp/{name}/auth", ...options, ...params, }) } -} -export class File extends HeyApiClient { /** - * List files + * Start MCP OAuth * - * List files and directories in a specified path. + * Start OAuth authentication flow for a Model Context Protocol (MCP) server. */ - public list( + public start( parameters: { + name: string directory?: string workspace?: string - path: string }, options?: Options, ) { @@ -2010,30 +2082,31 @@ export class File extends HeyApiClient { [ { args: [ + { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "path" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/file", + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/auth", ...options, ...params, }) } /** - * Read file + * Complete MCP OAuth * - * Read the content of a specified file. + * Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code. */ - public read( + public callback( parameters: { + name: string directory?: string workspace?: string - path: string + code?: string }, options?: Options, ) { @@ -2042,24 +2115,66 @@ export class File extends HeyApiClient { [ { args: [ + { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "path" }, + { in: "body", key: "code" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/file/content", + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/auth/callback", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } /** - * Get file status + * Authenticate MCP OAuth * - * Get the git status of all files in the project. + * Start OAuth flow and wait for callback (opens browser). + */ + public authenticate( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post( + { + url: "/mcp/{name}/auth/authenticate", + ...options, + ...params, + }, + ) + } +} + +export class Mcp extends HeyApiClient { + /** + * Get MCP status + * + * Get the status of all Model Context Protocol (MCP) servers. */ public status( parameters?: { @@ -2079,24 +2194,24 @@ export class File extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ - url: "/file/status", + return (options?.client ?? this.client).get({ + url: "/mcp", ...options, ...params, }) } -} -export class Instance extends HeyApiClient { /** - * Dispose instance + * Add MCP server * - * Clean up and dispose the current RedrobCode instance, releasing all resources. + * Dynamically add a new Model Context Protocol (MCP) server to the system. */ - public dispose( + public add( parameters?: { directory?: string workspace?: string + name?: string + config?: McpLocalConfig | McpRemoteConfig }, options?: Options, ) { @@ -2107,26 +2222,30 @@ export class Instance extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "name" }, + { in: "body", key: "config" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/instance/dispose", + return (options?.client ?? this.client).post({ + url: "/mcp", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } -} -export class Path extends HeyApiClient { /** - * Get paths - * - * Retrieve the current working directory and related path information for the RedrobCode instance. + * Connect an MCP server. */ - public get( - parameters?: { + public connect( + parameters: { + name: string directory?: string workspace?: string }, @@ -2137,30 +2256,72 @@ export class Path extends HeyApiClient { [ { args: [ + { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/path", + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/connect", + ...options, + ...params, + }) + } + + /** + * Disconnect an MCP server. + */ + public disconnect( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/disconnect", ...options, ...params, }) } + + private _app?: App2 + get app(): App2 { + return (this._app ??= new App2({ client: this.client })) + } + + private _auth?: Auth2 + get auth(): Auth2 { + return (this._auth ??= new Auth2({ client: this.client })) + } } -export class Diff extends HeyApiClient { +export class Find extends HeyApiClient { /** - * Get raw VCS diff + * Find text * - * Retrieve a raw patch for current uncommitted changes. + * Search for text patterns across files in the project using ripgrep. */ - public raw( - parameters?: { + public text( + parameters: { directory?: string workspace?: string + pattern: string }, options?: Options, ) { @@ -2171,28 +2332,31 @@ export class Diff extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "query", key: "pattern" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/vcs/diff/raw", + return (options?.client ?? this.client).get({ + url: "/find", ...options, ...params, }) } -} -export class Vcs extends HeyApiClient { /** - * Get VCS info + * Find files * - * Retrieve version control system (VCS) information for the current project, such as git branch. + * Search for files or directories by name or pattern in the project directory. */ - public get( - parameters?: { + public files( + parameters: { directory?: string workspace?: string + query: string + dirs?: "true" | "false" + type?: "file" | "directory" + limit?: number }, options?: Options, ) { @@ -2203,26 +2367,31 @@ export class Vcs extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "query", key: "query" }, + { in: "query", key: "dirs" }, + { in: "query", key: "type" }, + { in: "query", key: "limit" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/vcs", + return (options?.client ?? this.client).get({ + url: "/find/file", ...options, ...params, }) } /** - * Get VCS status + * Find symbols * - * Retrieve changed files in the current working tree without patches. + * Search for workspace symbols like functions, classes, and variables using LSP. */ - public status( - parameters?: { + public symbols( + parameters: { directory?: string workspace?: string + query: string }, options?: Options, ) { @@ -2233,28 +2402,30 @@ export class Vcs extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "query", key: "query" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/vcs/status", + return (options?.client ?? this.client).get({ + url: "/find/symbol", ...options, ...params, }) } +} +export class File extends HeyApiClient { /** - * Get VCS diff + * List files * - * Retrieve the current git diff for the working tree or against the default branch. + * List files and directories in a specified path. */ - public diff( + public list( parameters: { directory?: string workspace?: string - mode: "git" | "branch" - context?: number + path: string }, options?: Options, ) { @@ -2265,29 +2436,28 @@ export class Vcs extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "mode" }, - { in: "query", key: "context" }, + { in: "query", key: "path" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/vcs/diff", + return (options?.client ?? this.client).get({ + url: "/file", ...options, ...params, }) } /** - * Apply VCS patch + * Read file * - * Apply a raw patch to the current working tree. + * Read the content of a specified file. */ - public apply( - parameters?: { + public read( + parameters: { directory?: string workspace?: string - patch?: string + path: string }, options?: Options, ) { @@ -2298,36 +2468,24 @@ export class Vcs extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "patch" }, + { in: "query", key: "path" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/vcs/apply", + return (options?.client ?? this.client).get({ + url: "/file/content", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } - private _diff?: Diff - get diff2(): Diff { - return (this._diff ??= new Diff({ client: this.client })) - } -} - -export class Command extends HeyApiClient { /** - * List commands + * Get file status * - * Get a list of all available commands in the RedrobCode system. + * Get the git status of all files in the project. */ - public list( + public status( parameters?: { directory?: string workspace?: string @@ -2345,21 +2503,21 @@ export class Command extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ - url: "/command", + return (options?.client ?? this.client).get({ + url: "/file/status", ...options, ...params, }) } } -export class Lsp extends HeyApiClient { +export class Instance extends HeyApiClient { /** - * Get LSP status + * Dispose instance * - * Get LSP server status + * Clean up and dispose the current RedrobCode instance, releasing all resources. */ - public status( + public dispose( parameters?: { directory?: string workspace?: string @@ -2377,21 +2535,21 @@ export class Lsp extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ - url: "/lsp", + return (options?.client ?? this.client).post({ + url: "/instance/dispose", ...options, ...params, }) } } -export class Formatter extends HeyApiClient { +export class Path extends HeyApiClient { /** - * Get formatter status + * Get paths * - * Get formatter status + * Retrieve the current working directory and related path information for the RedrobCode instance. */ - public status( + public get( parameters?: { directory?: string workspace?: string @@ -2409,23 +2567,22 @@ export class Formatter extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ - url: "/formatter", + return (options?.client ?? this.client).get({ + url: "/path", ...options, ...params, }) } } -export class Auth2 extends HeyApiClient { +export class Diff extends HeyApiClient { /** - * Remove MCP OAuth + * Get raw VCS diff * - * Remove OAuth credentials for an MCP server. + * Retrieve a raw patch for current uncommitted changes. */ - public remove( - parameters: { - name: string + public raw( + parameters?: { directory?: string workspace?: string }, @@ -2436,28 +2593,28 @@ export class Auth2 extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).delete({ - url: "/mcp/{name}/auth", + return (options?.client ?? this.client).get({ + url: "/vcs/diff/raw", ...options, ...params, }) } +} +export class Vcs extends HeyApiClient { /** - * Start MCP OAuth + * Get VCS info * - * Start OAuth authentication flow for a Model Context Protocol (MCP) server. + * Retrieve version control system (VCS) information for the current project, such as git branch. */ - public start( - parameters: { - name: string + public get( + parameters?: { directory?: string workspace?: string }, @@ -2468,31 +2625,28 @@ export class Auth2 extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/mcp/{name}/auth", + return (options?.client ?? this.client).get({ + url: "/vcs", ...options, ...params, }) } /** - * Complete MCP OAuth + * Get VCS status * - * Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code. + * Retrieve changed files in the current working tree without patches. */ - public callback( - parameters: { - name: string + public status( + parameters?: { directory?: string workspace?: string - code?: string }, options?: Options, ) { @@ -2501,36 +2655,30 @@ export class Auth2 extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "code" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/mcp/{name}/auth/callback", + return (options?.client ?? this.client).get({ + url: "/vcs/status", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } /** - * Authenticate MCP OAuth + * Get VCS diff * - * Start OAuth flow and wait for callback (opens browser). + * Retrieve the current git diff for the working tree or against the default branch. */ - public authenticate( + public diff( parameters: { - name: string directory?: string workspace?: string + mode: "git" | "branch" + context?: number }, options?: Options, ) { @@ -2539,33 +2687,31 @@ export class Auth2 extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "query", key: "mode" }, + { in: "query", key: "context" }, ], }, ], ) - return (options?.client ?? this.client).post( - { - url: "/mcp/{name}/auth/authenticate", - ...options, - ...params, - }, - ) + return (options?.client ?? this.client).get({ + url: "/vcs/diff", + ...options, + ...params, + }) } -} -export class Mcp extends HeyApiClient { /** - * Get MCP status + * Apply VCS patch * - * Get the status of all Model Context Protocol (MCP) servers. + * Apply a raw patch to the current working tree. */ - public status( + public apply( parameters?: { directory?: string workspace?: string + patch?: string }, options?: Options, ) { @@ -2576,28 +2722,39 @@ export class Mcp extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "patch" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/mcp", + return (options?.client ?? this.client).post({ + url: "/vcs/apply", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } + private _diff?: Diff + get diff2(): Diff { + return (this._diff ??= new Diff({ client: this.client })) + } +} + +export class Command extends HeyApiClient { /** - * Add MCP server + * List commands * - * Dynamically add a new Model Context Protocol (MCP) server to the system. + * Get a list of all available commands in the RedrobCode system. */ - public add( + public list( parameters?: { directory?: string workspace?: string - name?: string - config?: McpLocalConfig | McpRemoteConfig }, options?: Options, ) { @@ -2608,30 +2765,26 @@ export class Mcp extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "name" }, - { in: "body", key: "config" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/mcp", + return (options?.client ?? this.client).get({ + url: "/command", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } +} +export class Lsp extends HeyApiClient { /** - * Connect an MCP server. + * Get LSP status + * + * Get LSP server status */ - public connect( - parameters: { - name: string + public status( + parameters?: { directory?: string workspace?: string }, @@ -2642,26 +2795,28 @@ export class Mcp extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/mcp/{name}/connect", + return (options?.client ?? this.client).get({ + url: "/lsp", ...options, ...params, }) } +} +export class Formatter extends HeyApiClient { /** - * Disconnect an MCP server. + * Get formatter status + * + * Get formatter status */ - public disconnect( - parameters: { - name: string + public status( + parameters?: { directory?: string workspace?: string }, @@ -2672,24 +2827,18 @@ export class Mcp extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/mcp/{name}/disconnect", + return (options?.client ?? this.client).get({ + url: "/formatter", ...options, ...params, }) } - - private _auth?: Auth2 - get auth(): Auth2 { - return (this._auth ??= new Auth2({ client: this.client })) - } } export class Memory extends HeyApiClient { @@ -8297,6 +8446,11 @@ export class RedrobClient extends HeyApiClient { return (this._worktree ??= new Worktree({ client: this.client })) } + private _mcp?: Mcp + get mcp(): Mcp { + return (this._mcp ??= new Mcp({ client: this.client })) + } + private _find?: Find get find(): Find { return (this._find ??= new Find({ client: this.client })) @@ -8337,11 +8491,6 @@ export class RedrobClient extends HeyApiClient { return (this._formatter ??= new Formatter({ client: this.client })) } - private _mcp?: Mcp - get mcp(): Mcp { - return (this._mcp ??= new Mcp({ client: this.client })) - } - private _memory?: Memory get memory(): Memory { return (this._memory ??= new Memory({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 115563b6f0..14f344d1e6 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2432,6 +2432,24 @@ export type McpResource = { client: string } +export type ExtensionCallResponse = { + ok: boolean + result?: { + b64?: string + mime?: string + path?: string + model?: string + costUsd?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + } + error?: string +} + +export type McpAppResource = { + html: string + mimeType?: string + server: string +} + export type Symbol = { name: string kind: number @@ -8728,6 +8746,121 @@ export type ExperimentalResourceListResponses = { export type ExperimentalResourceListResponse = ExperimentalResourceListResponses[keyof ExperimentalResourceListResponses] +export type ExperimentalExtensionsCallData = { + body?: { + extensionId: string + action: string + args?: { + [key: string]: unknown + } + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/extensions/call" +} + +export type ExperimentalExtensionsCallErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalExtensionsCallError = ExperimentalExtensionsCallErrors[keyof ExperimentalExtensionsCallErrors] + +export type ExperimentalExtensionsCallResponses = { + /** + * The extension's answer + */ + 200: ExtensionCallResponse +} + +export type ExperimentalExtensionsCallResponse = + ExperimentalExtensionsCallResponses[keyof ExperimentalExtensionsCallResponses] + +export type McpAppResolveData = { + body?: { + /** + * MCP server name, or the prefix of its projected tool names + */ + server: string + /** + * The ui:// resource the tool result pointed at + */ + uri: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/mcp-apps/resolve" +} + +export type McpAppResolveErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UpstreamError + */ + 502: UpstreamError +} + +export type McpAppResolveError = McpAppResolveErrors[keyof McpAppResolveErrors] + +export type McpAppResolveResponses = { + /** + * The app view's HTML + */ + 200: McpAppResource +} + +export type McpAppResolveResponse = McpAppResolveResponses[keyof McpAppResolveResponses] + +export type McpAppCallData = { + body?: { + server: string + /** + * Tool name as the MCP server knows it + */ + name: string + arguments?: { + [key: string]: unknown + } + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/mcp-apps/call" +} + +export type McpAppCallErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UpstreamError + */ + 502: UpstreamError +} + +export type McpAppCallError = McpAppCallErrors[keyof McpAppCallErrors] + +export type McpAppCallResponses = { + /** + * The MCP tool result + */ + 200: unknown +} + export type FindTextData = { body?: never path?: never diff --git a/packages/sdk/js/src/v2/server.ts b/packages/sdk/js/src/v2/server.ts index 578808df7d..b0a566b80d 100644 --- a/packages/sdk/js/src/v2/server.ts +++ b/packages/sdk/js/src/v2/server.ts @@ -33,6 +33,7 @@ export async function createRedrobServer(options?: ServerOptions) { if (options.config?.logLevel) args.push(`--log-level=${options.config.logLevel}`) const proc = launch(`redrob`, args, { + windowsHide: true, env: { ...process.env, REDROB_CONFIG_CONTENT: JSON.stringify(options.config ?? {}),