mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:10:44 +00:00
fix: keep native command auto defaults cold
This commit is contained in:
@@ -524,6 +524,12 @@ Non-bundled plugins that declare `channels[]` should also declare matching
|
||||
cold-path config schema, setup, and Control UI surfaces cannot know the
|
||||
channel-owned option shape until plugin runtime executes.
|
||||
|
||||
`channelConfigs.<channel-id>.commands.nativeCommandsAutoEnabled` and
|
||||
`nativeSkillsAutoEnabled` can declare static `auto` defaults for command config
|
||||
checks that run before channel runtime loads. Bundled channels can also publish
|
||||
the same defaults through `package.json#openclaw.channel.commands` alongside
|
||||
their other package-owned channel catalog metadata.
|
||||
|
||||
```json
|
||||
{
|
||||
"channelConfigs": {
|
||||
@@ -543,6 +549,10 @@ channel-owned option shape until plugin runtime executes.
|
||||
},
|
||||
"label": "Matrix",
|
||||
"description": "Matrix homeserver connection",
|
||||
"commands": {
|
||||
"nativeCommandsAutoEnabled": true,
|
||||
"nativeSkillsAutoEnabled": true
|
||||
},
|
||||
"preferOver": ["matrix-legacy"]
|
||||
}
|
||||
}
|
||||
@@ -557,6 +567,7 @@ Each channel entry can include:
|
||||
| `uiHints` | `Record<string, object>` | Optional UI labels/placeholders/sensitive hints for that channel config section. |
|
||||
| `label` | `string` | Channel label merged into picker and inspect surfaces when runtime metadata is not ready. |
|
||||
| `description` | `string` | Short channel description for inspect and catalog surfaces. |
|
||||
| `commands` | `object` | Static native command and native skill auto-defaults for pre-runtime config checks. |
|
||||
| `preferOver` | `string[]` | Legacy or lower-priority plugin ids this channel should outrank in selection surfaces. |
|
||||
|
||||
### Replacing another channel plugin
|
||||
@@ -792,6 +803,7 @@ Important examples:
|
||||
| `openclaw.setupEntry` | Lightweight setup-only entrypoint used during onboarding, deferred channel startup, and read-only channel status/SecretRef discovery. Must stay inside the plugin package directory. |
|
||||
| `openclaw.runtimeSetupEntry` | Declares the built JavaScript setup entrypoint for installed packages. Must stay inside the plugin package directory. |
|
||||
| `openclaw.channel` | Cheap channel catalog metadata like labels, docs paths, aliases, and selection copy. |
|
||||
| `openclaw.channel.commands` | Static native command and native skill auto-default metadata used by config, audit, and command-list surfaces before channel runtime loads. |
|
||||
| `openclaw.channel.configuredState` | Lightweight configured-state checker metadata that can answer "does env-only setup already exist?" without loading the full channel runtime. |
|
||||
| `openclaw.channel.persistedAuthState` | Lightweight persisted-auth checker metadata that can answer "is anything already signed in?" without loading the full channel runtime. |
|
||||
| `openclaw.install.npmSpec` / `openclaw.install.localPath` | Install/update hints for bundled and externally published plugins. |
|
||||
|
||||
@@ -40,6 +40,10 @@
|
||||
"blurb": "very well supported right now.",
|
||||
"systemImage": "bubble.left.and.bubble.right",
|
||||
"markdownCapable": true,
|
||||
"commands": {
|
||||
"nativeCommandsAutoEnabled": true,
|
||||
"nativeSkillsAutoEnabled": true
|
||||
},
|
||||
"configuredState": {
|
||||
"specifier": "./configured-state",
|
||||
"exportName": "hasDiscordConfiguredState"
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
"blurb": "supported (Socket Mode).",
|
||||
"systemImage": "number",
|
||||
"markdownCapable": true,
|
||||
"commands": {
|
||||
"nativeCommandsAutoEnabled": false,
|
||||
"nativeSkillsAutoEnabled": false
|
||||
},
|
||||
"configuredState": {
|
||||
"specifier": "./configured-state",
|
||||
"exportName": "hasSlackConfiguredState"
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
"https://openclaw.ai"
|
||||
],
|
||||
"markdownCapable": true,
|
||||
"commands": {
|
||||
"nativeCommandsAutoEnabled": true,
|
||||
"nativeSkillsAutoEnabled": true
|
||||
},
|
||||
"configuredState": {
|
||||
"specifier": "./configured-state",
|
||||
"exportName": "hasTelegramConfiguredState"
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { loadOpenClawPlugins as loadOpenClawPluginsType } from "../../plugi
|
||||
import type { PluginManifestRecord } from "../../plugins/manifest-registry.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "../../plugins/plugin-registry.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
import { sanitizeForLog } from "../../terminal/ansi.js";
|
||||
import { getBundledChannelSetupPlugin } from "./bundled.js";
|
||||
import { listChannelPlugins } from "./registry.js";
|
||||
@@ -72,6 +73,10 @@ type ReadOnlyChannelPluginResolution = {
|
||||
missingConfiguredChannelIds: string[];
|
||||
};
|
||||
type ManifestChannelConfigRecord = NonNullable<PluginManifestRecord["channelConfigs"]>[string];
|
||||
type ChannelCommandDefaults = Pick<
|
||||
NonNullable<ChannelPlugin["commands"]>,
|
||||
"nativeCommandsAutoEnabled" | "nativeSkillsAutoEnabled"
|
||||
>;
|
||||
|
||||
function addChannelPlugins(
|
||||
byId: Map<string, ChannelPlugin>,
|
||||
@@ -125,6 +130,26 @@ function normalizeManifestText(value: string | undefined, fallback: string): str
|
||||
return sanitizeForLog(value?.trim() || fallback).trim();
|
||||
}
|
||||
|
||||
function normalizeChannelCommandDefaults(
|
||||
value: ChannelCommandDefaults | undefined,
|
||||
): ChannelCommandDefaults | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const nativeCommandsAutoEnabled =
|
||||
typeof value.nativeCommandsAutoEnabled === "boolean"
|
||||
? value.nativeCommandsAutoEnabled
|
||||
: undefined;
|
||||
const nativeSkillsAutoEnabled =
|
||||
typeof value.nativeSkillsAutoEnabled === "boolean" ? value.nativeSkillsAutoEnabled : undefined;
|
||||
return nativeCommandsAutoEnabled !== undefined || nativeSkillsAutoEnabled !== undefined
|
||||
? {
|
||||
...(nativeCommandsAutoEnabled !== undefined ? { nativeCommandsAutoEnabled } : {}),
|
||||
...(nativeSkillsAutoEnabled !== undefined ? { nativeSkillsAutoEnabled } : {}),
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function rebindChannelConfig(
|
||||
cfg: OpenClawConfig,
|
||||
sourceChannelId: string,
|
||||
@@ -258,6 +283,9 @@ function buildManifestChannelPlugin(params: {
|
||||
channelConfig?.description ?? catalogMeta?.blurb,
|
||||
params.record.description || "",
|
||||
);
|
||||
const commands = normalizeChannelCommandDefaults(
|
||||
channelConfig?.commands ?? catalogMeta?.commands,
|
||||
);
|
||||
return {
|
||||
id: params.channelId,
|
||||
meta: {
|
||||
@@ -273,6 +301,7 @@ function buildManifestChannelPlugin(params: {
|
||||
: {}),
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
...(commands ? { commands } : {}),
|
||||
...(channelConfig
|
||||
? {
|
||||
configSchema: {
|
||||
@@ -318,6 +347,47 @@ function canUseManifestChannelPlugin(record: PluginManifestRecord, channelId: st
|
||||
return record.channelCatalogMeta?.id === channelId;
|
||||
}
|
||||
|
||||
export function resolveReadOnlyChannelCommandDefaults(
|
||||
channelId: string,
|
||||
options: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
stateDir?: string;
|
||||
workspaceDir?: string;
|
||||
} = {},
|
||||
): ChannelCommandDefaults | undefined {
|
||||
const normalizedChannelId = normalizeOptionalString(channelId) ?? "";
|
||||
if (!normalizedChannelId || !isSafeManifestChannelId(normalizedChannelId)) {
|
||||
return undefined;
|
||||
}
|
||||
const registry = loadPluginManifestRegistryForPluginRegistry({
|
||||
stateDir: options.stateDir,
|
||||
workspaceDir: options.workspaceDir,
|
||||
env: options.env ?? process.env,
|
||||
includeDisabled: true,
|
||||
});
|
||||
for (const record of registry.plugins) {
|
||||
if (!record.channels.includes(normalizedChannelId)) {
|
||||
continue;
|
||||
}
|
||||
const channelConfigValue = record.channelConfigs
|
||||
? readOwnRecordValue(record.channelConfigs as Record<string, unknown>, normalizedChannelId)
|
||||
: undefined;
|
||||
const channelConfig =
|
||||
channelConfigValue &&
|
||||
typeof channelConfigValue === "object" &&
|
||||
!Array.isArray(channelConfigValue)
|
||||
? (channelConfigValue as ManifestChannelConfigRecord)
|
||||
: undefined;
|
||||
const commands = normalizeChannelCommandDefaults(
|
||||
channelConfig?.commands ?? record.channelCatalogMeta?.commands,
|
||||
);
|
||||
if (commands) {
|
||||
return commands;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function rebindChannelPluginConfig(
|
||||
config: ChannelPlugin["config"],
|
||||
sourceChannelId: string,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import path from "node:path";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
@@ -100,6 +101,32 @@ describe("resolveNativeSkillsEnabled", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("uses package channel metadata for bundled auto defaults before runtime loads", () => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
const env = {
|
||||
...process.env,
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: path.resolve("extensions"),
|
||||
OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY: "1",
|
||||
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
|
||||
OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE: "1",
|
||||
};
|
||||
|
||||
expect(
|
||||
resolveNativeSkillsEnabled({
|
||||
providerId: "discord",
|
||||
globalSetting: "auto",
|
||||
env,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
resolveNativeCommandsEnabled({
|
||||
providerId: "slack",
|
||||
globalSetting: "auto",
|
||||
env,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("honors explicit provider settings", () => {
|
||||
expect(
|
||||
resolveNativeSkillsEnabled({
|
||||
|
||||
@@ -1,30 +1,43 @@
|
||||
import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js";
|
||||
import { getLoadedChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js";
|
||||
import { resolveReadOnlyChannelCommandDefaults } from "../channels/plugins/read-only.js";
|
||||
import type { ChannelId } from "../channels/plugins/types.public.js";
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
import type { NativeCommandsSetting } from "./types.js";
|
||||
export { isCommandFlagEnabled, isRestartEnabled, type CommandFlagKey } from "./commands.flags.js";
|
||||
|
||||
function resolveAutoDefault(
|
||||
providerId: ChannelId | undefined,
|
||||
kind: "native" | "nativeSkills",
|
||||
options?: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
stateDir?: string;
|
||||
workspaceDir?: string;
|
||||
autoDefault?: boolean;
|
||||
},
|
||||
): boolean {
|
||||
const id = normalizeChannelId(providerId);
|
||||
const id = normalizeChannelId(providerId) ?? normalizeOptionalLowercaseString(providerId);
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
const plugin = getChannelPlugin(id);
|
||||
if (!plugin) {
|
||||
return false;
|
||||
if (typeof options?.autoDefault === "boolean") {
|
||||
return options.autoDefault;
|
||||
}
|
||||
const commandDefaults =
|
||||
getLoadedChannelPlugin(id)?.commands ?? resolveReadOnlyChannelCommandDefaults(id, options);
|
||||
if (kind === "native") {
|
||||
return plugin.commands?.nativeCommandsAutoEnabled === true;
|
||||
return commandDefaults?.nativeCommandsAutoEnabled === true;
|
||||
}
|
||||
return plugin.commands?.nativeSkillsAutoEnabled === true;
|
||||
return commandDefaults?.nativeSkillsAutoEnabled === true;
|
||||
}
|
||||
|
||||
export function resolveNativeSkillsEnabled(params: {
|
||||
providerId: ChannelId;
|
||||
providerSetting?: NativeCommandsSetting;
|
||||
globalSetting?: NativeCommandsSetting;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
stateDir?: string;
|
||||
workspaceDir?: string;
|
||||
autoDefault?: boolean;
|
||||
}): boolean {
|
||||
return resolveNativeCommandSetting({ ...params, kind: "nativeSkills" });
|
||||
}
|
||||
@@ -33,6 +46,10 @@ export function resolveNativeCommandsEnabled(params: {
|
||||
providerId: ChannelId;
|
||||
providerSetting?: NativeCommandsSetting;
|
||||
globalSetting?: NativeCommandsSetting;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
stateDir?: string;
|
||||
workspaceDir?: string;
|
||||
autoDefault?: boolean;
|
||||
}): boolean {
|
||||
return resolveNativeCommandSetting({ ...params, kind: "native" });
|
||||
}
|
||||
@@ -42,8 +59,12 @@ function resolveNativeCommandSetting(params: {
|
||||
providerSetting?: NativeCommandsSetting;
|
||||
globalSetting?: NativeCommandsSetting;
|
||||
kind?: "native" | "nativeSkills";
|
||||
env?: NodeJS.ProcessEnv;
|
||||
stateDir?: string;
|
||||
workspaceDir?: string;
|
||||
autoDefault?: boolean;
|
||||
}): boolean {
|
||||
const { providerId, providerSetting, globalSetting, kind = "native" } = params;
|
||||
const { providerId, providerSetting, globalSetting, kind = "native", ...options } = params;
|
||||
const setting = providerSetting === undefined ? globalSetting : providerSetting;
|
||||
if (setting === true) {
|
||||
return true;
|
||||
@@ -51,7 +72,7 @@ function resolveNativeCommandSetting(params: {
|
||||
if (setting === false) {
|
||||
return false;
|
||||
}
|
||||
return resolveAutoDefault(providerId, kind);
|
||||
return resolveAutoDefault(providerId, kind, options);
|
||||
}
|
||||
|
||||
export function isNativeCommandsExplicitlyDisabled(params: {
|
||||
|
||||
@@ -176,6 +176,9 @@ export function collectBundledChannelConfigs(params: {
|
||||
: preferOver.length > 0
|
||||
? { preferOver }
|
||||
: {}),
|
||||
...((existing?.commands ?? channelMeta?.commands)
|
||||
? { commands: existing?.commands ?? channelMeta?.commands }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getChannelPlugin } from "../channels/plugins/index.js";
|
||||
import { getLoadedChannelPlugin } from "../channels/plugins/index.js";
|
||||
import { resolveReadOnlyChannelCommandDefaults } from "../channels/plugins/read-only.js";
|
||||
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
import type { OpenClawPluginCommandDefinition } from "./types.js";
|
||||
@@ -90,7 +91,10 @@ export function getPluginCommandSpecs(provider?: string): Array<{
|
||||
const providerName = normalizeOptionalLowercaseString(provider);
|
||||
if (
|
||||
providerName &&
|
||||
getChannelPlugin(providerName)?.commands?.nativeCommandsAutoEnabled !== true
|
||||
(
|
||||
getLoadedChannelPlugin(providerName)?.commands ??
|
||||
resolveReadOnlyChannelCommandDefaults(providerName)
|
||||
)?.nativeCommandsAutoEnabled !== true
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -143,6 +143,10 @@ function createRichPluginFixture(params: { packageVersion?: string } = {}) {
|
||||
label: "Demo",
|
||||
blurb: "Demo channel",
|
||||
preferOver: ["legacy-demo"],
|
||||
commands: {
|
||||
nativeCommandsAutoEnabled: true,
|
||||
nativeSkillsAutoEnabled: false,
|
||||
},
|
||||
},
|
||||
install: {
|
||||
npmSpec: "@vendor/demo-plugin@1.2.3",
|
||||
@@ -195,6 +199,10 @@ describe("installed plugin index", () => {
|
||||
label: "Demo",
|
||||
blurb: "Demo channel",
|
||||
preferOver: ["legacy-demo"],
|
||||
commands: {
|
||||
nativeCommandsAutoEnabled: true,
|
||||
nativeSkillsAutoEnabled: false,
|
||||
},
|
||||
},
|
||||
compat: [
|
||||
"activation-channel-hint",
|
||||
|
||||
@@ -69,7 +69,7 @@ export type InstalledPluginInstallRecordInfo = Pick<
|
||||
|
||||
export type InstalledPluginPackageChannelInfo = Pick<
|
||||
PluginPackageChannel,
|
||||
"id" | "label" | "blurb" | "preferOver"
|
||||
"id" | "label" | "blurb" | "preferOver" | "commands"
|
||||
>;
|
||||
|
||||
export type InstalledPluginIndexRecord = {
|
||||
@@ -317,11 +317,27 @@ function normalizePackageChannel(
|
||||
const label = normalizeStringField(channel?.label);
|
||||
const blurb = normalizeStringField(channel?.blurb);
|
||||
const preferOver = normalizeStringListField(channel?.preferOver);
|
||||
const commands =
|
||||
channel?.commands &&
|
||||
typeof channel.commands === "object" &&
|
||||
!Array.isArray(channel.commands) &&
|
||||
(typeof channel.commands.nativeCommandsAutoEnabled === "boolean" ||
|
||||
typeof channel.commands.nativeSkillsAutoEnabled === "boolean")
|
||||
? {
|
||||
...(typeof channel.commands.nativeCommandsAutoEnabled === "boolean"
|
||||
? { nativeCommandsAutoEnabled: channel.commands.nativeCommandsAutoEnabled }
|
||||
: {}),
|
||||
...(typeof channel.commands.nativeSkillsAutoEnabled === "boolean"
|
||||
? { nativeSkillsAutoEnabled: channel.commands.nativeSkillsAutoEnabled }
|
||||
: {}),
|
||||
}
|
||||
: undefined;
|
||||
return {
|
||||
id,
|
||||
...(label ? { label } : {}),
|
||||
...(blurb ? { blurb } : {}),
|
||||
...(preferOver ? { preferOver } : {}),
|
||||
...(commands ? { commands } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -142,6 +142,59 @@ describe("loadPluginManifestRegistryForInstalledIndex", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("hydrates package channel command metadata while reconstructing from an older index", () => {
|
||||
const rootDir = makeTempDir();
|
||||
writePlugin(rootDir, "installed", "installed-");
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "package.json"),
|
||||
JSON.stringify({
|
||||
openclaw: {
|
||||
channel: {
|
||||
id: "installed",
|
||||
label: "Installed",
|
||||
commands: {
|
||||
nativeCommandsAutoEnabled: true,
|
||||
nativeSkillsAutoEnabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const index = createIndex(rootDir);
|
||||
const registry = loadPluginManifestRegistryForInstalledIndex({
|
||||
index: {
|
||||
...index,
|
||||
plugins: [
|
||||
{
|
||||
...index.plugins[0],
|
||||
packageChannel: {
|
||||
id: "installed",
|
||||
label: "Installed",
|
||||
},
|
||||
packageJson: {
|
||||
path: "package.json",
|
||||
hash: "old-index-hash",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
env: {
|
||||
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
|
||||
OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE: "1",
|
||||
OPENCLAW_VERSION: "2026.4.25",
|
||||
VITEST: "true",
|
||||
},
|
||||
includeDisabled: true,
|
||||
});
|
||||
|
||||
expect(registry.plugins[0]?.channelCatalogMeta?.commands).toEqual({
|
||||
nativeCommandsAutoEnabled: true,
|
||||
nativeSkillsAutoEnabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("round-trips bundle metadata through the persisted index before reconstruction", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const rootDir = makeTempDir();
|
||||
|
||||
@@ -5,7 +5,12 @@ import type { PluginCandidate } from "./discovery.js";
|
||||
import type { InstalledPluginIndex, InstalledPluginIndexRecord } from "./installed-plugin-index.js";
|
||||
import { extractPluginInstallRecordsFromInstalledPluginIndex } from "./installed-plugin-index.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRegistry } from "./manifest-registry.js";
|
||||
import { DEFAULT_PLUGIN_ENTRY_CANDIDATES } from "./manifest.js";
|
||||
import {
|
||||
DEFAULT_PLUGIN_ENTRY_CANDIDATES,
|
||||
getPackageManifestMetadata,
|
||||
type OpenClawPackageManifest,
|
||||
type PackageManifest,
|
||||
} from "./manifest.js";
|
||||
|
||||
function resolveInstalledPluginRootDir(record: InstalledPluginIndexRecord): string {
|
||||
return record.rootDir || path.dirname(record.manifestPath || process.cwd());
|
||||
@@ -22,8 +27,45 @@ function resolveFallbackPluginSource(record: InstalledPluginIndexRecord): string
|
||||
return path.join(rootDir, DEFAULT_PLUGIN_ENTRY_CANDIDATES[0]);
|
||||
}
|
||||
|
||||
function resolveInstalledPackageManifest(
|
||||
record: InstalledPluginIndexRecord,
|
||||
): OpenClawPackageManifest | undefined {
|
||||
if (!record.packageChannel) {
|
||||
return undefined;
|
||||
}
|
||||
if (record.packageChannel.commands) {
|
||||
return { channel: record.packageChannel };
|
||||
}
|
||||
const rootDir = resolveInstalledPluginRootDir(record);
|
||||
const packageJsonPath = record.packageJson?.path
|
||||
? path.resolve(rootDir, record.packageJson.path)
|
||||
: undefined;
|
||||
if (!packageJsonPath) {
|
||||
return { channel: record.packageChannel };
|
||||
}
|
||||
const relative = path.relative(rootDir, packageJsonPath);
|
||||
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||
return { channel: record.packageChannel };
|
||||
}
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as PackageManifest;
|
||||
const packageManifest = getPackageManifestMetadata(packageJson);
|
||||
return {
|
||||
channel: {
|
||||
...record.packageChannel,
|
||||
...(packageManifest?.channel?.commands
|
||||
? { commands: packageManifest.channel.commands }
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return { channel: record.packageChannel };
|
||||
}
|
||||
}
|
||||
|
||||
function toPluginCandidate(record: InstalledPluginIndexRecord): PluginCandidate {
|
||||
const rootDir = resolveInstalledPluginRootDir(record);
|
||||
const packageManifest = resolveInstalledPackageManifest(record);
|
||||
return {
|
||||
idHint: record.pluginId,
|
||||
source: record.source ?? resolveFallbackPluginSource(record),
|
||||
@@ -34,7 +76,7 @@ function toPluginCandidate(record: InstalledPluginIndexRecord): PluginCandidate
|
||||
...(record.bundleFormat ? { bundleFormat: record.bundleFormat } : {}),
|
||||
...(record.packageName ? { packageName: record.packageName } : {}),
|
||||
...(record.packageVersion ? { packageVersion: record.packageVersion } : {}),
|
||||
...(record.packageChannel ? { packageManifest: { channel: record.packageChannel } } : {}),
|
||||
...(packageManifest ? { packageManifest } : {}),
|
||||
packageDir: rootDir,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
type PluginManifestActivation,
|
||||
type PluginManifestConfigContracts,
|
||||
type PluginManifest,
|
||||
type PluginManifestChannelCommandDefaults,
|
||||
type PluginManifestChannelConfig,
|
||||
type PluginManifestContracts,
|
||||
type PluginManifestMediaUnderstandingProviderMetadata,
|
||||
@@ -148,6 +149,7 @@ export type PluginManifestRecord = {
|
||||
label?: string;
|
||||
blurb?: string;
|
||||
preferOver?: readonly string[];
|
||||
commands?: PluginManifestChannelCommandDefaults;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -220,6 +222,29 @@ function normalizePreferredPluginIds(raw: unknown): string[] | undefined {
|
||||
return normalizeOptionalTrimmedStringList(raw);
|
||||
}
|
||||
|
||||
function normalizePackageChannelCommands(
|
||||
commands: unknown,
|
||||
): PluginManifestChannelCommandDefaults | undefined {
|
||||
if (!commands || typeof commands !== "object" || Array.isArray(commands)) {
|
||||
return undefined;
|
||||
}
|
||||
const record = commands as Record<string, unknown>;
|
||||
const nativeCommandsAutoEnabled =
|
||||
typeof record.nativeCommandsAutoEnabled === "boolean"
|
||||
? record.nativeCommandsAutoEnabled
|
||||
: undefined;
|
||||
const nativeSkillsAutoEnabled =
|
||||
typeof record.nativeSkillsAutoEnabled === "boolean"
|
||||
? record.nativeSkillsAutoEnabled
|
||||
: undefined;
|
||||
return nativeCommandsAutoEnabled !== undefined || nativeSkillsAutoEnabled !== undefined
|
||||
? {
|
||||
...(nativeCommandsAutoEnabled !== undefined ? { nativeCommandsAutoEnabled } : {}),
|
||||
...(nativeSkillsAutoEnabled !== undefined ? { nativeSkillsAutoEnabled } : {}),
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function mergePackageChannelMetaIntoChannelConfigs(params: {
|
||||
channelConfigs?: Record<string, PluginManifestChannelConfig>;
|
||||
packageChannel?: OpenClawPackageManifest["channel"];
|
||||
@@ -243,6 +268,8 @@ function mergePackageChannelMetaIntoChannelConfigs(params: {
|
||||
existing.description ?? normalizeOptionalString(params.packageChannel?.blurb) ?? "";
|
||||
const preferOver =
|
||||
existing.preferOver ?? normalizePreferredPluginIds(params.packageChannel?.preferOver);
|
||||
const commands =
|
||||
existing.commands ?? normalizePackageChannelCommands(params.packageChannel?.commands);
|
||||
|
||||
const merged: Record<string, PluginManifestChannelConfig> = Object.create(null);
|
||||
for (const [key, value] of Object.entries(params.channelConfigs)) {
|
||||
@@ -255,6 +282,7 @@ function mergePackageChannelMetaIntoChannelConfigs(params: {
|
||||
...(label ? { label } : {}),
|
||||
...(description ? { description } : {}),
|
||||
...(preferOver?.length ? { preferOver } : {}),
|
||||
...(commands ? { commands } : {}),
|
||||
};
|
||||
return merged;
|
||||
}
|
||||
@@ -270,6 +298,9 @@ function buildRecord(params: {
|
||||
channelConfigs: params.manifest.channelConfigs,
|
||||
packageChannel: params.candidate.packageManifest?.channel,
|
||||
});
|
||||
const packageChannelCommands = normalizePackageChannelCommands(
|
||||
params.candidate.packageManifest?.channel?.commands,
|
||||
);
|
||||
return {
|
||||
id: params.manifest.id,
|
||||
name: normalizeOptionalString(params.manifest.name) ?? params.candidate.packageName,
|
||||
@@ -335,6 +366,7 @@ function buildRecord(params: {
|
||||
...(params.candidate.packageManifest.channel.preferOver
|
||||
? { preferOver: params.candidate.packageManifest.channel.preferOver }
|
||||
: {}),
|
||||
...(packageChannelCommands ? { commands: packageChannelCommands } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
||||
@@ -40,6 +40,12 @@ export type PluginManifestChannelConfig = {
|
||||
label?: string;
|
||||
description?: string;
|
||||
preferOver?: string[];
|
||||
commands?: PluginManifestChannelCommandDefaults;
|
||||
};
|
||||
|
||||
export type PluginManifestChannelCommandDefaults = {
|
||||
nativeCommandsAutoEnabled?: boolean;
|
||||
nativeSkillsAutoEnabled?: boolean;
|
||||
};
|
||||
|
||||
export type PluginManifestModelSupport = {
|
||||
@@ -820,6 +826,7 @@ function normalizeChannelConfigs(
|
||||
const label = normalizeOptionalString(rawEntry.label) ?? "";
|
||||
const description = normalizeOptionalString(rawEntry.description) ?? "";
|
||||
const preferOver = normalizeTrimmedStringList(rawEntry.preferOver);
|
||||
const commandDefaults = normalizeManifestChannelCommandDefaults(rawEntry.commands);
|
||||
normalized[channelId] = {
|
||||
schema,
|
||||
...(uiHints ? { uiHints } : {}),
|
||||
@@ -827,11 +834,32 @@ function normalizeChannelConfigs(
|
||||
...(label ? { label } : {}),
|
||||
...(description ? { description } : {}),
|
||||
...(preferOver.length > 0 ? { preferOver } : {}),
|
||||
...(commandDefaults ? { commands: commandDefaults } : {}),
|
||||
};
|
||||
}
|
||||
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function normalizeManifestChannelCommandDefaults(
|
||||
value: unknown,
|
||||
): PluginManifestChannelCommandDefaults | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const nativeCommandsAutoEnabled =
|
||||
typeof value.nativeCommandsAutoEnabled === "boolean"
|
||||
? value.nativeCommandsAutoEnabled
|
||||
: undefined;
|
||||
const nativeSkillsAutoEnabled =
|
||||
typeof value.nativeSkillsAutoEnabled === "boolean" ? value.nativeSkillsAutoEnabled : undefined;
|
||||
return nativeCommandsAutoEnabled !== undefined || nativeSkillsAutoEnabled !== undefined
|
||||
? {
|
||||
...(nativeCommandsAutoEnabled !== undefined ? { nativeCommandsAutoEnabled } : {}),
|
||||
...(nativeSkillsAutoEnabled !== undefined ? { nativeSkillsAutoEnabled } : {}),
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function resolvePluginManifestPath(rootDir: string): string {
|
||||
for (const filename of PLUGIN_MANIFEST_FILENAMES) {
|
||||
const candidate = path.join(rootDir, filename);
|
||||
@@ -1012,6 +1040,7 @@ export type PluginPackageChannel = {
|
||||
quickstartAllowFrom?: boolean;
|
||||
forceAccountBinding?: boolean;
|
||||
preferSessionLookupForAnnounceTarget?: boolean;
|
||||
commands?: PluginManifestChannelCommandDefaults;
|
||||
configuredState?: {
|
||||
specifier?: string;
|
||||
exportName?: string;
|
||||
|
||||
@@ -342,6 +342,8 @@ export async function collectPluginsTrustFindings(params: {
|
||||
| boolean
|
||||
| undefined,
|
||||
globalSetting: params.cfg.commands?.nativeSkills,
|
||||
stateDir: params.stateDir,
|
||||
autoDefault: plugin.commands?.nativeSkillsAutoEnabled === true,
|
||||
});
|
||||
}),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user