diff --git a/sdk/packages/core/src/hub/client/plugin-commands.ts b/sdk/packages/core/src/hub/client/plugin-commands.ts index df7692733f..40e509cc21 100644 --- a/sdk/packages/core/src/hub/client/plugin-commands.ts +++ b/sdk/packages/core/src/hub/client/plugin-commands.ts @@ -1,9 +1,33 @@ +import type { HubCommandEnvelope, HubReplyEnvelope } from "@cline/shared"; import type { PluginCommandCatalog, PluginCommandResult, PluginCommandsApi, } from "../../services/plugin-command-api"; -import type { NodeHubClient } from "./index"; +import { HubCommandError, type NodeHubClient } from "./index"; + +/** + * A Hub that predates plugin command RPCs, kept alive while it has active + * work, answers unsupported_command. Resolves undefined for that Hub. + */ +async function commandIfSupported( + client: Pick, + command: HubCommandEnvelope["command"], + payload: Record, + sessionId: string | undefined, + options: Parameters[3], +): Promise { + try { + return await client.command(command, payload, sessionId, options); + } catch (error) { + if ( + error instanceof HubCommandError && + error.code === "unsupported_command" + ) + return undefined; + throw error; + } +} /** Uses the caller's existing connection and subscription lifecycle. */ export function createHubPluginCommandsApi( @@ -11,12 +35,14 @@ export function createHubPluginCommandsApi( ): PluginCommandsApi { return { async list(target) { - const reply = await client.command( + const reply = await commandIfSupported( + client, "plugins.commands.list", { ...target }, target.sessionId, { timeoutMs: null }, ); + if (!reply) return { ...target, status: "ready", commands: [] }; if (!reply.ok) throw new Error( reply.error?.message ?? "Plugin command discovery failed", @@ -24,12 +50,14 @@ export function createHubPluginCommandsApi( return reply.payload?.catalog as unknown as PluginCommandCatalog; }, async run(input) { - const reply = await client.command( + const reply = await commandIfSupported( + client, "plugins.commands.run", { ...input }, input.sessionId, { timeoutMs: null, retryAfterDispatch: false }, ); + if (!reply) return undefined; if (!reply.ok) throw new Error( reply.error?.message ?? "Plugin command execution failed", diff --git a/sdk/packages/core/src/hub/server/plugin-commands.test.ts b/sdk/packages/core/src/hub/server/plugin-commands.test.ts index 416b483648..71a5f93815 100644 --- a/sdk/packages/core/src/hub/server/plugin-commands.test.ts +++ b/sdk/packages/core/src/hub/server/plugin-commands.test.ts @@ -7,6 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { LocalRuntimeHost } from "../../runtime/host/local-runtime-host"; import { splitCoreSessionConfig } from "../../runtime/host/runtime-host"; import { FileSessionService } from "../../session/services/file-session-service"; +import { HubCommandError } from "../client"; import { createHubPluginCommandsApi } from "../client/plugin-commands"; import { createLocalHubScheduleRuntimeHandlers } from "../daemon/runtime-handlers"; import { HubServerTransport } from "./hub-server-transport"; @@ -265,6 +266,26 @@ api.registerCommand({name:'recovered',handler:()=> 'ready'}); }); stop(); }); + it("treats a Hub without plugin command RPCs as having no plugin commands", async () => { + const api = createHubPluginCommandsApi({ + command: async (name) => { + throw new HubCommandError( + name, + "unsupported_command", + `Unsupported hub schedule command: ${name}`, + ); + }, + subscribe: () => () => {}, + }); + expect(await api.list({ workspacePath: root })).toEqual({ + workspacePath: root, + status: "ready", + commands: [], + }); + expect( + await api.run({ workspacePath: root, prompt: "/counter" }), + ).toBeUndefined(); + }); it("shares a workspace sandbox across client requests and preserves arguments", async () => { const first = await command("plugins.commands.list", { workspacePath: root,