@kontextmind/kxm
Version:
KXM local-first multi-agent orchestration and operator dashboard
63 lines (60 loc) • 2.78 kB
text/typescript
import { randomUUID } from "node:crypto";
import { canonicalWorkflowEvidenceKey, workflowWebhookHeaders } from "../plugins/kxm/src/workflow.ts";
const [runId, signalKey, status, summary, ...evidenceArgs] = process.argv.slice(2);
const serverUrl = process.env.KXM_SERVER_URL?.trim() || "http://127.0.0.1:7331";
const definitionId = process.env.KXM_WORKFLOW_ID?.trim();
const secret = process.env.KXM_WORKFLOW_SIGNAL_SECRET?.trim();
if (!runId || !signalKey || !status || !summary || !definitionId || !secret) {
console.error([
"Usage: workflow-signal <runId> <signalKey> <passed|warning|failed> <summary> [<required-key>=<evidence> ...]",
"Required environment: KXM_WORKFLOW_ID, KXM_WORKFLOW_SIGNAL_SECRET",
"Optional environment: KXM_SERVER_URL, KXM_SIGNAL_DELIVERY_ID",
].join("\n"));
process.exitCode = 2;
} else if (status !== "passed" && status !== "warning" && status !== "failed") {
console.error("status must be passed, warning, or failed");
process.exitCode = 2;
} else {
const evidenceEntries = new Map<string, string>();
for (const value of evidenceArgs) {
const separator = value.indexOf("=");
if (separator <= 0 || separator === value.length - 1) {
throw new Error("evidence must use <required-key>=<evidence> syntax");
}
const key = canonicalWorkflowEvidenceKey(value.slice(0, separator));
const proof = value.slice(separator + 1).trim();
if (!key || !proof) throw new Error("evidence must use <required-key>=<evidence> syntax");
if (evidenceEntries.has(key)) throw new Error(`duplicate normalized evidence key: ${key}`);
evidenceEntries.set(key, proof);
}
const evidence = Object.fromEntries(evidenceEntries);
const body = JSON.stringify({ status, summary, evidence });
const deliveryId = process.env.KXM_SIGNAL_DELIVERY_ID?.trim() || `example-${randomUUID()}`;
const endpoint = [
serverUrl.replace(/\/$/, ""),
"v1/webhooks",
encodeURIComponent(definitionId),
"runs",
encodeURIComponent(runId),
"signals",
encodeURIComponent(signalKey),
].join("/");
const response = await fetch(endpoint, {
method: "POST",
headers: {
"content-type": "application/json",
// Signs the timestamp, delivery ID, definition, run, signal key, and body.
...workflowWebhookHeaders({ secret, scope: { definitionId, runId, signalKey }, deliveryId, body }),
},
body,
});
const responseBody = await response.text();
let parsedBody: unknown = responseBody;
try {
parsedBody = JSON.parse(responseBody) as unknown;
} catch {
// Preserve a non-JSON proxy or server response for diagnostics.
}
console.log(JSON.stringify({ status: response.status, deliveryId, body: parsedBody }, null, 2));
if (!response.ok) process.exitCode = 1;
}