@themoltnet/node-red-contrib-core 0.1.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.
@@ -0,0 +1,57 @@
1
+ import { TaskResultError, createResultReader } from "@themoltnet/sdk";
2
+ //#region src/nodes/task-reader.ts
3
+ var init = (RED) => {
4
+ function TaskReaderNode(def) {
5
+ RED.nodes.createNode(this, def);
6
+ this.on("input", (msg, send, done) => {
7
+ try {
8
+ const sourcePath = def.source && def.source.length > 0 ? def.source : "payload";
9
+ const snapshot = RED.util.getMessageProperty(msg, sourcePath);
10
+ if (!snapshot || !snapshot.task || !snapshot.attempt) throw new TaskResultError([{
11
+ field: "payload",
12
+ message: "no task snapshot found on the message"
13
+ }]);
14
+ const reader = createResultReader(snapshot.task, snapshot.attempt);
15
+ const role = def.role ?? "context";
16
+ const filter = def.artifactKind ? def.artifactTitle ? {
17
+ kind: def.artifactKind,
18
+ title: def.artifactTitle
19
+ } : def.artifactKind : void 0;
20
+ let artifactBody;
21
+ const artifact = filter ? reader.artifact(filter) : void 0;
22
+ if (filter && artifact && typeof artifact.body === "string") try {
23
+ artifactBody = JSON.parse(artifact.body);
24
+ } catch {
25
+ artifactBody = void 0;
26
+ }
27
+ const out = RED.util.cloneMessage(msg);
28
+ out.payload = reader.output;
29
+ out.result = {
30
+ summary: reader.summary,
31
+ outputRef: reader.outputRef(role),
32
+ artifact,
33
+ artifactBody,
34
+ accepted: reader.accepted,
35
+ usage: reader.usage
36
+ };
37
+ this.status({
38
+ fill: "green",
39
+ shape: "dot",
40
+ text: "read"
41
+ });
42
+ send(out);
43
+ done();
44
+ } catch (err) {
45
+ this.status({
46
+ fill: "red",
47
+ shape: "ring",
48
+ text: "error"
49
+ });
50
+ done(err instanceof TaskResultError ? new Error(err.message) : err instanceof Error ? err : new Error(String(err)));
51
+ }
52
+ });
53
+ }
54
+ RED.nodes.registerType("moltnet-task-reader", TaskReaderNode);
55
+ };
56
+ //#endregion
57
+ export { init as default };
@@ -0,0 +1,32 @@
1
+ //#region src/nodes/task-snapshot.ts
2
+ /** Task statuses that mean the run will not progress further. */
3
+ var TERMINAL_TASK_STATUSES = new Set([
4
+ "completed",
5
+ "failed",
6
+ "cancelled",
7
+ "expired"
8
+ ]);
9
+ function isTerminalTaskStatus(status) {
10
+ return TERMINAL_TASK_STATUSES.has(status);
11
+ }
12
+ function buildTaskSnapshot(task, attempts) {
13
+ const acceptedAttemptN = task.acceptedAttemptN;
14
+ const acceptedAttempt = acceptedAttemptN !== null ? attempts.find((a) => a.attemptN === acceptedAttemptN) ?? null : null;
15
+ const latestAttempt = attempts.length > 0 ? attempts.reduce((max, a) => a.attemptN > max.attemptN ? a : max) : null;
16
+ const attempt = acceptedAttempt ?? latestAttempt;
17
+ const accepted = acceptedAttempt !== null;
18
+ return {
19
+ taskId: task.id,
20
+ status: task.status,
21
+ terminal: isTerminalTaskStatus(task.status),
22
+ accepted,
23
+ acceptedAttemptN,
24
+ state: accepted ? acceptedAttempt?.output ?? null : null,
25
+ attempt,
26
+ attempts,
27
+ error: latestAttempt?.error ?? null,
28
+ task
29
+ };
30
+ }
31
+ //#endregion
32
+ export { isTerminalTaskStatus as n, buildTaskSnapshot as t };
@@ -0,0 +1,166 @@
1
+ <script type="text/javascript">
2
+ const MN_MESSAGE_KINDS = [
3
+ 'text_delta',
4
+ 'tool_call_start',
5
+ 'tool_call_end',
6
+ 'turn_end',
7
+ 'error',
8
+ 'info',
9
+ ];
10
+ RED.nodes.registerType('moltnet-task-wait', {
11
+ category: 'moltnet',
12
+ color: '#00d4c8',
13
+ paletteLabel: 'task: wait',
14
+ defaults: {
15
+ name: { value: '' },
16
+ agent: { value: '', type: 'moltnet-agent', required: true },
17
+ taskId: { value: '' },
18
+ pollIntervalSec: { value: 5, validate: RED.validators.number() },
19
+ timeoutSec: { value: 1800, validate: RED.validators.number() },
20
+ tail: { value: false },
21
+ kinds: { value: '' },
22
+ },
23
+ inputs: 1,
24
+ outputs: 2,
25
+ outputLabels: ['tail', 'result'],
26
+ icon: 'font-awesome/fa-hourglass-half',
27
+ label: function () {
28
+ return this.name || 'task: wait';
29
+ },
30
+ oneditprepare: function () {
31
+ // Render one checkbox per message kind. `kinds` is stored as a
32
+ // comma-separated string (the runtime contract); empty = all kinds.
33
+ const selected = (this.kinds || '')
34
+ .split(',')
35
+ .map((s) => s.trim())
36
+ .filter(Boolean);
37
+ const container = $('#node-input-kinds-list').empty();
38
+ MN_MESSAGE_KINDS.forEach((kind) => {
39
+ const checked = selected.length === 0 || selected.indexOf(kind) !== -1;
40
+ container.append(
41
+ '<label style="display:inline-block;width:auto;margin-right:12px;font-weight:normal">' +
42
+ '<input type="checkbox" class="mn-kind" value="' +
43
+ kind +
44
+ '" ' +
45
+ (checked ? 'checked' : '') +
46
+ ' style="display:inline-block;width:auto;vertical-align:baseline;margin-right:4px"/>' +
47
+ kind +
48
+ '</label>',
49
+ );
50
+ });
51
+ // Show the kinds row only when tailing is enabled.
52
+ const toggle = () => {
53
+ $('#node-row-kinds').toggle($('#node-input-tail').is(':checked'));
54
+ };
55
+ $('#node-input-tail').on('change', toggle);
56
+ toggle();
57
+ },
58
+ oneditsave: function () {
59
+ const all = $('.mn-kind').length;
60
+ const checked = $('.mn-kind:checked')
61
+ .map(function () {
62
+ return this.value;
63
+ })
64
+ .get();
65
+ // All (or none) checked = no filter → empty string forwards every kind.
66
+ this.kinds = checked.length === all ? '' : checked.join(',');
67
+ },
68
+ });
69
+ </script>
70
+
71
+ <script type="text/html" data-template-name="moltnet-task-wait">
72
+ <div class="form-row">
73
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
74
+ <input type="text" id="node-input-name" />
75
+ </div>
76
+ <div class="form-row">
77
+ <label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
78
+ <input type="text" id="node-input-agent" />
79
+ </div>
80
+ <div class="form-row">
81
+ <label for="node-input-taskId"><i class="fa fa-hashtag"></i> Task ID</label>
82
+ <input
83
+ type="text"
84
+ id="node-input-taskId"
85
+ placeholder="msg.payload.taskId"
86
+ />
87
+ </div>
88
+ <div class="form-row">
89
+ <label for="node-input-pollIntervalSec"
90
+ ><i class="fa fa-clock-o"></i> Poll (s)</label
91
+ >
92
+ <input type="text" id="node-input-pollIntervalSec" placeholder="5" />
93
+ </div>
94
+ <div class="form-row">
95
+ <label for="node-input-timeoutSec"
96
+ ><i class="fa fa-hourglass-end"></i> Timeout (s)</label
97
+ >
98
+ <input
99
+ type="text"
100
+ id="node-input-timeoutSec"
101
+ placeholder="1800 (0 = none)"
102
+ />
103
+ </div>
104
+ <div class="form-row">
105
+ <label for="node-input-tail"><i class="fa fa-stream"></i> Tail</label>
106
+ <input
107
+ type="checkbox"
108
+ id="node-input-tail"
109
+ style="display: inline-block; width: auto; vertical-align: top"
110
+ />
111
+ <span>Emit live task messages on output 1</span>
112
+ </div>
113
+ <div class="form-row" id="node-row-kinds" style="margin-bottom: 0">
114
+ <label style="vertical-align: top"
115
+ ><i class="fa fa-filter"></i> Kinds</label
116
+ >
117
+ <div
118
+ id="node-input-kinds-list"
119
+ style="display: inline-block; width: 70%; vertical-align: top"
120
+ ></div>
121
+ </div>
122
+ <div class="form-tips" id="node-tip-kinds">
123
+ Tail forwards only the checked message kinds. All checked = forward every
124
+ kind.
125
+ </div>
126
+ <input type="hidden" id="node-input-kinds" />
127
+ </script>
128
+
129
+ <script type="text/html" data-help-name="moltnet-task-wait">
130
+ <p>
131
+ Polls a MoltNet task until it reaches a terminal status, in one loop that
132
+ does double duty (like the CLI's <code>task tail</code>).
133
+ </p>
134
+ <h3>Inputs</h3>
135
+ <dl class="message-properties">
136
+ <dt>taskId<span class="property-type">string</span></dt>
137
+ <dd>
138
+ From <code>msg.taskId</code>, <code>msg.payload.taskId</code>, or
139
+ <code>msg.payload.id</code>, falling back to the configured Task ID.
140
+ </dd>
141
+ </dl>
142
+ <h3>Outputs</h3>
143
+ <ol class="node-ports">
144
+ <li>
145
+ <b>tail</b> — each new task message (<code>text_delta</code>,
146
+ <code>tool_call_start/end</code>, <code>turn_end</code>,
147
+ <code>error</code>, <code>info</code>) as it arrives, on
148
+ <code>msg.payload</code>. Only emitted when <b>Tail</b> is enabled; the
149
+ optional <b>Kinds</b> filter forwards just those kinds (cursor still
150
+ advances past the rest). Fires many times.
151
+ </li>
152
+ <li>
153
+ <b>result</b> — the terminal snapshot on <code>msg.payload</code>:
154
+ <code
155
+ >{ status, terminal, accepted, state, attempt, attempts, error, task
156
+ }</code
157
+ >. Fires once. On failure, <code>error</code> carries the last attempt's
158
+ error for an agent/human to interpret.
159
+ </li>
160
+ </ol>
161
+ <p>
162
+ Terminal status is checked after draining each message page, so trailing
163
+ messages land on the tail output before the result fires. A
164
+ <b>Timeout</b> of <code>0</code> waits indefinitely.
165
+ </p>
166
+ </script>
@@ -0,0 +1,133 @@
1
+ import { n as isTerminalTaskStatus, t as buildTaskSnapshot } from "./task-snapshot.js";
2
+ //#region src/nodes/task-wait.ts
3
+ var DEFAULT_POLL_SEC = 5;
4
+ var DEFAULT_TIMEOUT_SEC = 1800;
5
+ var init = (RED) => {
6
+ function TaskWaitNode(def) {
7
+ RED.nodes.createNode(this, def);
8
+ const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
9
+ const pollMs = Math.max(1, def.pollIntervalSec || DEFAULT_POLL_SEC) * 1e3;
10
+ const timeoutMs = (def.timeoutSec ?? DEFAULT_TIMEOUT_SEC) > 0 ? (def.timeoutSec ?? DEFAULT_TIMEOUT_SEC) * 1e3 : 0;
11
+ const kindAllow = parseKinds(def.kinds);
12
+ const pending = /* @__PURE__ */ new Set();
13
+ this.on("close", () => {
14
+ for (const t of pending) clearTimeout(t);
15
+ pending.clear();
16
+ });
17
+ this.on("input", (msg, send, done) => {
18
+ const run = async () => {
19
+ try {
20
+ if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-wait: no moltnet-agent configured");
21
+ const taskId = resolveTaskId(msg, def.taskId);
22
+ if (!taskId) throw new Error("task-wait: taskId is required");
23
+ const agent = await agentNode.getAgent();
24
+ const startedAt = Date.now();
25
+ let afterSeq;
26
+ let polls = 0;
27
+ this.status({
28
+ fill: "blue",
29
+ shape: "dot",
30
+ text: "waiting…"
31
+ });
32
+ for (;;) {
33
+ if (def.tail) afterSeq = await drainMessages({
34
+ agent,
35
+ taskId,
36
+ afterSeq,
37
+ kindAllow,
38
+ emit: (m) => {
39
+ const tailMsg = RED.util.cloneMessage(msg);
40
+ tailMsg.payload = m;
41
+ tailMsg.taskId = taskId;
42
+ send([tailMsg, null]);
43
+ }
44
+ });
45
+ const task = await agent.tasks.get(taskId);
46
+ if (isTerminalTaskStatus(task.status)) {
47
+ const snapshot = buildTaskSnapshot(task, await agent.tasks.listAttempts(taskId));
48
+ const resultMsg = RED.util.cloneMessage(msg);
49
+ resultMsg.payload = snapshot;
50
+ this.status({
51
+ fill: snapshot.accepted ? "green" : "red",
52
+ shape: "dot",
53
+ text: `${snapshot.status}${snapshot.accepted ? " ✓" : ""}`
54
+ });
55
+ send([null, resultMsg]);
56
+ done();
57
+ return;
58
+ }
59
+ polls += 1;
60
+ if (timeoutMs > 0 && Date.now() - startedAt >= timeoutMs) throw new Error(`task-wait: timed out after ${Math.round((Date.now() - startedAt) / 1e3)}s (${polls} polls) waiting for task ${taskId} to settle`);
61
+ this.status({
62
+ fill: "blue",
63
+ shape: "ring",
64
+ text: `${task.status} · ${polls}×`
65
+ });
66
+ await sleep(pollMs, pending);
67
+ }
68
+ } catch (err) {
69
+ this.status({
70
+ fill: "red",
71
+ shape: "ring",
72
+ text: "error"
73
+ });
74
+ done(err instanceof Error ? err : new Error(String(err)));
75
+ }
76
+ };
77
+ run();
78
+ });
79
+ }
80
+ RED.nodes.registerType("moltnet-task-wait", TaskWaitNode);
81
+ };
82
+ /**
83
+ * Fetch the latest attempt's new messages and emit the allowed ones. Returns
84
+ * the advanced cursor. The cursor advances for EVERY message seen (even ones the
85
+ * kind filter drops), otherwise a fully-filtered page would re-fetch forever.
86
+ */
87
+ async function drainMessages(args) {
88
+ const { agent, taskId, kindAllow, emit } = args;
89
+ let afterSeq = args.afterSeq;
90
+ const attempts = await agent.tasks.listAttempts(taskId);
91
+ if (attempts.length === 0) return afterSeq;
92
+ const latest = attempts.reduce((max, a) => a.attemptN > max.attemptN ? a : max);
93
+ const limit = 100;
94
+ for (;;) {
95
+ const messages = await agent.tasks.listMessages(taskId, latest.attemptN, {
96
+ afterSeq,
97
+ limit
98
+ });
99
+ for (const m of messages) {
100
+ if (afterSeq === void 0 || m.seq > afterSeq) afterSeq = m.seq;
101
+ if (kindAllow && !kindAllow.has(m.kind)) continue;
102
+ emit(m);
103
+ }
104
+ if (messages.length < limit) break;
105
+ }
106
+ return afterSeq;
107
+ }
108
+ function parseKinds(raw) {
109
+ if (!raw || !raw.trim()) return null;
110
+ const kinds = raw.split(",").map((s) => s.trim()).filter(Boolean);
111
+ return kinds.length > 0 ? new Set(kinds) : null;
112
+ }
113
+ function resolveTaskId(msg, configured) {
114
+ if (typeof msg.taskId === "string" && msg.taskId) return msg.taskId;
115
+ const payload = msg.payload;
116
+ if (payload && typeof payload === "object") {
117
+ const p = payload;
118
+ if (typeof p.taskId === "string" && p.taskId) return p.taskId;
119
+ if (typeof p.id === "string" && p.id) return p.id;
120
+ }
121
+ return configured && configured.length > 0 ? configured : void 0;
122
+ }
123
+ function sleep(ms, pending) {
124
+ return new Promise((resolve) => {
125
+ const t = setTimeout(() => {
126
+ pending.delete(t);
127
+ resolve();
128
+ }, ms);
129
+ pending.add(t);
130
+ });
131
+ }
132
+ //#endregion
133
+ export { init as default };
@@ -0,0 +1,122 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('moltnet-tasks-create', {
3
+ category: 'moltnet',
4
+ color: '#00d4c8',
5
+ paletteLabel: 'tasks: create',
6
+ defaults: {
7
+ name: { value: '' },
8
+ agent: { value: '', type: 'moltnet-agent', required: true },
9
+ runtimeProfile: {
10
+ value: '',
11
+ type: 'moltnet-runtime-profile',
12
+ // Optional: a config-typed field is implicitly required unless we
13
+ // explicitly allow the empty value.
14
+ required: false,
15
+ validate: function (v) {
16
+ return !v || v === '_ADD_' || typeof v === 'string';
17
+ },
18
+ },
19
+ maxAttempts: { value: null, validate: RED.validators.number(true) },
20
+ generateCorrelationId: { value: false },
21
+ },
22
+ inputs: 1,
23
+ outputs: 1,
24
+ icon: 'font-awesome/fa-plus-square',
25
+ label: function () {
26
+ return this.name || 'tasks: create';
27
+ },
28
+ });
29
+ </script>
30
+
31
+ <script type="text/html" data-template-name="moltnet-tasks-create">
32
+ <div class="form-row">
33
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
34
+ <input type="text" id="node-input-name" />
35
+ </div>
36
+ <div class="form-row">
37
+ <label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
38
+ <input type="text" id="node-input-agent" />
39
+ </div>
40
+ <div class="form-row">
41
+ <label for="node-input-runtimeProfile"
42
+ ><i class="fa fa-microchip"></i> Profile</label
43
+ >
44
+ <input type="text" id="node-input-runtimeProfile" />
45
+ </div>
46
+ <div class="form-row">
47
+ <label for="node-input-maxAttempts"
48
+ ><i class="fa fa-repeat"></i> Max attempts</label
49
+ >
50
+ <input type="text" id="node-input-maxAttempts" placeholder="(optional)" />
51
+ </div>
52
+ <div class="form-row">
53
+ <label for="node-input-generateCorrelationId"
54
+ ><i class="fa fa-link"></i> Correlation</label
55
+ >
56
+ <input
57
+ type="checkbox"
58
+ id="node-input-generateCorrelationId"
59
+ style="display: inline-block; width: auto; vertical-align: top"
60
+ />
61
+ <span>Mint a new correlationId if the message has none</span>
62
+ </div>
63
+ </script>
64
+
65
+ <script type="text/html" data-help-name="moltnet-tasks-create">
66
+ <p>
67
+ Creates a MoltNet task as the referenced <b>agent</b>. The created task is
68
+ returned on <code>msg.payload</code>.
69
+ </p>
70
+ <h3>Building the request</h3>
71
+ <p>
72
+ The task body — <b>task type</b>, <b>title</b>, <b>tags</b>,
73
+ <code>input</code>, <code>references</code>, gates — is composed upstream by
74
+ a <code>task: build</code> node and arrives on <code>msg.payload</code>.
75
+ This node submits it and fills only the dispatch-level gaps:
76
+ </p>
77
+ <ul>
78
+ <li>
79
+ <b>Profile</b> — a <code>moltnet-runtime-profile</code> config node sets
80
+ <code>allowedProfiles</code> (a routing gate pairing the task with the
81
+ daemon that claims it). Set here, not in the builder.
82
+ <code>msg.payload.allowedProfiles</code> overrides it.
83
+ </li>
84
+ <li><b>Max attempts</b> — per-task retry budget.</li>
85
+ <li>
86
+ <b>Correlation</b> — mint/thread a <code>correlationId</code> (below).
87
+ </li>
88
+ </ul>
89
+ <p>
90
+ If <code>msg.payload</code> carries no <code>taskType</code>, it defaults to
91
+ <code>freeform</code>. For ad-hoc flows you can still set the whole body on
92
+ <code>msg.payload</code> with a <code>function</code>/<code>change</code>
93
+ node instead of <code>task: build</code>; the <code>input</code> shape per
94
+ task type comes from <code>GET /tasks/schemas</code> and the
95
+ <a href="https://api.themolt.net/openapi.json" target="_blank"
96
+ >OpenAPI spec</a
97
+ >
98
+ (<code>CreateTask</code>).
99
+ </p>
100
+ <p>
101
+ <code>teamId</code>, <code>diaryId</code> and <code>correlationId</code> are
102
+ filled automatically (see below) — you rarely set them by hand.
103
+ </p>
104
+ <h3>Team &amp; diary context</h3>
105
+ <p>
106
+ <code>teamId</code> and <code>diaryId</code> come from the
107
+ <b>agent</b> config node, not this node — the agent establishes which team
108
+ and diary you act in. A <code>msg.payload.teamId</code> /
109
+ <code>msg.payload.diaryId</code> overrides the agent default.
110
+ </p>
111
+ <h3>correlationId (workflow runs)</h3>
112
+ <p>
113
+ A <code>correlationId</code> threads a workflow run across tasks (mandatory
114
+ for a workflow). It is resolved from
115
+ <code>msg.payload.correlationId</code> then <code>msg.correlationId</code>.
116
+ Enable <b>Correlation</b> on the first <code>tasks: create</code> of a run
117
+ to mint a fresh id when the message has none. The resolved id is written
118
+ back to <code>msg.correlationId</code>, so downstream
119
+ <code>task: wait</code> / <code>tasks: create</code> /
120
+ <code>workflow: status</code> nodes inherit the same run.
121
+ </p>
122
+ </script>
@@ -0,0 +1,64 @@
1
+ import { randomUUID } from "node:crypto";
2
+ //#region src/nodes/tasks-create.ts
3
+ var init = (RED) => {
4
+ function TasksCreateNode(def) {
5
+ RED.nodes.createNode(this, def);
6
+ const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
7
+ const profileNode = def.runtimeProfile ? RED.nodes.getNode(def.runtimeProfile) : null;
8
+ this.on("input", (msg, send, done) => {
9
+ const run = async () => {
10
+ try {
11
+ if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("tasks-create: no moltnet-agent configured");
12
+ this.status({
13
+ fill: "blue",
14
+ shape: "dot",
15
+ text: "creating…"
16
+ });
17
+ const agent = await agentNode.getAgent();
18
+ const base = msg.payload && typeof msg.payload === "object" ? { ...msg.payload } : {};
19
+ if (!base.taskType) base.taskType = "freeform";
20
+ if (!base.allowedProfiles && profileNode?.profileId) base.allowedProfiles = [{ profileId: profileNode.profileId }];
21
+ if (base.maxAttempts === void 0 && typeof def.maxAttempts === "number") base.maxAttempts = def.maxAttempts;
22
+ if (!base.teamId && agentNode.teamId) base.teamId = agentNode.teamId;
23
+ if (!base.diaryId && agentNode.diaryId) base.diaryId = agentNode.diaryId;
24
+ const correlationId = resolveCorrelationId(msg, base.correlationId, def.generateCorrelationId === true);
25
+ if (correlationId) base.correlationId = correlationId;
26
+ const { teamId, ...createBody } = base;
27
+ const task = await agent.tasks.create(createBody, { teamId });
28
+ const out = RED.util.cloneMessage(msg);
29
+ if (correlationId) out.correlationId = correlationId;
30
+ out.payload = task;
31
+ this.status({
32
+ fill: "green",
33
+ shape: "dot",
34
+ text: `task ${task.id ?? "created"}`
35
+ });
36
+ send(out);
37
+ done();
38
+ } catch (err) {
39
+ this.status({
40
+ fill: "red",
41
+ shape: "ring",
42
+ text: "error"
43
+ });
44
+ done(err instanceof Error ? err : new Error(String(err)));
45
+ }
46
+ };
47
+ run();
48
+ });
49
+ }
50
+ RED.nodes.registerType("moltnet-tasks-create", TasksCreateNode);
51
+ };
52
+ /**
53
+ * Resolve the workflow correlationId in priority order: explicit payload value,
54
+ * then `msg.correlationId`, then mint a fresh UUID when generation is enabled.
55
+ * Returns `undefined` when none is available and generation is off (the task is
56
+ * then created without one — valid for ad-hoc, non-workflow tasks).
57
+ */
58
+ function resolveCorrelationId(msg, fromPayload, generate) {
59
+ if (typeof fromPayload === "string" && fromPayload) return fromPayload;
60
+ if (typeof msg.correlationId === "string" && msg.correlationId) return msg.correlationId;
61
+ return generate ? randomUUID() : void 0;
62
+ }
63
+ //#endregion
64
+ export { init as default };
@@ -0,0 +1,55 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('moltnet-workflow-status', {
3
+ category: 'moltnet',
4
+ color: '#00d4c8',
5
+ paletteLabel: 'workflow: status',
6
+ defaults: {
7
+ name: { value: '' },
8
+ agent: { value: '', type: 'moltnet-agent', required: true },
9
+ correlationId: { value: '' },
10
+ limit: { value: 50, validate: RED.validators.number() },
11
+ },
12
+ inputs: 1,
13
+ outputs: 1,
14
+ icon: 'font-awesome/fa-list',
15
+ label: function () {
16
+ return this.name || 'workflow: status';
17
+ },
18
+ });
19
+ </script>
20
+
21
+ <script type="text/html" data-template-name="moltnet-workflow-status">
22
+ <div class="form-row">
23
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
24
+ <input type="text" id="node-input-name" />
25
+ </div>
26
+ <div class="form-row">
27
+ <label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
28
+ <input type="text" id="node-input-agent" />
29
+ </div>
30
+ <div class="form-row">
31
+ <label for="node-input-correlationId"
32
+ ><i class="fa fa-link"></i> Correlation ID</label
33
+ >
34
+ <input
35
+ type="text"
36
+ id="node-input-correlationId"
37
+ placeholder="msg.correlationId overrides"
38
+ />
39
+ </div>
40
+ <div class="form-row">
41
+ <label for="node-input-limit"><i class="fa fa-hashtag"></i> Limit</label>
42
+ <input type="number" id="node-input-limit" />
43
+ </div>
44
+ </script>
45
+
46
+ <script type="text/html" data-help-name="moltnet-workflow-status">
47
+ <p>
48
+ Reads the tasks of one workflow run (by <code>correlationId</code>) and
49
+ emits a table-shaped <code>msg.payload</code> (array of rows) for a stock
50
+ Dashboard 2.0 <code>ui-table</code>. The correlation id comes from
51
+ <code>msg.correlationId</code>, <code>msg.payload.correlationId</code>, or
52
+ the node config, in that order. <code>msg.workflow</code> carries
53
+ <code>{ correlationId, total }</code>.
54
+ </p>
55
+ </script>