File size: 4,581 Bytes
63522a5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | import { PluginsClient } from "@openhands/typescript-client/clients";
import { getActiveBackend } from "./backend-registry/active-store";
import { getAgentServerClientOptions } from "./agent-server-client-options";
import type { PluginBundledSkill } from "./plugins-service";
/**
* An installed plugin, as returned by the agent-server management router
* (`GET /api/plugins/installed`). Matches the typescript-client
* `InstalledPluginInfo`. The contents fields (`skills`, `files`, relative to
* `install_path`) are absent on older agent-servers and null when the
* installed plugin directory failed to load.
*/
export interface InstalledPluginInfo {
name: string;
version: string;
description: string | null;
enabled: boolean;
source: string;
resolved_ref?: string | null;
repo_path?: string | null;
installed_at: string;
install_path: string;
skills?: PluginBundledSkill[] | null;
files?: string[] | null;
}
/** Coordinates for installing a plugin from a git source or local path. */
export interface InstallPluginRequest {
source: string;
ref?: string | null;
repo_path?: string | null;
force?: boolean;
}
/**
* The slice of the typescript-client `PluginsClient` this service drives. The
* installed `@openhands/typescript-client` package does not yet export these
* management methods (they ship in typescript-client PRs #222/#223); narrowing
* to this local interface keeps `vitest` (types stripped) and `eslint` green
* until the client is republished, at which point the cast below can be dropped.
*/
interface PluginsManagementClient {
listInstalledPlugins(): Promise<{ plugins: InstalledPluginInfo[] }>;
installPlugin(request: InstallPluginRequest): Promise<InstalledPluginInfo>;
setPluginEnabled(
name: string,
enabled: boolean,
): Promise<{ name: string; enabled: boolean }>;
uninstallPlugin(name: string): Promise<{ message: string }>;
refreshPlugin(
name: string,
): Promise<{ message: string; plugin: InstalledPluginInfo }>;
}
function isCloudBackend(): boolean {
return getActiveBackend().backend.kind === "cloud";
}
function getManagementClient(): PluginsManagementClient {
return new PluginsClient(
getAgentServerClientOptions(),
) as unknown as PluginsManagementClient;
}
/**
* Front-end management layer for installed plugins: list / install / enable /
* disable / uninstall / refresh. Kept separate from the read-only catalog
* service (`plugins-service.ts`), exactly as skills separate the marketplace
* catalog from install actions.
*
* Local backend only for now (per Appendix C Q5): installed plugins live on the
* local agent-server's `~/.openhands/plugins/installed/`. A cloud backend has no
* per-user installed store yet, so reads return an empty list and mutating
* actions throw (the UI also disables them on cloud).
*/
class PluginsManagementService {
static async listInstalledPlugins(): Promise<InstalledPluginInfo[]> {
if (isCloudBackend()) {
return [];
}
try {
const response = await getManagementClient().listInstalledPlugins();
return response.plugins ?? [];
} catch {
// Agent-server may predate the plugins router or be unreachable; surface
// an empty list rather than throwing (mirrors the catalog service).
return [];
}
}
static async installPlugin(
request: InstallPluginRequest,
): Promise<InstalledPluginInfo> {
if (isCloudBackend()) {
throw new Error(
"Installing plugins is only available on a local backend.",
);
}
return getManagementClient().installPlugin(request);
}
static async setPluginEnabled(
name: string,
enabled: boolean,
): Promise<{ name: string; enabled: boolean }> {
if (isCloudBackend()) {
throw new Error(
"Enabling and disabling plugins is only available on a local backend.",
);
}
return getManagementClient().setPluginEnabled(name, enabled);
}
static async uninstallPlugin(name: string): Promise<{ message: string }> {
if (isCloudBackend()) {
throw new Error(
"Uninstalling plugins is only available on a local backend.",
);
}
return getManagementClient().uninstallPlugin(name);
}
static async refreshPlugin(
name: string,
): Promise<{ message: string; plugin: InstalledPluginInfo }> {
if (isCloudBackend()) {
throw new Error(
"Refreshing plugins is only available on a local backend.",
);
}
return getManagementClient().refreshPlugin(name);
}
}
export default PluginsManagementService;
|