mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-14 18:50:44 +00:00
Summary: - clarify CLI diagnostics when an unknown subcommand is actually a plugin agent tool - route early proxy-preflight misses through the same policy helper - refresh bundled sidecar update fixtures for current package ownership Verification: - pnpm test src/cli/run-main.test.ts src/cli/run-main.exit.test.ts src/plugins/manifest-command-aliases.test.ts - pnpm test src/infra/update-global.test.ts src/infra/update-runner.test.ts - pnpm exec oxfmt --check --threads=1 CHANGELOG.md src/cli/run-main-policy.ts src/cli/run-main.ts src/cli/run-main.test.ts src/cli/run-main.exit.test.ts src/plugins/manifest-command-aliases.ts src/plugins/manifest-command-aliases.runtime.ts src/plugins/manifest-command-aliases.test.ts - pnpm build - live temp lossless-claw manifest: pnpm openclaw lcm_recent emits the agent-tool diagnostic and no plugins.allow suggestion Co-authored-by: 100yenadmin <100yenadmin+agent-77214@100yen.org>
136 lines
4.1 KiB
TypeScript
136 lines
4.1 KiB
TypeScript
import {
|
|
normalizeOptionalLowercaseString,
|
|
normalizeOptionalString,
|
|
} from "../shared/string-coerce.js";
|
|
import { isRecord } from "../utils.js";
|
|
|
|
export type PluginManifestCommandAliasKind = "runtime-slash";
|
|
|
|
export type PluginManifestCommandAlias = {
|
|
/** Command-like name users may put in plugin config by mistake. */
|
|
name: string;
|
|
/** Command family, used for targeted diagnostics. */
|
|
kind?: PluginManifestCommandAliasKind;
|
|
/** Optional root CLI command that handles related CLI operations. */
|
|
cliCommand?: string;
|
|
};
|
|
|
|
export type PluginManifestCommandAliasRecord = PluginManifestCommandAlias & {
|
|
pluginId: string;
|
|
enabledByDefault?: boolean;
|
|
};
|
|
|
|
export type PluginManifestToolOwnerRecord = {
|
|
toolName: string;
|
|
pluginId: string;
|
|
/**
|
|
* "loaded" — the owning plugin passes control-plane availability filters and
|
|
* the tool itself passes manifest-tool-availability checks (configSignals/
|
|
* authSignals). The diagnostic can say the tool is available from this plugin.
|
|
*
|
|
* "manifest-only" — the manifest claims ownership but availability checks
|
|
* either failed (plugin denied/disabled, missing required config) or were
|
|
* not performed (pure registry lookup with no plugin metadata snapshot).
|
|
* Emit a softer "may be provided by" message in that case so the diagnostic
|
|
* does not over-assert about plugins that the runtime never registered.
|
|
*/
|
|
availability?: "loaded" | "manifest-only";
|
|
};
|
|
|
|
export type PluginManifestCommandAliasRegistry = {
|
|
plugins: readonly {
|
|
id: string;
|
|
enabledByDefault?: boolean;
|
|
commandAliases?: readonly PluginManifestCommandAlias[];
|
|
contracts?: { tools?: readonly string[] };
|
|
}[];
|
|
};
|
|
|
|
export function normalizeManifestCommandAliases(
|
|
value: unknown,
|
|
): PluginManifestCommandAlias[] | undefined {
|
|
if (!Array.isArray(value)) {
|
|
return undefined;
|
|
}
|
|
|
|
const normalized: PluginManifestCommandAlias[] = [];
|
|
for (const entry of value) {
|
|
if (typeof entry === "string") {
|
|
const name = normalizeOptionalString(entry) ?? "";
|
|
if (name) {
|
|
normalized.push({ name });
|
|
}
|
|
continue;
|
|
}
|
|
if (!isRecord(entry)) {
|
|
continue;
|
|
}
|
|
const name = normalizeOptionalString(entry.name) ?? "";
|
|
if (!name) {
|
|
continue;
|
|
}
|
|
const kind = entry.kind === "runtime-slash" ? entry.kind : undefined;
|
|
const cliCommand = normalizeOptionalString(entry.cliCommand) ?? "";
|
|
normalized.push({
|
|
name,
|
|
...(kind ? { kind } : {}),
|
|
...(cliCommand ? { cliCommand } : {}),
|
|
});
|
|
}
|
|
return normalized.length > 0 ? normalized : undefined;
|
|
}
|
|
|
|
export function resolveManifestToolOwnerInRegistry(params: {
|
|
toolName: string | undefined;
|
|
registry: PluginManifestCommandAliasRegistry;
|
|
}): PluginManifestToolOwnerRecord | undefined {
|
|
const normalizedToolName = normalizeOptionalLowercaseString(params.toolName);
|
|
if (!normalizedToolName) {
|
|
return undefined;
|
|
}
|
|
for (const plugin of params.registry.plugins) {
|
|
const tools = plugin.contracts?.tools;
|
|
if (!tools || tools.length === 0) {
|
|
continue;
|
|
}
|
|
const match = tools.find(
|
|
(entry) => normalizeOptionalLowercaseString(entry) === normalizedToolName,
|
|
);
|
|
if (match) {
|
|
return { toolName: match, pluginId: plugin.id };
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function resolveManifestCommandAliasOwnerInRegistry(params: {
|
|
command: string | undefined;
|
|
registry: PluginManifestCommandAliasRegistry;
|
|
}): PluginManifestCommandAliasRecord | undefined {
|
|
const normalizedCommand = normalizeOptionalLowercaseString(params.command);
|
|
if (!normalizedCommand) {
|
|
return undefined;
|
|
}
|
|
|
|
const commandIsPluginId = params.registry.plugins.some(
|
|
(plugin) => normalizeOptionalLowercaseString(plugin.id) === normalizedCommand,
|
|
);
|
|
if (commandIsPluginId) {
|
|
return undefined;
|
|
}
|
|
|
|
for (const plugin of params.registry.plugins) {
|
|
const alias = plugin.commandAliases?.find(
|
|
(entry) => normalizeOptionalLowercaseString(entry.name) === normalizedCommand,
|
|
);
|
|
if (alias) {
|
|
return {
|
|
...alias,
|
|
pluginId: plugin.id,
|
|
...(plugin.enabledByDefault === true ? { enabledByDefault: true } : {}),
|
|
};
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|