@checkstack/automation-frontend 0.4.8 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,121 @@
1
1
  # @checkstack/automation-frontend
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6005271: Add AI "skills" - reusable prompt templates for the chat assistant and the
8
+ `ai_analyze` automation action. A skill bundles a system-prompt fragment, an
9
+ optional starter prompt, and (for analyze) suggested output fields, tagged with
10
+ the surfaces it targets.
11
+
12
+ Skills come from two sources merged into one catalogue: builtin skills
13
+ contributed by core/plugins via the new `aiSkillExtensionPoint`, and GLOBAL
14
+ user skills authored by operators (new `ai_skill` table) and visible to everyone
15
+ who can read skills. New access rules `ai.skill.read`, `ai.skill-create.manage`
16
+ (a dedicated create permission), and `ai.skill.manage` (edit/delete, author-only
17
+ with admin moderation) gate the feature - all default-on, admin-revocable.
18
+
19
+ The chat composer gains a skill picker (its system prompt seeds the turn, its
20
+ starter prompt seeds the message box); the `ai_analyze` action gains an optional
21
+ `skillId` that seeds the system prompt, prompt (when blank), and output fields
22
+ (when none) - explicit config always wins. A new "AI skills" settings page lets
23
+ operators browse, view full details (prompts + output fields), publish, edit,
24
+ and delete their global skills. Ships six builtin skills across chat and analyze.
25
+
26
+ To support rich pickers, `@checkstack/ui`'s `DynamicForm` gains a `catalog`
27
+ options style (`x-options-style: "catalog"`, with resolver options carrying an
28
+ optional `description`) that renders a browsable modal of cards instead of a
29
+ plain Select, and `@checkstack/backend-api` propagates the new annotation. The
30
+ shared `PageHeader` now wraps a long subtitle beside its actions instead of
31
+ letting them overlap.
32
+
33
+ - 748268c: Add an example-automation template catalogue. Creating a new automation now
34
+ opens a picker (`/automation/new`) with curated, ready-to-use starting points
35
+ grouped by category, plus a "Blank automation" option. Selecting a template
36
+ seeds the editor (the operator still chooses a service account and saves).
37
+
38
+ Templates are an extensible registry: external plugins contribute their own via
39
+ the new `automationTemplateExtensionPoint`, exactly like actions / triggers /
40
+ artifact types. Every registered template is validated against the LIVE
41
+ trigger/action/artifact registries at server startup - a template that
42
+ references a capability that is not installed is withheld with a console
43
+ warning, and one whose definition no longer validates (interface drift) is
44
+ withheld with a console error - so a template can never silently drift when an
45
+ action, trigger, condition, or artifact interface changes.
46
+
47
+ Ships five built-in templates spanning incident response and alerting
48
+ (AI-triage-and-file-Jira-bug, close-Jira-on-recovery, AI-summarize-incident,
49
+ page-on-call-on-sustained-degradation, AI-severity-escalation).
50
+
51
+ ### Patch Changes
52
+
53
+ - Updated dependencies [4134ed9]
54
+ - Updated dependencies [6005271]
55
+ - Updated dependencies [748268c]
56
+ - Updated dependencies [4134ed9]
57
+ - Updated dependencies [079369a]
58
+ - @checkstack/ai-common@0.4.0
59
+ - @checkstack/ui@1.16.0
60
+ - @checkstack/automation-common@0.6.0
61
+ - @checkstack/auth-common@0.9.1
62
+ - @checkstack/template-engine@0.4.4
63
+ - @checkstack/gitops-frontend@0.5.7
64
+ - @checkstack/script-packages-frontend@0.3.10
65
+ - @checkstack/secrets-frontend@0.2.6
66
+ - @checkstack/catalog-common@2.3.6
67
+
68
+ ## 0.5.0
69
+
70
+ ### Minor Changes
71
+
72
+ - ebef442: feat(ai): let the assistant resolve dynamic integration-action field values
73
+
74
+ Integration action fields like Jira `create_issue`'s `projectKey`, `issueTypeId`,
75
+ and `priorityId` are not free-form - their valid values come from the connected
76
+ system (the editor renders them as cascading dropdowns via `x-options-resolver`).
77
+ The AI assistant had no way to fetch those values, so it guessed, and propose-time
78
+ validation never checked them - a fabricated `projectKey` only failed at runtime.
79
+
80
+ - **New user-callable `integration.resolveConnectionOptions` RPC** (the non-admin
81
+ counterpart of `getConnectionOptions`, mirroring `listConnectionSummaries`), so
82
+ automation authors and the assistant can resolve a field's options without
83
+ `integration.manage`. Returns option labels/values only.
84
+ - **New `automation.resolveActionOptions` AI tool**: resolves a field's valid
85
+ values live from the connection, the same source the editor dropdown uses. It
86
+ is provider-agnostic (reads the field's resolver and `x-depends-on` from the
87
+ action's own schema) and dependency-aware - for a cascade like `issueTypeId`
88
+ (depends on `projectKey`), the model resolves the parent first and passes it in
89
+ `dependencies`.
90
+ - **Propose-time options validation**: `automation.propose` now checks every
91
+ literal dynamic-option value against the live options for its connection
92
+ (sourcing each field's dependency values from the same config so cascades
93
+ resolve), flagging values the connection does not offer with guidance to call
94
+ `automation.resolveActionOptions`. Templated values and fields with
95
+ templated/absent dependencies are skipped; a resolver lookup failure is skipped
96
+ rather than blocking, so transient provider flakiness never gates a proposal.
97
+
98
+ - **Automation editor works for non-admins**: the editor's option-resolver
99
+ bridge now calls the user-callable `listConnectionSummaries` /
100
+ `resolveConnectionOptions` instead of the admin-gated `listConnections` /
101
+ `getConnectionOptions`, so an automation author without `integration.manage`
102
+ gets working connection pickers and cascading dropdowns instead of empty/
103
+ forbidden ones.
104
+
105
+ The resolver lookup and the dependency handling are factored into reusable
106
+ helpers that work for any provider's `x-options-resolver` fields.
107
+
108
+ ### Patch Changes
109
+
110
+ - Updated dependencies [ebef442]
111
+ - Updated dependencies [ebef442]
112
+ - @checkstack/integration-common@0.9.0
113
+ - @checkstack/automation-common@0.5.0
114
+ - @checkstack/auth-common@0.9.0
115
+ - @checkstack/catalog-common@2.3.5
116
+ - @checkstack/script-packages-frontend@0.3.9
117
+ - @checkstack/gitops-frontend@0.5.6
118
+
3
119
  ## 0.4.8
4
120
 
5
121
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/automation-frontend",
3
- "version": "0.4.8",
3
+ "version": "0.6.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.tsx",
@@ -13,18 +13,19 @@
13
13
  "lint:code": "eslint . --max-warnings 0"
14
14
  },
15
15
  "dependencies": {
16
- "@checkstack/auth-common": "0.8.3",
17
- "@checkstack/automation-common": "0.4.3",
18
- "@checkstack/catalog-common": "2.3.4",
16
+ "@checkstack/auth-common": "0.9.1",
17
+ "@checkstack/automation-common": "0.6.0",
18
+ "@checkstack/ai-common": "0.4.0",
19
+ "@checkstack/catalog-common": "2.3.6",
19
20
  "@checkstack/common": "0.15.0",
20
21
  "@checkstack/frontend-api": "0.9.0",
21
- "@checkstack/gitops-frontend": "0.5.5",
22
- "@checkstack/integration-common": "0.8.0",
23
- "@checkstack/script-packages-frontend": "0.3.8",
24
- "@checkstack/secrets-frontend": "0.2.5",
22
+ "@checkstack/gitops-frontend": "0.5.7",
23
+ "@checkstack/integration-common": "0.9.0",
24
+ "@checkstack/script-packages-frontend": "0.3.10",
25
+ "@checkstack/secrets-frontend": "0.2.6",
25
26
  "@checkstack/signal-frontend": "0.2.4",
26
- "@checkstack/template-engine": "0.4.3",
27
- "@checkstack/ui": "1.15.1",
27
+ "@checkstack/template-engine": "0.4.4",
28
+ "@checkstack/ui": "1.16.0",
28
29
  "@dnd-kit/core": "^6.3.1",
29
30
  "@dnd-kit/sortable": "^8.0.0",
30
31
  "@dnd-kit/utilities": "^3.2.2",
@@ -32,7 +32,7 @@ import type {
32
32
  import { useAutomationRegistry } from "./registry-context";
33
33
  import { ItemPicker } from "./ItemPicker";
34
34
  import { ConditionEditor } from "./ConditionEditor";
35
- import { useConnectionOptionResolvers } from "./useConnectionOptionResolvers";
35
+ import { useActionOptionResolvers } from "./useActionOptionResolvers";
36
36
  import { automationScriptTestRenderer } from "./ScriptTestRenderer";
37
37
  import {
38
38
  useScriptPackageTypeAcquisition,
@@ -127,7 +127,7 @@ export const ProviderActionBody: React.FC<{
127
127
  // `connectionProviderId`; the bridge turns their `x-options-resolver`
128
128
  // fields into a live connection picker + cascading provider dropdowns.
129
129
  // Non-connection actions get an empty map (no-op) since the id is absent.
130
- const optionsResolvers = useConnectionOptionResolvers(
130
+ const optionsResolvers = useActionOptionResolvers(
131
131
  selected?.connectionProviderId,
132
132
  );
133
133
 
@@ -0,0 +1,51 @@
1
+ import React from "react";
2
+ import { usePluginClient } from "@checkstack/frontend-api";
3
+ import { AiApi } from "@checkstack/ai-common";
4
+ import type { OptionsResolver } from "@checkstack/ui";
5
+ import { useConnectionOptionResolvers } from "./useConnectionOptionResolvers";
6
+
7
+ /**
8
+ * The `x-options-resolver` name the `ai_analyze` action declares for its
9
+ * `skillId` field. Resolved here (NOT through the connection bridge) because
10
+ * skills are not connection-scoped: the connection bridge returns `[]` whenever
11
+ * no `connectionId` is selected, which would wrongly empty the skill dropdown.
12
+ */
13
+ const AI_SKILL_RESOLVER_NAME = "aiSkillOptions";
14
+
15
+ /**
16
+ * Resolver map for an action editor's `x-options-resolver` config fields.
17
+ *
18
+ * Composes the connection-backed resolvers (connection picker + cascading
19
+ * provider dropdowns) with non-connection resolvers that need their own data
20
+ * source. Today the only non-connection resolver is `aiSkillOptions`, which
21
+ * lists the AI skills targeting the `ai_analyze` action via `listSkills` -
22
+ * independent of any selected connection.
23
+ */
24
+ export function useActionOptionResolvers(
25
+ connectionProviderId?: string,
26
+ ): Record<string, OptionsResolver> {
27
+ const connectionResolvers = useConnectionOptionResolvers(connectionProviderId);
28
+ const aiClient = usePluginClient(AiApi);
29
+
30
+ return React.useMemo(() => {
31
+ const skillResolver: OptionsResolver = async () => {
32
+ const { skills } = await aiClient.listSkills.call({ target: "ai_analyze" });
33
+ return skills.map((skill) => ({
34
+ value: skill.id,
35
+ label: skill.name,
36
+ description: skill.description,
37
+ }));
38
+ };
39
+
40
+ return new Proxy<Record<string, OptionsResolver>>(
41
+ {},
42
+ {
43
+ get: (_target, prop) => {
44
+ if (prop === AI_SKILL_RESOLVER_NAME) return skillResolver;
45
+ if (typeof prop !== "string") return;
46
+ return connectionResolvers[prop];
47
+ },
48
+ },
49
+ );
50
+ }, [connectionResolvers, aiClient]);
51
+ }
@@ -7,9 +7,9 @@ import type { OptionsResolver } from "@checkstack/ui";
7
7
  * Resolver name for the connection picker itself. Mirrors the
8
8
  * `CONNECTION_OPTIONS` constant every integration provider defines
9
9
  * (`provider.ts` → `*_RESOLVERS.CONNECTION_OPTIONS`). The bridge resolves
10
- * this one via `listConnections` (no connection is selected yet) and every
11
- * other resolver name via `getConnectionOptions` (cascading dropdowns that
12
- * depend on the chosen `connectionId`).
10
+ * this one via `listConnectionSummaries` (no connection is selected yet) and
11
+ * every other resolver name via `resolveConnectionOptions` (cascading dropdowns
12
+ * that depend on the chosen `connectionId`).
13
13
  */
14
14
  const CONNECTION_RESOLVER_NAME = "connectionOptions";
15
15
 
@@ -20,8 +20,13 @@ const CONNECTION_RESOLVER_NAME = "connectionOptions";
20
20
  *
21
21
  * Returns a stable, name-agnostic resolver map: the `connectionOptions`
22
22
  * resolver lists the provider's connections, and any other resolver name is
23
- * forwarded to `getConnectionOptions` for the currently selected connection,
24
- * passing the live form values as `context` for dependent fields.
23
+ * forwarded to `resolveConnectionOptions` for the currently selected
24
+ * connection, passing the live form values as `context` for dependent fields.
25
+ *
26
+ * Both calls are the USER-CALLABLE integration endpoints (`listConnectionSummaries`
27
+ * + `resolveConnectionOptions`), NOT the admin-gated `listConnections` /
28
+ * `getConnectionOptions`: an automation author needs working dropdowns without
29
+ * holding `integration.manage`. They return labels/ids only (no config/secrets).
25
30
  *
26
31
  * When `connectionProviderId` is undefined (a non-connection action such as
27
32
  * Log or Run Script), an empty map is returned so nothing tries to resolve.
@@ -45,7 +50,7 @@ export function useConnectionOptionResolvers(
45
50
 
46
51
  const resolver: OptionsResolver = async (formValues) => {
47
52
  if (resolverName === CONNECTION_RESOLVER_NAME) {
48
- const connections = await client.listConnections.call({
53
+ const connections = await client.listConnectionSummaries.call({
49
54
  providerId,
50
55
  });
51
56
  return connections.map((connection) => ({
@@ -59,7 +64,7 @@ export function useConnectionOptionResolvers(
59
64
  return [];
60
65
  }
61
66
 
62
- const options = await client.getConnectionOptions.call({
67
+ const options = await client.resolveConnectionOptions.call({
63
68
  providerId,
64
69
  connectionId,
65
70
  resolverName,
package/src/index.tsx CHANGED
@@ -20,7 +20,8 @@ export {
20
20
  * Routes:
21
21
  *
22
22
  * - `/automation/` → list view
23
- * - `/automation/new` → blank edit page (create)
23
+ * - `/automation/new` → template picker (create)
24
+ * - `/automation/new/blank` → blank / template-seeded editor
24
25
  * - `/automation/:automationId` → edit page
25
26
  * - `/automation/:automationId/runs` → run history
26
27
  * - `/automation/:automationId/runs/:runId` → single run drill-down
@@ -53,6 +54,15 @@ export default createFrontendPlugin({
53
54
  },
54
55
  {
55
56
  route: automationRoutes.routes.create,
57
+ load: () =>
58
+ import("./pages/AutomationTemplatePickerPage").then((m) => ({
59
+ default: m.AutomationTemplatePickerPage,
60
+ })),
61
+ title: "New automation",
62
+ accessRule: automationAccess.manage,
63
+ },
64
+ {
65
+ route: automationRoutes.routes.createBlank,
56
66
  load: () =>
57
67
  import("./pages/AutomationEditPage").then((m) => ({
58
68
  default: m.AutomationEditPage,
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { Link, useNavigate, useParams } from "react-router-dom";
2
+ import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom";
3
3
  import {
4
4
  ChevronLeft,
5
5
  History as HistoryIcon,
@@ -99,6 +99,7 @@ type EditTab = "visual" | "yaml";
99
99
  */
100
100
  const AutomationEditContent: React.FC = () => {
101
101
  const { automationId } = useParams<{ automationId: string }>();
102
+ const [searchParams] = useSearchParams();
102
103
  const isNew = !automationId || automationId === "new";
103
104
  const client = usePluginClient(AutomationApi);
104
105
  const accessApi = useApi(accessApiRef);
@@ -183,6 +184,35 @@ const AutomationEditContent: React.FC = () => {
183
184
  },
184
185
  );
185
186
 
187
+ // Template seeding (new automations only): `/automation/new/blank?template=<id>`
188
+ // pre-fills the editor from a curated example template. The template's
189
+ // `definition` seeds the canonical state once; the operator still picks a
190
+ // `runAs` service account and saves. `enabled: false` on the query keeps it
191
+ // off the wire for blank (no-template) and existing-automation editors.
192
+ const templateId = isNew ? searchParams.get("template") : null;
193
+ const templatesQuery = client.listAutomationTemplates.useQuery(undefined, {
194
+ enabled: Boolean(templateId),
195
+ gcTime: 0,
196
+ });
197
+ const selectedTemplate = templateId
198
+ ? templatesQuery.data?.items.find((t) => t.id === templateId)
199
+ : undefined;
200
+ useInitOnceForKey(
201
+ templatesQuery.isFetchedAfterMount ? selectedTemplate : undefined,
202
+ selectedTemplate?.id,
203
+ (template) => {
204
+ const normalized: AutomationDefinition = {
205
+ ...template.definition,
206
+ triggers: assignDefaultTriggerIds(template.definition.triggers),
207
+ actions: assignDefaultIds(template.definition.actions, new Set()),
208
+ };
209
+ setName(template.definition.name);
210
+ setDescription(template.definition.description ?? "");
211
+ setDefinition(normalized);
212
+ setYamlText(stringifyYaml(normalized));
213
+ },
214
+ );
215
+
186
216
  // Keep the YAML mirror in sync with definition while the visual editor
187
217
  // is active — the YAML tab needs a non-stale starting point when the
188
218
  // operator switches over.
@@ -0,0 +1,152 @@
1
+ import React from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import { Workflow, FilePlus2, ArrowRight } from "lucide-react";
4
+ import {
5
+ usePluginClient,
6
+ accessApiRef,
7
+ useApi,
8
+ wrapInSuspense,
9
+ } from "@checkstack/frontend-api";
10
+ import {
11
+ AutomationApi,
12
+ automationAccess,
13
+ automationRoutes,
14
+ type AutomationTemplate,
15
+ } from "@checkstack/automation-common";
16
+ import {
17
+ PageLayout,
18
+ Card,
19
+ CardHeader,
20
+ CardTitle,
21
+ CardContent,
22
+ Button,
23
+ Badge,
24
+ DynamicIcon,
25
+ LoadingSpinner,
26
+ QueryErrorState,
27
+ } from "@checkstack/ui";
28
+ import { resolveRoute } from "@checkstack/common";
29
+
30
+ /**
31
+ * Landing page for "create a new automation". Presents a "Blank automation"
32
+ * option plus a gallery of curated example templates (grouped by category)
33
+ * contributed by core + plugins. Picking a template opens the blank editor
34
+ * pre-seeded from it (`/automation/new/blank?template=<id>`); the operator
35
+ * still chooses a service account and saves.
36
+ */
37
+ const AutomationTemplatePickerContent: React.FC = () => {
38
+ const client = usePluginClient(AutomationApi);
39
+ const accessApi = useApi(accessApiRef);
40
+ const navigate = useNavigate();
41
+
42
+ const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
43
+ automationAccess.manage,
44
+ );
45
+
46
+ const query = client.listAutomationTemplates.useQuery();
47
+
48
+ const startBlank = () =>
49
+ navigate(resolveRoute(automationRoutes.routes.createBlank));
50
+
51
+ const openTemplate = (template: AutomationTemplate) =>
52
+ navigate(
53
+ `${resolveRoute(automationRoutes.routes.createBlank)}?template=${encodeURIComponent(
54
+ template.id,
55
+ )}`,
56
+ );
57
+
58
+ // Group templates by category, preserving first-seen category order.
59
+ const grouped = React.useMemo(() => {
60
+ const byCategory = new Map<string, AutomationTemplate[]>();
61
+ for (const template of query.data?.items ?? []) {
62
+ const list = byCategory.get(template.category) ?? [];
63
+ list.push(template);
64
+ byCategory.set(template.category, list);
65
+ }
66
+ return [...byCategory.entries()];
67
+ }, [query.data]);
68
+
69
+ return (
70
+ <PageLayout
71
+ title="New automation"
72
+ subtitle="Start from a curated example or build your own from scratch"
73
+ icon={Workflow}
74
+ loading={accessLoading}
75
+ allowed={canManage}
76
+ >
77
+ <div className="space-y-6">
78
+ <Card>
79
+ <CardContent className="flex flex-col gap-3 p-4 sm:flex-row sm:items-center sm:justify-between">
80
+ <div className="flex items-center gap-3">
81
+ <div className="flex h-10 w-10 items-center justify-center rounded-md bg-muted">
82
+ <FilePlus2 className="h-5 w-5 text-muted-foreground" />
83
+ </div>
84
+ <div>
85
+ <div className="font-medium">Blank automation</div>
86
+ <div className="text-sm text-muted-foreground">
87
+ Start with an empty editor and add triggers and actions yourself.
88
+ </div>
89
+ </div>
90
+ </div>
91
+ <Button onClick={startBlank}>
92
+ Start blank
93
+ <ArrowRight className="ml-1 h-4 w-4" />
94
+ </Button>
95
+ </CardContent>
96
+ </Card>
97
+
98
+ {query.isLoading ? (
99
+ <div className="p-6">
100
+ <LoadingSpinner />
101
+ </div>
102
+ ) : query.isError ? (
103
+ <QueryErrorState error={query.error} onRetry={() => query.refetch()} />
104
+ ) : (
105
+ grouped.map(([category, items]) => (
106
+ <section key={category} className="space-y-3">
107
+ <h2 className="text-sm font-semibold text-muted-foreground">
108
+ {category}
109
+ </h2>
110
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
111
+ {items.map((template) => (
112
+ <Card
113
+ key={template.id}
114
+ className="flex h-full cursor-pointer flex-col transition-colors hover:border-primary"
115
+ onClick={() => openTemplate(template)}
116
+ >
117
+ <CardHeader className="flex-row items-start gap-3 space-y-0">
118
+ <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
119
+ <DynamicIcon name={template.icon} className="h-5 w-5" />
120
+ </div>
121
+ <CardTitle className="text-base leading-snug">
122
+ {template.name}
123
+ </CardTitle>
124
+ </CardHeader>
125
+ <CardContent className="flex flex-1 flex-col justify-between gap-3">
126
+ <p className="text-sm text-muted-foreground">
127
+ {template.description}
128
+ </p>
129
+ <div className="flex items-center justify-between">
130
+ <Badge variant="secondary" className="font-normal">
131
+ {template.ownerPluginId}
132
+ </Badge>
133
+ <span className="flex items-center text-sm font-medium text-primary">
134
+ Use template
135
+ <ArrowRight className="ml-1 h-4 w-4" />
136
+ </span>
137
+ </div>
138
+ </CardContent>
139
+ </Card>
140
+ ))}
141
+ </div>
142
+ </section>
143
+ ))
144
+ )}
145
+ </div>
146
+ </PageLayout>
147
+ );
148
+ };
149
+
150
+ export const AutomationTemplatePickerPage = wrapInSuspense(
151
+ AutomationTemplatePickerContent,
152
+ );
package/tsconfig.json CHANGED
@@ -4,6 +4,9 @@
4
4
  "src"
5
5
  ],
6
6
  "references": [
7
+ {
8
+ "path": "../ai-common"
9
+ },
7
10
  {
8
11
  "path": "../auth-common"
9
12
  },