feat: finish the automations plugin (B17)

This commit is contained in:
LukeGus committed 2026-09-24 13:05:44 -05:00
1 parent 993a91c899
commit 6b4d1432ab
96 files changed
+25106 -5077

No files matched your search

+81
View File
@@ -248,3 +248,84 @@ export async function listCommandHistory(
return null;
}
}
/** The automations plugin, as ctx.services.get("automations.access") returns it. */
interface AutomationsAccess {
list: () => Promise<
Array<{
id: number;
name: string;
description: string | null;
enabled: boolean;
triggerKind: string | null;
lastRunAt: string | null;
lastRunStatus: string | null;
missingPlugins: string[];
}>
>;
get: (id: number) => Promise<{
id: number;
name: string;
enabled: boolean;
definition: unknown;
} | null>;
create: (input: {
name: string;
description?: string | null;
definition: unknown;
enabled?: boolean;
}) => Promise<{ id: number; name: string }>;
}
function automations(userId: string): Partial<AutomationsAccess> {
if (!current) return {};
try {
return current.get<AutomationsAccess>("automations.access", { userId });
} catch {
return {};
}
}
/**
* The user's automations, or null when the automations plugin is off or the
* user may not use it.
*/
export async function listAutomations(
userId: string,
): Promise<Awaited<ReturnType<AutomationsAccess["list"]>> | null> {
const list = automations(userId).list;
if (typeof list !== "function") return null;
try {
return await list();
} catch {
return null;
}
}
export async function getAutomation(
userId: string,
id: number,
): Promise<Awaited<ReturnType<AutomationsAccess["get"]>> | undefined> {
const get = automations(userId).get;
if (typeof get !== "function") return undefined;
try {
return await get(id);
} catch {
return undefined;
}
}
/**
* Creates an automation through the plugin, which validates it exactly as
* its own route does. Throws when the plugin is off.
*/
export async function createAutomation(
userId: string,
input: Parameters<AutomationsAccess["create"]>[0],
): Promise<{ id: number; name: string }> {
const create = automations(userId).create;
if (typeof create !== "function") {
throw new Error("The automations plugin is not available");
}
return create(input);
}
+7 -18
View File
@@ -1,8 +1,4 @@
import {
createCurrentAutomationRepository,
createCurrentHostRepository,
} from "../../../../../src/backend/database/repositories/factory.js";
import { validateDefinition } from "../../../../automations/src/backend/routes.js";
import { createCurrentHostRepository } from "../../../../../src/backend/database/repositories/factory.js";
import { resolveHostById } from "../../../../../src/backend/hosts/host-resolver.js";
import { execCommand } from "@termix/plugin-sdk/host-commands";
import { withSshConnection } from "../ssh.js";
@@ -14,6 +10,7 @@ import {
getSnippet,
createFleet,
addFleetMember,
createAutomation,
} from "../services.js";
/** Approved commands get a bounded window rather than hanging the request. */
@@ -204,20 +201,12 @@ export async function applyProposal(
}
case "propose_create_automation": {
// Reuses the same validator the automations route runs, so an
// LLM-authored definition is held to exactly the human standard.
const validation = validateDefinition(payload.definition);
if (!validation.ok || !validation.definition) {
throw new Error(
validation.error ?? "The automation definition is invalid",
);
}
const created = await createCurrentAutomationRepository().create({
userId,
// The automations plugin runs the same validator its own route does,
// so an LLM-authored definition is held to exactly the human standard.
const created = await createAutomation(userId, {
name: requireString(payload.name, "name"),
description: optionalString(payload.description),
definition: JSON.stringify(validation.definition),
definition: payload.definition,
// Starts disabled: an automation the user has not watched run once
// should not begin firing against their servers on approval.
enabled: false,
@@ -225,7 +214,7 @@ export async function applyProposal(
return {
ok: true,
summary: `Created automation ${(created as any).name} (disabled until you enable it)`,
summary: `Created automation ${created.name} (disabled until you enable it)`,
};
}
+17 -14
View File
@@ -1,6 +1,5 @@
import {
createCurrentNotificationChannelRepository,
createCurrentAutomationRepository,
createCurrentHomepageItemRepository,
createCurrentHostRepository,
} from "../../../../../src/backend/database/repositories/factory.js";
@@ -11,6 +10,8 @@ import {
listSnippets,
listFleets,
listCommandHistory,
listAutomations,
getAutomation,
} from "../services.js";
/**
@@ -128,17 +129,18 @@ export const readTools: AiTool[] = [
category: "read",
parameters: objectSchema({}),
handler: async (_args, context) => {
const automations = await createCurrentAutomationRepository().list(
context.userId,
);
const automations = await listAutomations(context.userId);
if (!automations) return { automations: [], unavailable: true };
return {
automations: automations.map((automation: any) => ({
automations: automations.map((automation) => ({
id: automation.id,
name: automation.name,
description: automation.description ?? null,
description: automation.description,
enabled: automation.enabled,
lastRunAt: automation.lastRunAt ?? null,
lastRunStatus: automation.lastRunStatus ?? null,
triggerKind: automation.triggerKind,
lastRunAt: automation.lastRunAt,
lastRunStatus: automation.lastRunStatus,
needsPlugins: automation.missingPlugins,
})),
};
},
@@ -155,17 +157,18 @@ export const readTools: AiTool[] = [
["automationId"],
),
handler: async (args, context) => {
const automation = await createCurrentAutomationRepository().findForUser(
Number(args.automationId),
const automation = await getAutomation(
context.userId,
Number(args.automationId),
);
if (automation === undefined) return { unavailable: true };
if (!automation) return { error: "Automation not found" };
return {
automation: {
id: (automation as any).id,
name: (automation as any).name,
enabled: (automation as any).enabled,
definition: (automation as any).definition,
id: automation.id,
name: automation.name,
enabled: automation.enabled,
definition: automation.definition,
},
};
},