mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 06:02:53 +00:00
perf(cli): slim agent command registration
This commit is contained in:
@@ -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([
|
||||
{
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
108
src/cli/program/register.agent-turn.ts
Normal file
108
src/cli/program/register.agent-turn.ts
Normal file
@@ -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<AgentViaGatewayModule["agentCliCommand"]> {
|
||||
return (await import("../../commands/agent-via-gateway.js")).agentCliCommand;
|
||||
}
|
||||
|
||||
async function loadDefaultRuntime(): Promise<RuntimeModule["defaultRuntime"]> {
|
||||
return (await import("../../runtime.js")).defaultRuntime;
|
||||
}
|
||||
|
||||
async function loadRunCommandWithRuntime(): Promise<CliUtilsModule["runCommandWithRuntime"]> {
|
||||
return (await import("../cli-utils.js")).runCommandWithRuntime;
|
||||
}
|
||||
|
||||
async function loadSetVerbose(): Promise<GlobalStateModule["setVerbose"]> {
|
||||
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 <text>", "Message body for the agent")
|
||||
.option("-t, --to <number>", "Recipient number in E.164 used to derive the session key")
|
||||
.option("--session-key <key>", "Explicit session key (agent:<id>:<key>, or scoped to --agent)")
|
||||
.option("--session-id <id>", "Use an explicit session id")
|
||||
.option("--agent <id>", "Agent id (overrides routing bindings)")
|
||||
.option("--model <id>", "Model override for this run (provider/model or model id)")
|
||||
.option(
|
||||
"--thinking <level>",
|
||||
"Thinking level: off | minimal | low | medium | high | xhigh | adaptive | max where supported",
|
||||
)
|
||||
.option("--verbose <on|off>", "Persist agent verbose level for the session")
|
||||
.option(
|
||||
"--channel <channel>",
|
||||
`Delivery channel: ${args.agentChannelOptions} (omit to use the main session channel)`,
|
||||
)
|
||||
.option("--reply-to <target>", "Delivery target override (separate from session routing)")
|
||||
.option("--reply-channel <channel>", "Delivery channel override (separate from routing)")
|
||||
.option("--reply-account <id>", "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 <seconds>",
|
||||
"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<void> => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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<AgentsBindModule> | undefined;
|
||||
|
||||
@@ -22,10 +20,6 @@ function loadAgentsBindModule(): Promise<AgentsBindModule> {
|
||||
return (agentsBindModulePromise ??= import("../../commands/agents.commands.bind.js"));
|
||||
}
|
||||
|
||||
async function loadAgentCliCommand(): Promise<AgentViaGatewayModule["agentCliCommand"]> {
|
||||
return (await import("../../commands/agent-via-gateway.js")).agentCliCommand;
|
||||
}
|
||||
|
||||
async function loadAgentsAddCommand(): Promise<AgentsAddModule["agentsAddCommand"]> {
|
||||
return (await import("../../commands/agents.commands.add.js")).agentsAddCommand;
|
||||
}
|
||||
@@ -56,86 +50,35 @@ async function loadAgentsListCommand(): Promise<AgentsListModule["agentsListComm
|
||||
return (await import("../../commands/agents.commands.list.js")).agentsListCommand;
|
||||
}
|
||||
|
||||
async function loadSetVerbose(): Promise<GlobalStateModule["setVerbose"]> {
|
||||
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<void>,
|
||||
): Promise<void> {
|
||||
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 <text>", "Message body for the agent")
|
||||
.option("-t, --to <number>", "Recipient number in E.164 used to derive the session key")
|
||||
.option("--session-key <key>", "Explicit session key (agent:<id>:<key>, or scoped to --agent)")
|
||||
.option("--session-id <id>", "Use an explicit session id")
|
||||
.option("--agent <id>", "Agent id (overrides routing bindings)")
|
||||
.option("--model <id>", "Model override for this run (provider/model or model id)")
|
||||
.option(
|
||||
"--thinking <level>",
|
||||
"Thinking level: off | minimal | low | medium | high | xhigh | adaptive | max where supported",
|
||||
)
|
||||
.option("--verbose <on|off>", "Persist agent verbose level for the session")
|
||||
.option(
|
||||
"--channel <channel>",
|
||||
`Delivery channel: ${args.agentChannelOptions} (omit to use the main session channel)`,
|
||||
)
|
||||
.option("--reply-to <target>", "Delivery target override (separate from session routing)")
|
||||
.option("--reply-channel <channel>", "Delivery channel override (separate from routing)")
|
||||
.option("--reply-account <id>", "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 <seconds>",
|
||||
"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<void> => {
|
||||
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<void> => {
|
||||
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 <id>", "Filter by agent id")
|
||||
.option("--json", "Output JSON instead of text", false)
|
||||
.action(async (opts): Promise<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
await runCommandWithRuntime(defaultRuntime, async () => {
|
||||
await runAgentsCommandAction(async (runtime) => {
|
||||
const agentsListCommand = await loadAgentsListCommand();
|
||||
await agentsListCommand({}, defaultRuntime);
|
||||
await agentsListCommand({}, runtime);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user