File size: 5,153 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 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | import {
FileClient,
PluginsClient,
} from "@openhands/typescript-client/clients";
import { getActiveBackend } from "./backend-registry/active-store";
import { getAgentServerClientOptions } from "./agent-server-client-options";
/** Summary of a skill bundled in a plugin (agent-server `PluginSkillSummary`). */
export interface PluginBundledSkill {
name: string;
description?: string | null;
}
/**
* A plugin in the dynamic marketplace catalog, with attachable coordinates and
* install state. Matches the agent-server `MarketplacePluginInfo` / the
* typescript-client `MarketplacePlugin`. The contents fields (`path`, `skills`,
* `files`) are populated when the entry resolves to a directory in the
* server's local marketplace clone, and are absent on older agent-servers.
*/
export interface MarketplacePlugin {
name: string;
description: string | null;
source: string;
ref?: string | null;
repo_path?: string | null;
installed: boolean;
path?: string | null;
skills?: PluginBundledSkill[] | null;
files?: string[] | null;
}
/**
* A locally-discovered ("ambient") plugin reported by the agent-server — one
* found in the user's local plugin directories (e.g. `~/.agents/plugins`).
* These auto-load into conversations and are not managed via install/uninstall,
* so the Plugins page renders them as a read-only "Local" group. Matches the
* typescript-client `PluginInfo`; the contents fields are absent on older
* agent-servers.
*/
export interface LocalPlugin {
name: string;
version: string;
description: string;
path?: string;
skills?: PluginBundledSkill[];
files?: string[];
}
/** Content of a single plugin file fetched for the detail-modal viewer. */
export interface PluginFileContent {
kind: "text" | "binary";
text: string | null;
}
function isLikelyBinary(buffer: ArrayBuffer): boolean {
// Same heuristic git uses: presence of a NUL byte in the first ~8KB. Small
// private copy of `isLikelyBinary` in `use-workspace-file-content.ts` — that
// module is conversation-workspace-specific and heavy to import from here.
const view = new Uint8Array(buffer, 0, Math.min(buffer.byteLength, 8000));
for (let i = 0; i < view.length; i += 1) {
if (view[i] === 0) return true;
}
return false;
}
class PluginsService {
/**
* Fetch the dynamic plugins marketplace catalog.
*
* Local backend only for now: the catalog is fetched at run time from the
* agent-server via the typed client (no bundled catalog, so the list stays
* dynamic). On a cloud backend an empty catalog is returned — there is no
* cloud plugins-marketplace endpoint yet (tracked as a follow-up ticket).
*/
static async getPluginsMarketplace(): Promise<MarketplacePlugin[]> {
if (getActiveBackend().backend.kind === "cloud") {
return [];
}
try {
const response = await new PluginsClient(
getAgentServerClientOptions(),
).getPluginsMarketplace();
return (response.plugins ?? []) as MarketplacePlugin[];
} catch {
// Agent-server may not support the plugins endpoint or be unreachable;
// surface an empty catalog rather than throwing.
return [];
}
}
/**
* Fetch the locally-discovered ("ambient") plugins from the agent-server.
*
* Only user-level plugins are requested (`~/.agents/plugins`,
* `~/.openhands/plugins`, plus enabled installed plugins): the Plugins page is
* global, so there is no project workspace to scope project plugins to.
*
* Local backend only — a cloud backend has no local plugin directories, so an
* empty list is returned. Errors surface as an empty list (mirrors the
* catalog) rather than throwing.
*/
static async getLocalPlugins(): Promise<LocalPlugin[]> {
if (getActiveBackend().backend.kind === "cloud") {
return [];
}
try {
const response = await new PluginsClient(
getAgentServerClientOptions(),
).getPlugins({ load_user: true, load_project: false });
return (response.plugins ?? []) as LocalPlugin[];
} catch {
return [];
}
}
/**
* Fetch one plugin file's content for the detail-modal viewer. `basePath` is
* the plugin directory reported by the agent-server (`path`/`install_path`)
* and `relativePath` a POSIX path from the plugin's `files` listing.
*
* Local backend only — plugin files live on the local agent-server's disk.
* Errors propagate so the caller can render a load-error state.
*/
static async getPluginFileContent(
basePath: string,
relativePath: string,
): Promise<PluginFileContent> {
if (getActiveBackend().backend.kind === "cloud") {
throw new Error(
"Reading plugin files is only available on a local backend.",
);
}
const buffer = await new FileClient(
getAgentServerClientOptions(),
).downloadFile(`${basePath}/${relativePath}`);
if (isLikelyBinary(buffer)) {
return { kind: "binary", text: null };
}
return {
kind: "text",
text: new TextDecoder("utf-8", { fatal: false }).decode(buffer),
};
}
}
export default PluginsService;
|