feat: add slash plugin installs

This commit is contained in:
Peter Steinberger
2026-03-23 00:18:47 -07:00
parent deecf68b59
commit abbd1b6b8a
8 changed files with 635 additions and 48 deletions

View File

@@ -45,6 +45,17 @@ with OpenClaw), others are **external** (published on npm by the community).
</Step>
</Steps>
If you prefer chat-native control, enable `commands.plugins: true` and use:
```text
/plugin install clawhub:@openclaw/voice-call
/plugin show voice-call
/plugin enable voice-call
```
The install path uses the same resolver as the CLI: local path/archive, explicit
`clawhub:<pkg>`, or bare package spec (ClawHub first, then npm fallback).
## Plugin types
OpenClaw recognizes two plugin formats:

View File

@@ -62,7 +62,7 @@ They run immediately, are stripped before the model sees the message, and the re
- `commands.bashForegroundMs` (default `2000`) controls how long bash waits before switching to background mode (`0` backgrounds immediately).
- `commands.config` (default `false`) enables `/config` (reads/writes `openclaw.json`).
- `commands.mcp` (default `false`) enables `/mcp` (reads/writes OpenClaw-managed MCP config under `mcp.servers`).
- `commands.plugins` (default `false`) enables `/plugins` (plugin discovery/status plus enable/disable toggles).
- `commands.plugins` (default `false`) enables `/plugins` (plugin discovery/status plus install + enable/disable controls).
- `commands.debug` (default `false`) enables `/debug` (runtime-only overrides).
- `commands.allowFrom` (optional) sets a per-provider allowlist for command authorization. When configured, it is the
only authorization source for commands and directives (channel allowlists/pairing and `commands.useAccessGroups`
@@ -95,8 +95,9 @@ Text + native (when enabled):
- `/tell <id|#> <message>` (alias for `/steer`)
- `/config show|get|set|unset` (persist config to disk, owner-only; requires `commands.config: true`)
- `/mcp show|get|set|unset` (manage OpenClaw MCP server config, owner-only; requires `commands.mcp: true`)
- `/plugins list|show|get|enable|disable` (inspect discovered plugins and toggle enablement, owner-only for writes; requires `commands.plugins: true`)
- `/plugins list|show|get|install|enable|disable` (inspect discovered plugins, install new ones, and toggle enablement; owner-only for writes; requires `commands.plugins: true`)
- `/plugin` is an alias for `/plugins`.
- `/plugin install <spec>` accepts the same plugin specs as `openclaw plugins install`: local path/archive, npm package, or `clawhub:<pkg>`.
- Enable/disable writes still reply with a restart hint. On a watched foreground gateway, OpenClaw may perform that restart automatically right after the write.
- `/debug show|set|unset|reset` (runtime overrides, owner-only; requires `commands.debug: true`)
- `/usage off|tokens|full|cost` (per-response usage footer or local cost summary)

View File

@@ -503,6 +503,70 @@ gateway_log="/tmp/openclaw-plugin-command-e2e.log"
start_gateway "$gateway_log"
wait_for_gateway_health
echo "Testing /plugin install with auto-restart..."
slash_install_dir="$(mktemp -d "/tmp/openclaw-plugin-slash-install.XXXXXX")"
cat > "$slash_install_dir/package.json" <<'JSON'
{
"name": "@openclaw/slash-install-plugin",
"version": "0.0.1",
"openclaw": { "extensions": ["./index.js"] }
}
JSON
cat > "$slash_install_dir/index.js" <<'JS'
module.exports = {
id: "slash-install-plugin",
name: "Slash Install Plugin",
register(api) {
api.registerGatewayMethod("demo.slash.install", async () => ({ ok: true }));
},
};
JS
cat > "$slash_install_dir/openclaw.plugin.json" <<'JSON'
{
"id": "slash-install-plugin",
"configSchema": {
"type": "object",
"properties": {}
}
}
JSON
run_gateway_chat_json \
"plugin-e2e-install" \
"/plugin install $slash_install_dir" \
/tmp/plugin-command-install.json \
30000
node - <<'NODE'
const fs = require("node:fs");
const payload = JSON.parse(fs.readFileSync("/tmp/plugin-command-install.json", "utf8"));
const text = payload.text || "";
if (!text.includes('Installed plugin "slash-install-plugin"')) {
throw new Error(`expected install confirmation, got:\n${text}`);
}
if (!text.includes("Restart the gateway to load plugins.")) {
throw new Error(`expected restart hint, got:\n${text}`);
}
console.log("ok");
NODE
wait_for_gateway_health
run_gateway_chat_json "plugin-e2e-install-show" "/plugin show slash-install-plugin" /tmp/plugin-command-install-show.json
node - <<'NODE'
const fs = require("node:fs");
const payload = JSON.parse(fs.readFileSync("/tmp/plugin-command-install-show.json", "utf8"));
const text = payload.text || "";
if (!text.includes('"status": "loaded"')) {
throw new Error(`expected loaded status after slash install, got:\n${text}`);
}
if (!text.includes('"enabled": true')) {
throw new Error(`expected enabled status after slash install, got:\n${text}`);
}
if (!text.includes('"demo.slash.install"')) {
throw new Error(`expected installed gateway method, got:\n${text}`);
}
console.log("ok");
NODE
run_gateway_chat_json "plugin-e2e-list" "/plugin list" /tmp/plugin-command-list.json
node - <<'NODE'
const fs = require("node:fs");

View File

@@ -0,0 +1,201 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { withTempHome } from "../../config/home-env.test-harness.js";
import { handleCommands } from "./commands-core.js";
import { createCommandWorkspaceHarness } from "./commands-filesystem.test-support.js";
import { buildCommandTestParams } from "./commands.test-harness.js";
const installPluginFromPathMock = vi.fn();
const installPluginFromClawHubMock = vi.fn();
const persistPluginInstallMock = vi.fn();
vi.mock("../../plugins/install.js", async () => {
const actual = await vi.importActual<typeof import("../../plugins/install.js")>(
"../../plugins/install.js",
);
return {
...actual,
installPluginFromPath: installPluginFromPathMock,
};
});
vi.mock("../../plugins/clawhub.js", async () => {
const actual = await vi.importActual<typeof import("../../plugins/clawhub.js")>(
"../../plugins/clawhub.js",
);
return {
...actual,
installPluginFromClawHub: installPluginFromClawHubMock,
};
});
vi.mock("../../cli/plugins-install-persist.js", () => ({
persistPluginInstall: persistPluginInstallMock,
}));
const workspaceHarness = createCommandWorkspaceHarness("openclaw-command-plugins-install-");
describe("handleCommands /plugins install", () => {
afterEach(async () => {
installPluginFromPathMock.mockReset();
installPluginFromClawHubMock.mockReset();
persistPluginInstallMock.mockReset();
await workspaceHarness.cleanupWorkspaces();
});
it("installs a plugin from a local path", async () => {
installPluginFromPathMock.mockResolvedValue({
ok: true,
pluginId: "path-install-plugin",
targetDir: "/tmp/path-install-plugin",
version: "0.0.1",
extensions: ["index.js"],
});
persistPluginInstallMock.mockResolvedValue({});
await withTempHome("openclaw-command-plugins-home-", async () => {
const workspaceDir = await workspaceHarness.createWorkspace();
const pluginDir = path.join(workspaceDir, "fixtures", "path-install-plugin");
await fs.mkdir(pluginDir, { recursive: true });
const params = buildCommandTestParams(
`/plugins install ${pluginDir}`,
{
commands: {
text: true,
plugins: true,
},
},
undefined,
{ workspaceDir },
);
params.command.senderIsOwner = true;
const result = await handleCommands(params);
expect(result.reply?.text).toContain('Installed plugin "path-install-plugin"');
expect(installPluginFromPathMock).toHaveBeenCalledWith(
expect.objectContaining({
path: pluginDir,
}),
);
expect(persistPluginInstallMock).toHaveBeenCalledWith(
expect.objectContaining({
pluginId: "path-install-plugin",
install: expect.objectContaining({
source: "path",
sourcePath: pluginDir,
installPath: "/tmp/path-install-plugin",
version: "0.0.1",
}),
}),
);
});
});
it("installs from an explicit clawhub: spec", async () => {
installPluginFromClawHubMock.mockResolvedValue({
ok: true,
pluginId: "clawhub-demo",
targetDir: "/tmp/clawhub-demo",
version: "1.2.3",
extensions: ["index.js"],
packageName: "@openclaw/clawhub-demo",
clawhub: {
source: "clawhub",
clawhubUrl: "https://clawhub.ai",
clawhubPackage: "@openclaw/clawhub-demo",
clawhubFamily: "code-plugin",
clawhubChannel: "official",
version: "1.2.3",
integrity: "sha512-demo",
resolvedAt: "2026-03-22T12:00:00.000Z",
},
});
persistPluginInstallMock.mockResolvedValue({});
await withTempHome("openclaw-command-plugins-home-", async () => {
const workspaceDir = await workspaceHarness.createWorkspace();
const params = buildCommandTestParams(
"/plugins install clawhub:@openclaw/clawhub-demo@1.2.3",
{
commands: {
text: true,
plugins: true,
},
},
undefined,
{ workspaceDir },
);
params.command.senderIsOwner = true;
const result = await handleCommands(params);
expect(result.reply?.text).toContain('Installed plugin "clawhub-demo"');
expect(installPluginFromClawHubMock).toHaveBeenCalledWith(
expect.objectContaining({
spec: "clawhub:@openclaw/clawhub-demo@1.2.3",
}),
);
expect(persistPluginInstallMock).toHaveBeenCalledWith(
expect.objectContaining({
pluginId: "clawhub-demo",
install: expect.objectContaining({
source: "clawhub",
spec: "clawhub:@openclaw/clawhub-demo@1.2.3",
installPath: "/tmp/clawhub-demo",
version: "1.2.3",
integrity: "sha512-demo",
clawhubPackage: "@openclaw/clawhub-demo",
clawhubChannel: "official",
}),
}),
);
});
});
it("treats /plugin add as an install alias", async () => {
installPluginFromClawHubMock.mockResolvedValue({
ok: true,
pluginId: "alias-demo",
targetDir: "/tmp/alias-demo",
version: "1.0.0",
extensions: ["index.js"],
packageName: "@openclaw/alias-demo",
clawhub: {
source: "clawhub",
clawhubUrl: "https://clawhub.ai",
clawhubPackage: "@openclaw/alias-demo",
clawhubFamily: "code-plugin",
clawhubChannel: "official",
version: "1.0.0",
integrity: "sha512-alias",
resolvedAt: "2026-03-23T12:00:00.000Z",
},
});
persistPluginInstallMock.mockResolvedValue({});
await withTempHome("openclaw-command-plugins-home-", async () => {
const workspaceDir = await workspaceHarness.createWorkspace();
const params = buildCommandTestParams(
"/plugin add clawhub:@openclaw/alias-demo@1.0.0",
{
commands: {
text: true,
plugins: true,
},
},
undefined,
{ workspaceDir },
);
params.command.senderIsOwner = true;
const result = await handleCommands(params);
expect(result.reply?.text).toContain('Installed plugin "alias-demo"');
expect(installPluginFromClawHubMock).toHaveBeenCalledWith(
expect.objectContaining({
spec: "clawhub:@openclaw/alias-demo@1.0.0",
}),
);
});
});
});

View File

@@ -23,6 +23,9 @@ async function createClaudeBundlePlugin(params: { workspaceDir: string; pluginId
function buildCfg(): OpenClawConfig {
return {
plugins: {
enabled: true,
},
commands: {
text: true,
plugins: true,
@@ -81,50 +84,6 @@ describe("handleCommands /plugins", () => {
});
});
it("enables and disables a discovered plugin", async () => {
await withTempHome("openclaw-command-plugins-home-", async () => {
const workspaceDir = await workspaceHarness.createWorkspace();
await createClaudeBundlePlugin({ workspaceDir, pluginId: "superpowers" });
const enableParams = buildCommandTestParams(
"/plugins enable superpowers",
buildCfg(),
undefined,
{
workspaceDir,
},
);
enableParams.command.senderIsOwner = true;
const enableResult = await handleCommands(enableParams);
expect(enableResult.reply?.text).toContain('Plugin "superpowers" enabled');
const showEnabledParams = buildCommandTestParams(
"/plugins show superpowers",
buildCfg(),
undefined,
{
workspaceDir,
},
);
showEnabledParams.command.senderIsOwner = true;
const showEnabledResult = await handleCommands(showEnabledParams);
expect(showEnabledResult.reply?.text).toContain('"status": "loaded"');
expect(showEnabledResult.reply?.text).toContain('"enabled": true');
const disableParams = buildCommandTestParams(
"/plugins disable superpowers",
buildCfg(),
undefined,
{
workspaceDir,
},
);
disableParams.command.senderIsOwner = true;
const disableResult = await handleCommands(disableParams);
expect(disableResult.reply?.text).toContain('Plugin "superpowers" disabled');
});
});
it("rejects internal writes without operator.admin", async () => {
await withTempHome("openclaw-command-plugins-home-", async () => {
const workspaceDir = await workspaceHarness.createWorkspace();

View File

@@ -0,0 +1,169 @@
import { afterEach, describe, expect, it, vi } from "vitest";
const {
readConfigFileSnapshotMock,
validateConfigObjectWithPluginsMock,
writeConfigFileMock,
buildPluginStatusReportMock,
} = vi.hoisted(() => ({
readConfigFileSnapshotMock: vi.fn(),
validateConfigObjectWithPluginsMock: vi.fn(),
writeConfigFileMock: vi.fn(),
buildPluginStatusReportMock: vi.fn(),
}));
vi.mock("../../config/config.js", async () => {
const actual =
await vi.importActual<typeof import("../../config/config.js")>("../../config/config.js");
return {
...actual,
readConfigFileSnapshot: readConfigFileSnapshotMock,
validateConfigObjectWithPlugins: validateConfigObjectWithPluginsMock,
writeConfigFile: writeConfigFileMock,
};
});
vi.mock("../../plugins/status.js", async () => {
const actual =
await vi.importActual<typeof import("../../plugins/status.js")>("../../plugins/status.js");
return {
...actual,
buildPluginStatusReport: buildPluginStatusReportMock,
};
});
import { handleCommands } from "./commands-core.js";
import { buildCommandTestParams } from "./commands.test-harness.js";
function buildCfg() {
return {
plugins: {
enabled: true,
},
commands: {
text: true,
plugins: true,
},
};
}
describe("handleCommands /plugins toggle", () => {
afterEach(() => {
readConfigFileSnapshotMock.mockReset();
validateConfigObjectWithPluginsMock.mockReset();
writeConfigFileMock.mockReset();
buildPluginStatusReportMock.mockReset();
});
it("enables a discovered plugin", async () => {
const config = buildCfg();
readConfigFileSnapshotMock.mockResolvedValue({
valid: true,
path: "/tmp/openclaw.json",
resolved: config,
});
buildPluginStatusReportMock.mockReturnValue({
workspaceDir: "/tmp/workspace",
plugins: [
{
id: "superpowers",
name: "superpowers",
format: "bundle",
source: "/tmp/workspace/.openclaw/extensions/superpowers",
origin: "workspace",
enabled: false,
status: "disabled",
toolNames: [],
hookNames: [],
channelIds: [],
providerIds: [],
speechProviderIds: [],
mediaUnderstandingProviderIds: [],
imageGenerationProviderIds: [],
webSearchProviderIds: [],
gatewayMethods: [],
cliCommands: [],
services: [],
commands: [],
httpRoutes: 0,
hookCount: 0,
configSchema: false,
},
],
diagnostics: [],
});
validateConfigObjectWithPluginsMock.mockImplementation((next) => ({ ok: true, config: next }));
writeConfigFileMock.mockResolvedValue(undefined);
const params = buildCommandTestParams("/plugins enable superpowers", buildCfg());
params.command.senderIsOwner = true;
const result = await handleCommands(params);
expect(result.reply?.text).toContain('Plugin "superpowers" enabled');
expect(writeConfigFileMock).toHaveBeenCalledWith(
expect.objectContaining({
plugins: expect.objectContaining({
entries: expect.objectContaining({
superpowers: expect.objectContaining({ enabled: true }),
}),
}),
}),
);
});
it("disables a discovered plugin", async () => {
const config = buildCfg();
readConfigFileSnapshotMock.mockResolvedValue({
valid: true,
path: "/tmp/openclaw.json",
resolved: config,
});
buildPluginStatusReportMock.mockReturnValue({
workspaceDir: "/tmp/workspace",
plugins: [
{
id: "superpowers",
name: "superpowers",
format: "bundle",
source: "/tmp/workspace/.openclaw/extensions/superpowers",
origin: "workspace",
enabled: true,
status: "loaded",
toolNames: [],
hookNames: [],
channelIds: [],
providerIds: [],
speechProviderIds: [],
mediaUnderstandingProviderIds: [],
imageGenerationProviderIds: [],
webSearchProviderIds: [],
gatewayMethods: [],
cliCommands: [],
services: [],
commands: [],
httpRoutes: 0,
hookCount: 0,
configSchema: false,
},
],
diagnostics: [],
});
validateConfigObjectWithPluginsMock.mockImplementation((next) => ({ ok: true, config: next }));
writeConfigFileMock.mockResolvedValue(undefined);
const params = buildCommandTestParams("/plugins disable superpowers", buildCfg());
params.command.senderIsOwner = true;
const result = await handleCommands(params);
expect(result.reply?.text).toContain('Plugin "superpowers" disabled');
expect(writeConfigFileMock).toHaveBeenCalledWith(
expect.objectContaining({
plugins: expect.objectContaining({
entries: expect.objectContaining({
superpowers: expect.objectContaining({ enabled: false }),
}),
}),
}),
);
});
});

View File

@@ -1,3 +1,12 @@
import fs from "node:fs";
import { buildNpmInstallRecordFields } from "../../cli/npm-resolution.js";
import {
buildPreferredClawHubSpec,
createPluginInstallLogger,
decidePreferredClawHubFallback,
resolveFileNpmSpecToLocalPath,
} from "../../cli/plugins-command-helpers.js";
import { persistPluginInstall } from "../../cli/plugins-install-persist.js";
import {
readConfigFileSnapshot,
validateConfigObjectWithPlugins,
@@ -5,6 +14,11 @@ import {
} from "../../config/config.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { PluginInstallRecord } from "../../config/types.plugins.js";
import { resolveArchiveKind } from "../../infra/archive.js";
import { parseClawHubPluginSpec } from "../../infra/clawhub.js";
import { installPluginFromClawHub } from "../../plugins/clawhub.js";
import { installPluginFromNpmSpec, installPluginFromPath } from "../../plugins/install.js";
import { clearPluginManifestRegistryCache } from "../../plugins/manifest-registry.js";
import type { PluginRecord } from "../../plugins/registry.js";
import {
buildAllPluginInspectReports,
@@ -14,6 +28,7 @@ import {
type PluginStatusReport,
} from "../../plugins/status.js";
import { setPluginEnabledInConfig } from "../../plugins/toggle-config.js";
import { resolveUserPath } from "../../utils.js";
import { isInternalMessageChannel } from "../../utils/message-channel.js";
import {
rejectNonOwnerCommand,
@@ -121,6 +136,142 @@ function findPlugin(report: PluginStatusReport, rawName: string): PluginRecord |
);
}
function looksLikeLocalPluginInstallSpec(raw: string): boolean {
return (
raw.startsWith(".") ||
raw.startsWith("~") ||
raw.startsWith("/") ||
raw.endsWith(".ts") ||
raw.endsWith(".js") ||
raw.endsWith(".mjs") ||
raw.endsWith(".cjs") ||
raw.endsWith(".tgz") ||
raw.endsWith(".tar.gz") ||
raw.endsWith(".tar") ||
raw.endsWith(".zip")
);
}
async function installPluginFromPluginsCommand(params: {
raw: string;
config: OpenClawConfig;
}): Promise<{ ok: true; pluginId: string } | { ok: false; error: string }> {
const fileSpec = resolveFileNpmSpecToLocalPath(params.raw);
if (fileSpec && !fileSpec.ok) {
return { ok: false, error: fileSpec.error };
}
const normalized = fileSpec && fileSpec.ok ? fileSpec.path : params.raw;
const resolved = resolveUserPath(normalized);
if (fs.existsSync(resolved)) {
const result = await installPluginFromPath({
path: resolved,
logger: createPluginInstallLogger(),
});
if (!result.ok) {
return { ok: false, error: result.error };
}
clearPluginManifestRegistryCache();
const source: "archive" | "path" = resolveArchiveKind(resolved) ? "archive" : "path";
await persistPluginInstall({
config: params.config,
pluginId: result.pluginId,
install: {
source,
sourcePath: resolved,
installPath: result.targetDir,
version: result.version,
},
});
return { ok: true, pluginId: result.pluginId };
}
if (looksLikeLocalPluginInstallSpec(params.raw)) {
return { ok: false, error: `Path not found: ${resolved}` };
}
const clawhubSpec = parseClawHubPluginSpec(params.raw);
if (clawhubSpec) {
const result = await installPluginFromClawHub({
spec: params.raw,
logger: createPluginInstallLogger(),
});
if (!result.ok) {
return { ok: false, error: result.error };
}
clearPluginManifestRegistryCache();
await persistPluginInstall({
config: params.config,
pluginId: result.pluginId,
install: {
source: "clawhub",
spec: params.raw,
installPath: result.targetDir,
version: result.version,
integrity: result.clawhub.integrity,
resolvedAt: result.clawhub.resolvedAt,
clawhubUrl: result.clawhub.clawhubUrl,
clawhubPackage: result.clawhub.clawhubPackage,
clawhubFamily: result.clawhub.clawhubFamily,
clawhubChannel: result.clawhub.clawhubChannel,
},
});
return { ok: true, pluginId: result.pluginId };
}
const preferredClawHubSpec = buildPreferredClawHubSpec(params.raw);
if (preferredClawHubSpec) {
const clawhubResult = await installPluginFromClawHub({
spec: preferredClawHubSpec,
logger: createPluginInstallLogger(),
});
if (clawhubResult.ok) {
clearPluginManifestRegistryCache();
await persistPluginInstall({
config: params.config,
pluginId: clawhubResult.pluginId,
install: {
source: "clawhub",
spec: preferredClawHubSpec,
installPath: clawhubResult.targetDir,
version: clawhubResult.version,
integrity: clawhubResult.clawhub.integrity,
resolvedAt: clawhubResult.clawhub.resolvedAt,
clawhubUrl: clawhubResult.clawhub.clawhubUrl,
clawhubPackage: clawhubResult.clawhub.clawhubPackage,
clawhubFamily: clawhubResult.clawhub.clawhubFamily,
clawhubChannel: clawhubResult.clawhub.clawhubChannel,
},
});
return { ok: true, pluginId: clawhubResult.pluginId };
}
if (decidePreferredClawHubFallback(clawhubResult) !== "fallback_to_npm") {
return { ok: false, error: clawhubResult.error };
}
}
const result = await installPluginFromNpmSpec({
spec: params.raw,
logger: createPluginInstallLogger(),
});
if (!result.ok) {
return { ok: false, error: result.error };
}
clearPluginManifestRegistryCache();
const installRecord = buildNpmInstallRecordFields({
spec: params.raw,
installPath: result.targetDir,
version: result.version,
resolution: result.npmResolution,
});
await persistPluginInstall({
config: params.config,
pluginId: result.pluginId,
install: installRecord,
});
return { ok: true, pluginId: result.pluginId };
}
async function loadPluginCommandState(workspaceDir: string): Promise<
| {
ok: true;
@@ -226,6 +377,7 @@ export const handlePluginsCommand: CommandHandler = async (params, allowTextComm
reply: {
text: renderJsonBlock(`🔌 Plugin "${payload.inspect.plugin.id}"`, {
...payload.inspect,
compatibilityWarnings: payload.compatibilityWarnings,
install: payload.install,
}),
},
@@ -235,12 +387,31 @@ export const handlePluginsCommand: CommandHandler = async (params, allowTextComm
const missingAdminScope = requireGatewayClientScopeForInternalChannel(params, {
label: "/plugins write",
allowedScopes: ["operator.admin"],
missingText: "❌ /plugins enable|disable requires operator.admin for gateway clients.",
missingText: "❌ /plugins install|enable|disable requires operator.admin for gateway clients.",
});
if (missingAdminScope) {
return missingAdminScope;
}
if (pluginsCommand.action === "install") {
const installed = await installPluginFromPluginsCommand({
raw: pluginsCommand.spec,
config: structuredClone(loaded.config),
});
if (!installed.ok) {
return {
shouldContinue: false,
reply: { text: `⚠️ ${installed.error}` },
};
}
return {
shouldContinue: false,
reply: {
text: `🔌 Installed plugin "${installed.pluginId}". Restart the gateway to load plugins.`,
},
};
}
const plugin = findPlugin(loaded.report, pluginsCommand.name);
if (!plugin) {
return {

View File

@@ -1,6 +1,7 @@
export type PluginsCommand =
| { action: "list" }
| { action: "inspect"; name?: string }
| { action: "install"; spec: string }
| { action: "enable"; name: string }
| { action: "disable"; name: string }
| { action: "error"; message: string };
@@ -33,6 +34,16 @@ export function parsePluginsCommand(raw: string): PluginsCommand | null {
return { action: "inspect", name: name || undefined };
}
if (action === "install" || action === "add") {
if (!name) {
return {
action: "error",
message: "Usage: /plugins install <path|archive|npm-spec|clawhub:pkg>",
};
}
return { action: "install", spec: name };
}
if (action === "enable" || action === "disable") {
if (!name) {
return {
@@ -45,6 +56,6 @@ export function parsePluginsCommand(raw: string): PluginsCommand | null {
return {
action: "error",
message: "Usage: /plugins list|inspect|show|get|enable|disable [plugin]",
message: "Usage: /plugins list|inspect|show|get|install|enable|disable [plugin]",
};
}