From 6fb1f386c6d6adc2e2fd61c82824f6fac7ab555e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 31 May 2026 11:14:05 +0100 Subject: [PATCH] perf(cli): slim agent command registration --- src/cli/help-cold-imports.test.ts | 1 + src/cli/program/command-registry-core.ts | 13 ++- src/cli/program/command-registry.test.ts | 23 ++-- src/cli/program/register.agent-turn.ts | 108 ++++++++++++++++++ src/cli/program/register.agent.ts | 139 +++++++---------------- 5 files changed, 175 insertions(+), 109 deletions(-) create mode 100644 src/cli/program/register.agent-turn.ts diff --git a/src/cli/help-cold-imports.test.ts b/src/cli/help-cold-imports.test.ts index afaad21e045..a74f671c954 100644 --- a/src/cli/help-cold-imports.test.ts +++ b/src/cli/help-cold-imports.test.ts @@ -332,6 +332,7 @@ describe("subcommand help cold imports", () => { expect(loaded.modules).not.toContain("agents-delete-command"); expect(loaded.modules).not.toContain("agents-identity-command"); expect(loaded.modules).not.toContain("agents-list-command"); + expect(loaded.modules).not.toContain("default-runtime"); }); it("keeps secrets help out of secrets action modules", async () => { diff --git a/src/cli/program/command-registry-core.ts b/src/cli/program/command-registry-core.ts index 084a697cbbe..da7d9189fb5 100644 --- a/src/cli/program/command-registry-core.ts +++ b/src/cli/program/command-registry-core.ts @@ -115,14 +115,21 @@ const coreEntrySpecs: readonly CommandGroupDescriptorSpec< ]), ), defineImportedCommandGroupSpec( - ["agent", "agents"], - () => import("./register.agent.js"), + ["agent"], + () => import("./register.agent-turn.js"), (mod, { program, ctx }) => { - mod.registerAgentCommands(program, { + mod.registerAgentTurnCommand(program, { agentChannelOptions: ctx.agentChannelOptions, }); }, ), + defineImportedCommandGroupSpec( + ["agents"], + () => import("./register.agent.js"), + (mod, { program }) => { + mod.registerAgentsCommands(program); + }, + ), ...withProgramOnlySpecs( defineImportedProgramCommandGroupSpecs([ { diff --git a/src/cli/program/command-registry.test.ts b/src/cli/program/command-registry.test.ts index 231d1f09dd9..311dcd37685 100644 --- a/src/cli/program/command-registry.test.ts +++ b/src/cli/program/command-registry.test.ts @@ -5,12 +5,17 @@ import type { ProgramContext } from "./context.js"; // Perf: `registerCoreCliByName(...)` dynamically imports registrar modules. // Mock the heavy registrars so this suite stays focused on command-registry wiring. vi.mock("./register.agent.js", () => ({ - registerAgentCommands: (program: Command) => { - program.command("agent"); + registerAgentsCommands: (program: Command) => { program.command("agents"); }, })); +vi.mock("./register.agent-turn.js", () => ({ + registerAgentTurnCommand: (program: Command) => { + program.command("agent"); + }, +})); + vi.mock("./register.backup.js", () => ({ registerBackupCommand: (program: Command) => { const backup = program.command("backup"); @@ -95,15 +100,17 @@ describe("command-registry", () => { expect(names).not.toContain("doctor"); }); - it("registerCoreCliByName resolves agents to the agent entry", async () => { + it("registerCoreCliByName resolves agent and agents separately", async () => { const program = createProgram(); const found = await registerCoreCliByName(program, testProgramContext, "agents"); expect(found).toBe(true); - // The registrar also installs the singular "agent" command from the same entry. - expect(program.commands.map((command) => command.name()).toSorted()).toEqual([ - "agent", - "agents", - ]); + expect(program.commands.map((command) => command.name())).toEqual(["agents"]); + + const agentProgram = createProgram(); + await expect(registerCoreCliByName(agentProgram, testProgramContext, "agent")).resolves.toBe( + true, + ); + expect(agentProgram.commands.map((command) => command.name())).toEqual(["agent"]); }); it("registerCoreCliByName returns false for unknown commands", async () => { diff --git a/src/cli/program/register.agent-turn.ts b/src/cli/program/register.agent-turn.ts new file mode 100644 index 00000000000..d765c91b22c --- /dev/null +++ b/src/cli/program/register.agent-turn.ts @@ -0,0 +1,108 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; +import type { Command } from "commander"; +import { formatDocsLink } from "../../../packages/terminal-core/src/links.js"; +import { theme } from "../../../packages/terminal-core/src/theme.js"; +import { formatHelpExamples } from "../help-format.js"; + +type AgentViaGatewayModule = typeof import("../../commands/agent-via-gateway.js"); +type CliUtilsModule = typeof import("../cli-utils.js"); +type GlobalStateModule = typeof import("../../global-state.js"); +type RuntimeModule = typeof import("../../runtime.js"); + +async function loadAgentCliCommand(): Promise { + return (await import("../../commands/agent-via-gateway.js")).agentCliCommand; +} + +async function loadDefaultRuntime(): Promise { + return (await import("../../runtime.js")).defaultRuntime; +} + +async function loadRunCommandWithRuntime(): Promise { + return (await import("../cli-utils.js")).runCommandWithRuntime; +} + +async function loadSetVerbose(): Promise { + return (await import("../../global-state.js")).setVerbose; +} + +export function registerAgentTurnCommand( + program: Command, + args: { agentChannelOptions: string }, +): void { + program + .command("agent") + .description("Run an agent turn via the Gateway (use --local for embedded)") + .requiredOption("-m, --message ", "Message body for the agent") + .option("-t, --to ", "Recipient number in E.164 used to derive the session key") + .option("--session-key ", "Explicit session key (agent::, or scoped to --agent)") + .option("--session-id ", "Use an explicit session id") + .option("--agent ", "Agent id (overrides routing bindings)") + .option("--model ", "Model override for this run (provider/model or model id)") + .option( + "--thinking ", + "Thinking level: off | minimal | low | medium | high | xhigh | adaptive | max where supported", + ) + .option("--verbose ", "Persist agent verbose level for the session") + .option( + "--channel ", + `Delivery channel: ${args.agentChannelOptions} (omit to use the main session channel)`, + ) + .option("--reply-to ", "Delivery target override (separate from session routing)") + .option("--reply-channel ", "Delivery channel override (separate from routing)") + .option("--reply-account ", "Delivery account id override") + .option( + "--local", + "Run the embedded agent locally (requires model provider API keys in your shell)", + false, + ) + .option("--deliver", "Send the agent's reply back to the selected channel", false) + .option("--json", "Output result as JSON", false) + .option( + "--timeout ", + "Override agent command timeout (seconds, default 600 or config value)", + ) + .addHelpText( + "after", + () => + ` +${theme.heading("Examples:")} +${formatHelpExamples([ + ['openclaw agent --to +15555550123 --message "status update"', "Start a new session."], + ['openclaw agent --agent ops --message "Summarize logs"', "Use a specific agent."], + [ + 'openclaw agent --session-key agent:ops:incident-42 --message "Summarize status"', + "Target an exact session key.", + ], + [ + 'openclaw agent --session-id 1234 --message "Summarize inbox" --thinking medium', + "Target a session with explicit thinking level.", + ], + [ + 'openclaw agent --to +15555550123 --message "Trace logs" --verbose on --json', + "Enable verbose logging and JSON output.", + ], + ['openclaw agent --to +15555550123 --message "Summon reply" --deliver', "Deliver reply."], + [ + 'openclaw agent --agent ops --message "Generate report" --deliver --reply-channel slack --reply-to "#reports"', + "Send reply to a different channel/target.", + ], +])} + +${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/agent")}`, + ) + .action(async (opts): Promise => { + const verboseLevel = + typeof opts.verbose === "string" ? normalizeLowercaseStringOrEmpty(opts.verbose) : ""; + const [defaultRuntime, runCommandWithRuntime, setVerbose, agentCliCommand] = + await Promise.all([ + loadDefaultRuntime(), + loadRunCommandWithRuntime(), + loadSetVerbose(), + loadAgentCliCommand(), + ]); + await runCommandWithRuntime(defaultRuntime, async () => { + setVerbose(verboseLevel === "on"); + await agentCliCommand(opts, defaultRuntime); + }); + }); +} diff --git a/src/cli/program/register.agent.ts b/src/cli/program/register.agent.ts index 7e23bcbf28d..62208195ed1 100644 --- a/src/cli/program/register.agent.ts +++ b/src/cli/program/register.agent.ts @@ -1,20 +1,18 @@ -import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import type { Command } from "commander"; import { formatDocsLink } from "../../../packages/terminal-core/src/links.js"; import { theme } from "../../../packages/terminal-core/src/theme.js"; -import { defaultRuntime } from "../../runtime.js"; -import { runCommandWithRuntime } from "../cli-utils.js"; import { hasExplicitOptions } from "../command-options.js"; import { formatHelpExamples } from "../help-format.js"; import { collectOption } from "./helpers.js"; +import { registerAgentTurnCommand } from "./register.agent-turn.js"; -type AgentViaGatewayModule = typeof import("../../commands/agent-via-gateway.js"); type AgentsAddModule = typeof import("../../commands/agents.commands.add.js"); type AgentsBindModule = typeof import("../../commands/agents.commands.bind.js"); type AgentsDeleteModule = typeof import("../../commands/agents.commands.delete.js"); type AgentsIdentityModule = typeof import("../../commands/agents.commands.identity.js"); type AgentsListModule = typeof import("../../commands/agents.commands.list.js"); -type GlobalStateModule = typeof import("../../global-state.js"); +type CliUtilsModule = typeof import("../cli-utils.js"); +type RuntimeModule = typeof import("../../runtime.js"); let agentsBindModulePromise: Promise | undefined; @@ -22,10 +20,6 @@ function loadAgentsBindModule(): Promise { return (agentsBindModulePromise ??= import("../../commands/agents.commands.bind.js")); } -async function loadAgentCliCommand(): Promise { - return (await import("../../commands/agent-via-gateway.js")).agentCliCommand; -} - async function loadAgentsAddCommand(): Promise { return (await import("../../commands/agents.commands.add.js")).agentsAddCommand; } @@ -56,86 +50,35 @@ async function loadAgentsListCommand(): Promise { - return (await import("../../global-state.js")).setVerbose; +async function loadAgentsActionRuntime(): Promise<{ + defaultRuntime: RuntimeModule["defaultRuntime"]; + runCommandWithRuntime: CliUtilsModule["runCommandWithRuntime"]; +}> { + const [{ defaultRuntime }, { runCommandWithRuntime }] = await Promise.all([ + import("../../runtime.js"), + import("../cli-utils.js"), + ]); + return { defaultRuntime, runCommandWithRuntime }; +} + +async function runAgentsCommandAction( + action: (runtime: RuntimeModule["defaultRuntime"]) => Promise, +): Promise { + const { defaultRuntime, runCommandWithRuntime } = await loadAgentsActionRuntime(); + await runCommandWithRuntime(defaultRuntime, async () => { + await action(defaultRuntime); + }); } export function registerAgentCommands( program: Command, args: { agentChannelOptions: string }, ): void { - program - .command("agent") - .description("Run an agent turn via the Gateway (use --local for embedded)") - .requiredOption("-m, --message ", "Message body for the agent") - .option("-t, --to ", "Recipient number in E.164 used to derive the session key") - .option("--session-key ", "Explicit session key (agent::, or scoped to --agent)") - .option("--session-id ", "Use an explicit session id") - .option("--agent ", "Agent id (overrides routing bindings)") - .option("--model ", "Model override for this run (provider/model or model id)") - .option( - "--thinking ", - "Thinking level: off | minimal | low | medium | high | xhigh | adaptive | max where supported", - ) - .option("--verbose ", "Persist agent verbose level for the session") - .option( - "--channel ", - `Delivery channel: ${args.agentChannelOptions} (omit to use the main session channel)`, - ) - .option("--reply-to ", "Delivery target override (separate from session routing)") - .option("--reply-channel ", "Delivery channel override (separate from routing)") - .option("--reply-account ", "Delivery account id override") - .option( - "--local", - "Run the embedded agent locally (requires model provider API keys in your shell)", - false, - ) - .option("--deliver", "Send the agent's reply back to the selected channel", false) - .option("--json", "Output result as JSON", false) - .option( - "--timeout ", - "Override agent command timeout (seconds, default 600 or config value)", - ) - .addHelpText( - "after", - () => - ` -${theme.heading("Examples:")} -${formatHelpExamples([ - ['openclaw agent --to +15555550123 --message "status update"', "Start a new session."], - ['openclaw agent --agent ops --message "Summarize logs"', "Use a specific agent."], - [ - 'openclaw agent --session-key agent:ops:incident-42 --message "Summarize status"', - "Target an exact session key.", - ], - [ - 'openclaw agent --session-id 1234 --message "Summarize inbox" --thinking medium', - "Target a session with explicit thinking level.", - ], - [ - 'openclaw agent --to +15555550123 --message "Trace logs" --verbose on --json', - "Enable verbose logging and JSON output.", - ], - ['openclaw agent --to +15555550123 --message "Summon reply" --deliver', "Deliver reply."], - [ - 'openclaw agent --agent ops --message "Generate report" --deliver --reply-channel slack --reply-to "#reports"', - "Send reply to a different channel/target.", - ], -])} - -${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/agent")}`, - ) - .action(async (opts): Promise => { - const verboseLevel = - typeof opts.verbose === "string" ? normalizeLowercaseStringOrEmpty(opts.verbose) : ""; - await runCommandWithRuntime(defaultRuntime, async () => { - const setVerbose = await loadSetVerbose(); - setVerbose(verboseLevel === "on"); - const agentCliCommand = await loadAgentCliCommand(); - await agentCliCommand(opts, defaultRuntime); - }); - }); + registerAgentTurnCommand(program, args); + registerAgentsCommands(program); +} +export function registerAgentsCommands(program: Command): void { const agents = program .command("agents") .description("Manage isolated agents (workspaces + auth + routing)") @@ -151,11 +94,11 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age .option("--json", "Output JSON instead of text", false) .option("--bindings", "Include routing bindings", false) .action(async (opts): Promise => { - await runCommandWithRuntime(defaultRuntime, async () => { + await runAgentsCommandAction(async (runtime) => { const agentsListCommand = await loadAgentsListCommand(); await agentsListCommand( { json: Boolean(opts.json), bindings: Boolean(opts.bindings) }, - defaultRuntime, + runtime, ); }); }); @@ -166,14 +109,14 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age .option("--agent ", "Filter by agent id") .option("--json", "Output JSON instead of text", false) .action(async (opts): Promise => { - await runCommandWithRuntime(defaultRuntime, async () => { + await runAgentsCommandAction(async (runtime) => { const agentsBindingsCommand = await loadAgentsBindingsCommand(); await agentsBindingsCommand( { agent: opts.agent as string | undefined, json: Boolean(opts.json), }, - defaultRuntime, + runtime, ); }); }); @@ -190,7 +133,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age ) .option("--json", "Output JSON summary", false) .action(async (opts): Promise => { - await runCommandWithRuntime(defaultRuntime, async () => { + await runAgentsCommandAction(async (runtime) => { const agentsBindCommand = await loadAgentsBindCommand(); await agentsBindCommand( { @@ -198,7 +141,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age bind: Array.isArray(opts.bind) ? (opts.bind as string[]) : undefined, json: Boolean(opts.json), }, - defaultRuntime, + runtime, ); }); }); @@ -211,7 +154,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age .option("--all", "Remove all bindings for this agent", false) .option("--json", "Output JSON summary", false) .action(async (opts): Promise => { - await runCommandWithRuntime(defaultRuntime, async () => { + await runAgentsCommandAction(async (runtime) => { const agentsUnbindCommand = await loadAgentsUnbindCommand(); await agentsUnbindCommand( { @@ -220,7 +163,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age all: Boolean(opts.all), json: Boolean(opts.json), }, - defaultRuntime, + runtime, ); }); }); @@ -235,7 +178,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age .option("--non-interactive", "Disable prompts; requires --workspace", false) .option("--json", "Output JSON summary", false) .action(async (name, opts, command): Promise => { - await runCommandWithRuntime(defaultRuntime, async () => { + await runAgentsCommandAction(async (runtime) => { const hasFlags = hasExplicitOptions(command, [ "workspace", "model", @@ -254,7 +197,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/age nonInteractive: Boolean(opts.nonInteractive), json: Boolean(opts.json), }, - defaultRuntime, + runtime, { hasFlags }, ); }); @@ -292,7 +235,7 @@ ${formatHelpExamples([ `, ) .action(async (opts): Promise => { - await runCommandWithRuntime(defaultRuntime, async () => { + await runAgentsCommandAction(async (runtime) => { const agentsSetIdentityCommand = await loadAgentsSetIdentityCommand(); await agentsSetIdentityCommand( { @@ -306,7 +249,7 @@ ${formatHelpExamples([ avatar: opts.avatar as string | undefined, json: Boolean(opts.json), }, - defaultRuntime, + runtime, ); }); }); @@ -317,7 +260,7 @@ ${formatHelpExamples([ .option("--force", "Skip confirmation", false) .option("--json", "Output JSON summary", false) .action(async (id, opts): Promise => { - await runCommandWithRuntime(defaultRuntime, async () => { + await runAgentsCommandAction(async (runtime) => { const agentsDeleteCommand = await loadAgentsDeleteCommand(); await agentsDeleteCommand( { @@ -325,15 +268,15 @@ ${formatHelpExamples([ force: Boolean(opts.force), json: Boolean(opts.json), }, - defaultRuntime, + runtime, ); }); }); agents.action(async (): Promise => { - await runCommandWithRuntime(defaultRuntime, async () => { + await runAgentsCommandAction(async (runtime) => { const agentsListCommand = await loadAgentsListCommand(); - await agentsListCommand({}, defaultRuntime); + await agentsListCommand({}, runtime); }); }); }