diff --git a/apps/docs/content/docs/cli/cli-reference.mdx b/apps/docs/content/docs/cli/cli-reference.mdx index 3565651f14d..a0619b5b1c7 100644 --- a/apps/docs/content/docs/cli/cli-reference.mdx +++ b/apps/docs/content/docs/cli/cli-reference.mdx @@ -606,7 +606,7 @@ List automations in the active organization. output="Automation" > Get an automation's metadata. The prompt body is omitted — use -[`automations prompt`](#superset-automations-prompt-id) to read it. +[`automations prompt get`](#superset-automations-prompt-get-id) to read it. Use [`automations logs`](#superset-automations-logs-id) for run history. @@ -659,33 +659,50 @@ superset automations create \ > Update an automation's metadata (name, schedule, agent, host). All flags optional. Omitting a flag preserves the existing value — `undefined` means -"no change", not "clear". Use [`automations prompt`](#superset-automations-prompt-id) -to read or replace the prompt body. +"no change", not "clear". Use [`automations prompt get`](#superset-automations-prompt-get-id) +or [`automations prompt set`](#superset-automations-prompt-set-id) to read or +replace the prompt body. +Print an automation's prompt body to stdout. The output is the raw prompt +with no trailing newline added, so `prompt get` and `prompt set` round-trip +byte-exactly. + +```bash +# Read to a file +superset automations prompt get aut_… > prompt.md + +# Verify a push landed +superset automations prompt get aut_… | diff - ./prompt.md +``` + + +", description: "Read the new prompt from a file. Use `-` for stdin." }, + { flag: "--from-file ", required: true, description: "Read the new prompt from a file. Use `-` for stdin." }, ]} - output="Markdown (read mode) or Automation (write mode)" + output="Automation" > -Read or replace an automation's prompt body. Without `--from-file`, prints -the current prompt to stdout. With `--from-file ` or piped stdin, -replaces the prompt verbatim. +Replace an automation's prompt body. The new prompt fully overwrites the +old one. ```bash -# Read -superset automations prompt aut_… > prompt.md - # Write from file -superset automations prompt aut_… --from-file ./prompt.md +superset automations prompt set aut_… --from-file ./prompt.md # Write from stdin -cat ./prompt.md | superset automations prompt aut_… +cat ./prompt.md | superset automations prompt set aut_… --from-file - ``` diff --git a/apps/docs/content/docs/sdk/reference.mdx b/apps/docs/content/docs/sdk/reference.mdx index bd8ba86f658..b1aa82768b4 100644 --- a/apps/docs/content/docs/sdk/reference.mdx +++ b/apps/docs/content/docs/sdk/reference.mdx @@ -187,7 +187,9 @@ Recurring agent runs scheduled by RRULE. Requires a Pro subscription on the org const automations = await client.automations.list(); ``` -Each row includes `scheduleText` — a human-readable rendering of the rrule. +Each row is an `AutomationSummary` — the `prompt` body is omitted (it can be +large markdown). Fetch one with `automations.getPrompt(id)`. Each row includes +`scheduleText`, a human-readable rendering of the rrule. ### `automations.retrieve(id)` @@ -195,6 +197,8 @@ Each row includes `scheduleText` — a human-readable rendering of the rrule. const a = await client.automations.retrieve(id); ``` +Returns an `AutomationSummary` (no `prompt` body — call `getPrompt(id)`). + ### `automations.create(body)` Create a recurring automation. diff --git a/packages/cli/src/commands/automations/get/command.ts b/packages/cli/src/commands/automations/get/command.ts index f25391e0fb1..c72d5da4956 100644 --- a/packages/cli/src/commands/automations/get/command.ts +++ b/packages/cli/src/commands/automations/get/command.ts @@ -6,10 +6,7 @@ export default command({ args: [positional("id").required().desc("Automation id")], run: async ({ ctx, args }) => { const id = args.id as string; - const result = await ctx.api.automation.get.query({ id }); - // Prompt is fetched via `superset automations prompt ` (it can be - // large markdown). Runs are paginated via `superset automations logs `. - const { prompt: _prompt, ...automation } = result; + const automation = await ctx.api.automation.get.query({ id }); return { data: automation }; }, }); diff --git a/packages/cli/src/commands/automations/prompt/command.ts b/packages/cli/src/commands/automations/prompt/command.ts deleted file mode 100644 index d0113a58b8d..00000000000 --- a/packages/cli/src/commands/automations/prompt/command.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { readFileSync } from "node:fs"; -import { positional, string } from "@superset/cli-framework"; -import { command } from "../../../lib/command"; - -async function readStdin(): Promise { - const chunks: Buffer[] = []; - for await (const chunk of process.stdin) { - chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk); - } - return Buffer.concat(chunks).toString("utf-8"); -} - -export default command({ - description: "Read or write an automation's prompt", - args: [positional("id").required().desc("Automation id")], - options: { - fromFile: string().desc( - "Path to a markdown file with the new prompt. Use '-' to read from stdin.", - ), - }, - run: async ({ ctx, args, options }) => { - const id = args.id as string; - const stdinIsPiped = !process.stdin.isTTY; - const useStdin = - options.fromFile === "-" || (!options.fromFile && stdinIsPiped); - - if (options.fromFile && options.fromFile !== "-") { - const next = readFileSync(options.fromFile, "utf-8"); - const result = await ctx.api.automation.setPrompt.mutate({ - id, - prompt: next, - }); - return { - data: { id: result.id, name: result.name, length: next.length }, - message: `Updated prompt for "${result.name}" (${next.length} chars).`, - }; - } - - if (useStdin) { - const next = await readStdin(); - if (!next.trim()) { - throw new Error("Refusing to write an empty prompt from stdin."); - } - const result = await ctx.api.automation.setPrompt.mutate({ - id, - prompt: next, - }); - return { - data: { id: result.id, name: result.name, length: next.length }, - message: `Updated prompt for "${result.name}" (${next.length} chars).`, - }; - } - - const { prompt } = await ctx.api.automation.getPrompt.query({ id }); - return { data: { id, prompt } }; - }, - display: (data) => { - const obj = data as { id: string; prompt?: string }; - return obj.prompt ?? ""; - }, -}); diff --git a/packages/cli/src/commands/automations/prompt/get/command.ts b/packages/cli/src/commands/automations/prompt/get/command.ts new file mode 100644 index 00000000000..b5900c62fa5 --- /dev/null +++ b/packages/cli/src/commands/automations/prompt/get/command.ts @@ -0,0 +1,24 @@ +import { isAgentMode, positional } from "@superset/cli-framework"; +import { command } from "../../../../lib/command"; + +export default command({ + description: "Print an automation's prompt to stdout", + args: [positional("id").required().desc("Automation id")], + run: async ({ ctx, args, options }) => { + const id = args.id as string; + const { prompt } = await ctx.api.automation.getPrompt.query({ id }); + // `--quiet` is intentionally ignored here: it would route through + // `extractIds` and emit only the UUID (which the caller already has + // as input), discarding the prompt body. Plain stdout is the right + // "machine-friendly" output for this command. + const globals = options as Record; + if (globals.json === true || isAgentMode()) { + return { data: { id, prompt } }; + } + // Default: write the raw prompt with no trailing newline so that + // `prompt get > out.md` round-trips byte-exactly with a + // subsequent `prompt set --from-file out.md`. + process.stdout.write(prompt ?? ""); + return undefined; + }, +}); diff --git a/packages/cli/src/commands/automations/prompt/meta.ts b/packages/cli/src/commands/automations/prompt/meta.ts new file mode 100644 index 00000000000..71d5919b53f --- /dev/null +++ b/packages/cli/src/commands/automations/prompt/meta.ts @@ -0,0 +1,3 @@ +export default { + description: "Read or write an automation's prompt", +}; diff --git a/packages/cli/src/commands/automations/prompt/set/command.ts b/packages/cli/src/commands/automations/prompt/set/command.ts new file mode 100644 index 00000000000..3baa55c71d1 --- /dev/null +++ b/packages/cli/src/commands/automations/prompt/set/command.ts @@ -0,0 +1,43 @@ +import { readFileSync } from "node:fs"; +import { positional, string } from "@superset/cli-framework"; +import { command } from "../../../../lib/command"; + +async function readStdin(): Promise { + const chunks: Buffer[] = []; + for await (const chunk of process.stdin) { + chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk); + } + return Buffer.concat(chunks).toString("utf-8"); +} + +export default command({ + description: "Replace an automation's prompt from a file or stdin", + args: [positional("id").required().desc("Automation id")], + options: { + fromFile: string() + .required() + .desc( + "Path to a markdown file with the new prompt. Use '-' to read from stdin.", + ), + }, + run: async ({ ctx, args, options }) => { + const id = args.id as string; + const next = + options.fromFile === "-" + ? await readStdin() + : readFileSync(options.fromFile, "utf-8"); + + if (!next.trim()) { + throw new Error("Refusing to write an empty prompt."); + } + + const result = await ctx.api.automation.setPrompt.mutate({ + id, + prompt: next, + }); + return { + data: { id: result.id, name: result.name, length: next.length }, + message: `Updated prompt for "${result.name}" (${next.length} chars).`, + }; + }, +}); diff --git a/packages/mcp-v2/src/tools/automations/get.ts b/packages/mcp-v2/src/tools/automations/get.ts index d21e0fa72ea..6a06324b96c 100644 --- a/packages/mcp-v2/src/tools/automations/get.ts +++ b/packages/mcp-v2/src/tools/automations/get.ts @@ -13,10 +13,7 @@ export function register(server: McpServer): void { }, handler: async (input, ctx) => { const caller = createMcpCaller(ctx); - const { prompt: _prompt, ...rest } = await caller.automation.get({ - id: input.id, - }); - return rest; + return await caller.automation.get({ id: input.id }); }, }); } diff --git a/packages/mcp-v2/src/tools/automations/list.ts b/packages/mcp-v2/src/tools/automations/list.ts index 32cc4243f59..09d686f582c 100644 --- a/packages/mcp-v2/src/tools/automations/list.ts +++ b/packages/mcp-v2/src/tools/automations/list.ts @@ -6,11 +6,10 @@ export function register(server: McpServer): void { defineTool(server, { name: "automations_list", description: - "List automations (scheduled agent runs) the calling user owns in the active organization. Returns a summary shape — call automations_get to fetch the full prompt and agentConfig for one automation.", + "List automations (scheduled agent runs) the calling user owns in the active organization. Returns a summary shape — call automations_get_prompt to fetch the prompt for one automation, or automations_get for the rest of its config.", handler: async (_input, ctx) => { const caller = createMcpCaller(ctx); - const rows = await caller.automation.list(); - return rows.map(({ prompt: _prompt, ...rest }) => rest); + return await caller.automation.list(); }, }); } diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index 5a4a44197ce..98ddc13f10e 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -59,6 +59,7 @@ import { AutomationRun, AutomationRunDispatched, Automations, + AutomationSummary, AutomationUpdateParams, } from "./resources/automations"; import { Host, HostListResponse, Hosts } from "./resources/hosts"; @@ -1138,6 +1139,7 @@ export declare namespace Superset { export { Automations, Automation, + AutomationSummary, AutomationListResponse, AutomationCreateParams, AutomationUpdateParams, diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 6d4cd708006..234a7ffc554 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -32,6 +32,7 @@ export { type AutomationRun, type AutomationRunDispatched, Automations, + type AutomationSummary, type AutomationUpdateParams, type Host, type HostListResponse, diff --git a/packages/sdk/src/resources/automations.ts b/packages/sdk/src/resources/automations.ts index 2ba293a081c..ccc995453ba 100644 --- a/packages/sdk/src/resources/automations.ts +++ b/packages/sdk/src/resources/automations.ts @@ -4,7 +4,8 @@ import type { RequestOptions } from "../internal/request-options"; export class Automations extends APIResource { /** - * List automations in the active organization. + * List automations in the active organization. Returned rows omit the + * `prompt` body — fetch one prompt with `getPrompt(id)`. * * Mirrors `superset automations list`. */ @@ -17,12 +18,20 @@ export class Automations extends APIResource { } /** - * Retrieve a single automation by id. + * Retrieve a single automation by id. The `prompt` body is omitted — + * fetch it separately with `getPrompt(id)`. * * Mirrors `superset automations get`. */ - retrieve(id: string, options?: RequestOptions): APIPromise { - return this._client.query("automation.get", { id }, options); + retrieve( + id: string, + options?: RequestOptions, + ): APIPromise { + return this._client.query( + "automation.get", + { id }, + options, + ); } /** @@ -125,9 +134,10 @@ export class Automations extends APIResource { } /** - * Get the prompt for an automation. + * Get the prompt body (markdown) for an automation. `retrieve` and + * `list` omit it because it can be large. * - * Mirrors `superset automations prompt --get`. + * Mirrors `superset automations prompt get`. */ getPrompt( id: string, @@ -141,9 +151,10 @@ export class Automations extends APIResource { } /** - * Update the prompt for an automation. + * Replace the prompt body for an automation. The new prompt fully + * overwrites the old one. * - * Mirrors `superset automations prompt`. + * Mirrors `superset automations prompt set`. */ setPrompt( id: string, @@ -165,12 +176,15 @@ export interface AgentConfig { [key: string]: unknown; } -export interface Automation { +/** + * Lean automation row returned by `list` and `retrieve`. The `prompt` + * body is omitted — call `getPrompt(id)` to fetch it. + */ +export interface AutomationSummary { id: string; organizationId: string; ownerUserId: string; name: string; - prompt: string; agentConfig: AgentConfig; targetHostId: string | null; v2ProjectId: string; @@ -187,7 +201,15 @@ export interface Automation { updatedAt: string; } -export type AutomationListResponse = Array; +/** + * Full automation row including the `prompt` body. Returned by mutations + * like `create`, `update`, `pause`, `resume`, and `setPrompt`. + */ +export interface Automation extends AutomationSummary { + prompt: string; +} + +export type AutomationListResponse = Array; export interface AutomationCreateParams { name: string; @@ -252,6 +274,7 @@ export interface AutomationRunDispatched { export declare namespace Automations { export type { Automation, + AutomationSummary, AutomationListResponse, AutomationCreateParams, AutomationUpdateParams, diff --git a/packages/sdk/src/resources/index.ts b/packages/sdk/src/resources/index.ts index 64380763d6b..562aaf9a0b4 100644 --- a/packages/sdk/src/resources/index.ts +++ b/packages/sdk/src/resources/index.ts @@ -8,6 +8,7 @@ export { type AutomationRun, type AutomationRunDispatched, Automations, + type AutomationSummary, type AutomationUpdateParams, } from "./automations"; export { type Host, type HostListResponse, Hosts } from "./hosts"; diff --git a/packages/trpc/src/router/automation/automation.ts b/packages/trpc/src/router/automation/automation.ts index 4b5e16652f5..ebf10651fd2 100644 --- a/packages/trpc/src/router/automation/automation.ts +++ b/packages/trpc/src/router/automation/automation.ts @@ -18,7 +18,7 @@ import { parseRrule, } from "@superset/shared/rrule"; import { TRPCError, type TRPCRouterRecord } from "@trpc/server"; -import { and, desc, eq } from "drizzle-orm"; +import { and, desc, eq, getTableColumns } from "drizzle-orm"; import { z } from "zod"; import { env } from "../../env"; import { protectedProcedure } from "../../trpc"; @@ -156,12 +156,16 @@ async function getAutomationForUser( } export const automationRouter = { - /** List automations scoped to the caller's active organization. */ + /** + * List automations scoped to the caller's active organization. The + * `prompt` body is omitted — call `getPrompt` to fetch it for one row. + */ list: protectedProcedure.query(async ({ ctx }) => { const organizationId = await requireActiveOrgMembership(ctx); + const { prompt: _prompt, ...summaryCols } = getTableColumns(automations); const rows = await db - .select() + .select(summaryCols) .from(automations) .where(eq(automations.organizationId, organizationId)) .orderBy(desc(automations.createdAt)); @@ -172,20 +176,36 @@ export const automationRouter = { })); }), - /** Get one automation. Use listRuns for run history. */ + /** + * Get one automation's metadata. The `prompt` body is omitted (it can be + * large markdown) — call `getPrompt` to fetch it. Use `listRuns` for + * run history. + */ get: protectedProcedure .input(z.object({ id: z.string().uuid() })) .query(async ({ ctx, input }) => { const organizationId = await requireActiveOrgMembership(ctx); - const automation = await getAutomationForUser( - ctx.session.user.id, - organizationId, - input.id, - ); - return { - ...automation, - scheduleText: safeDescribeRrule(automation), - }; + + const { prompt: _prompt, ...summaryCols } = getTableColumns(automations); + const [row] = await db + .select(summaryCols) + .from(automations) + .where( + and( + eq(automations.id, input.id), + eq(automations.organizationId, organizationId), + ), + ) + .limit(1); + + if (!row || row.ownerUserId !== ctx.session.user.id) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Automation not found", + }); + } + + return { ...row, scheduleText: safeDescribeRrule(row) }; }), create: protectedProcedure