grafana: status/datasources/dashboards/alerts tools, emits alert.firing. tautulli: activity/history/stats tools, emits watch.recorded. nextcloud: users/shares/apps/occ tools (occ via docker exec, shares over OCS), emits user.created/share.created. nodered: flows/nodes/deploy tools, emits flows.deployed inline from the deploy tool. All typecheck; manifests parse.
65 lines
2.7 KiB
TypeScript
65 lines
2.7 KiB
TypeScript
// nodered's tools — importing nodered's own admin-API client (novox/hq ADR 0044). They return
|
|
// structured data; the mesh serves them through the sdk's tool harness.
|
|
//
|
|
// The deploy tool is nodered's one event source (novox/hq ADR 0046/0047): a successful deploy
|
|
// emits module.nodered.flows.deployed. nodered has nothing to observe on a timer, so there is no
|
|
// separate events entrypoint — the emit rides the action that causes it. The emit is best-effort:
|
|
// if no broker is bound, the deploy still succeeds and the announcement is simply skipped.
|
|
|
|
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
|
|
import { emit } from "@novox/mesh-sdk/events";
|
|
import { NodeRedClient } from "../client.js";
|
|
|
|
export function getNodeRedTools(nodered: NodeRedClient): ToolDefinition[] {
|
|
return [
|
|
{
|
|
name: "nodered_list_flows",
|
|
description: "List Node-RED flows (tabs) — id, label, disabled state — and the total node count.",
|
|
input: {},
|
|
run: async () => nodered.listFlows(),
|
|
},
|
|
{
|
|
name: "nodered_list_nodes",
|
|
description: "List the Node-RED node modules installed in the runtime and their versions.",
|
|
input: {},
|
|
run: async () => {
|
|
const nodes = await nodered.listNodes();
|
|
return { count: nodes.length, nodes };
|
|
},
|
|
},
|
|
{
|
|
name: "nodered_deploy",
|
|
description:
|
|
"Replace the whole Node-RED flow configuration and deploy it. `flows` is the full node " +
|
|
"array (as GET /flows returns). Emits module.nodered.flows.deployed on success.",
|
|
input: {
|
|
flows: { type: "array", description: "the full flow configuration — every node across every tab" },
|
|
type: { type: "string", description: "deployment type: full (default), nodes, or flows" },
|
|
},
|
|
run: async (args) => {
|
|
const flows = args.flows as unknown[];
|
|
if (!Array.isArray(flows)) throw new Error("flows must be an array of Node-RED nodes");
|
|
const type = args.type ? String(args.type) : "full";
|
|
const result = await nodered.deployFlows(flows, type);
|
|
// Best-effort announcement — a deploy must not fail because the broker is unbound here.
|
|
try {
|
|
await emit("module.nodered.flows.deployed", { rev: result.rev, nodeCount: result.nodeCount, type });
|
|
} catch (err) {
|
|
console.error(`[nodered] deployed but could not emit: ${err}`);
|
|
}
|
|
return result;
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
// The tools exist only when a Node-RED URL is configured; without one, nodered contributes none
|
|
// rather than failing the whole tool runtime.
|
|
registerModuleTools("nodered", (env) => {
|
|
try {
|
|
return getNodeRedTools(NodeRedClient.fromEnv(env));
|
|
} catch {
|
|
return [];
|
|
}
|
|
});
|