diff --git a/docs/cli/cron.md b/docs/cli/cron.md index e0fcf17c75c2..f9db03d8ab91 100644 --- a/docs/cli/cron.md +++ b/docs/cli/cron.md @@ -52,6 +52,13 @@ openclaw automations create "*/15 * * * *" \ `--command ` stores `argv: ["sh", "-lc", ]`. Use `--command-argv '["node","scripts/report.mjs"]'` for exact argv execution. Command jobs capture stdout/stderr, record normal run history, and route output through the same `announce`, `webhook`, or `none` delivery modes as isolated jobs. A command that prints only `NO_REPLY` is suppressed. +Use `--display-name ` when the list and detail views should show a +human-readable label distinct from the automation's stable name. Set or update +that label with `automations add|edit --display-name`. Use +`automations edit --clear-display-name` to remove the label and restore +the stable name in list and detail views. The set and clear options cannot be +combined. + ## Sessions `--session` accepts `main`, `isolated`, `current`, or `session:`. diff --git a/src/cli/cron-cli/register.cron-edit.test.ts b/src/cli/cron-cli/register.cron-edit.test.ts index 10d7bab71621..ed695869ed83 100644 --- a/src/cli/cron-cli/register.cron-edit.test.ts +++ b/src/cli/cron-cli/register.cron-edit.test.ts @@ -53,9 +53,44 @@ describe("cron edit command", () => { const help = editCommand?.helpInformation() ?? ""; expect(help).toContain("--best-effort-deliver"); + expect(help).toContain("--display-name "); + expect(help).toContain("--clear-display-name"); expect(help).toMatch(/also\s+implies --announce when used alone/); }); + it("updates the human-readable display name without changing the job name", async () => { + await createCronProgram().parseAsync(["edit", "job-1", "--display-name", "Daily summary"], { + from: "user", + }); + + expect(callGatewayFromCli).toHaveBeenCalledWith("cron.update", expect.anything(), { + id: "job-1", + patch: { displayName: "Daily summary" }, + }); + }); + + it.each(["", " "])("rejects a blank --display-name value", async (value) => { + await expectCronEditRejection(["--display-name", value], "--display-name must not be blank"); + }); + + it("clears the display name and restores the stable name fallback", async () => { + await createCronProgram().parseAsync(["edit", "job-1", "--clear-display-name"], { + from: "user", + }); + + expect(callGatewayFromCli).toHaveBeenCalledWith("cron.update", expect.anything(), { + id: "job-1", + patch: { displayName: null }, + }); + }); + + it("rejects combining display-name set and clear flags", async () => { + await expectCronEditRejection( + ["--display-name", "Daily summary", "--clear-display-name"], + "Use --display-name or --clear-display-name, not both", + ); + }); + it("updates one pacing bound while preserving the other", async () => { callGatewayFromCli.mockImplementation(async (method: string) => { if (method === "cron.get") { diff --git a/src/cli/cron-cli/register.cron-edit.ts b/src/cli/cron-cli/register.cron-edit.ts index ba06d15201e4..0ce70f7779c9 100644 --- a/src/cli/cron-cli/register.cron-edit.ts +++ b/src/cli/cron-cli/register.cron-edit.ts @@ -59,6 +59,8 @@ export function registerCronEditCommand(cron: Command) { .description("Edit an automation (patch fields)") .argument("", "Job id") .option("--name ", "Set name") + .option("--display-name ", "Set human-readable display name") + .option("--clear-display-name", "Restore the stable name in list and detail views", false) .option("--description ", "Set description") .option("--enable", "Enable job", false) .option("--disable", "Disable job", false) @@ -243,6 +245,19 @@ export function registerCronEditCommand(cron: Command) { if (typeof opts.name === "string") { patch.name = opts.name; } + const displayName = normalizeOptionalString(opts.displayName); + if (typeof opts.displayName === "string" && !displayName) { + throw new Error("--display-name must not be blank"); + } + if (displayName && opts.clearDisplayName) { + throw new Error("Use --display-name or --clear-display-name, not both"); + } + if (displayName) { + patch.displayName = displayName; + } + if (opts.clearDisplayName) { + patch.displayName = null; + } if (typeof opts.description === "string") { patch.description = opts.description; }