@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,156 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('moltnet-runtime-profile', {
3
+ category: 'config',
4
+ paletteLabel: 'runtime profile',
5
+ defaults: {
6
+ name: { value: '' },
7
+ agent: { value: '', type: 'moltnet-agent', required: true },
8
+ profileId: { value: '', required: true },
9
+ profileName: { value: '' },
10
+ },
11
+ label: function () {
12
+ return (
13
+ this.name || this.profileName || this.profileId || 'runtime profile'
14
+ );
15
+ },
16
+ oneditprepare: function () {
17
+ const select = $('#node-config-input-profile-select');
18
+ const manualRow = $('#node-config-row-profileId');
19
+ const hint = $('#node-config-profile-hint');
20
+ const profileIdInput = $('#node-config-input-profileId');
21
+ const profileNameInput = $('#node-config-input-profileName');
22
+ const current = this.profileId;
23
+
24
+ const showManual = (message) => {
25
+ hint.text(message);
26
+ manualRow.show();
27
+ select.hide();
28
+ };
29
+
30
+ const loadProfiles = () => {
31
+ const agentId = $('#node-config-input-agent').val();
32
+ if (!agentId) {
33
+ showManual('Select an agent, or enter a Profile ID manually.');
34
+ return;
35
+ }
36
+ select.empty().append($('<option>').val('').text('loading…')).show();
37
+ manualRow.hide();
38
+ hint.text('');
39
+ $.getJSON('moltnet-runtime-profiles/' + agentId)
40
+ .done((data) => {
41
+ const profiles = (data && data.profiles) || [];
42
+ if (!profiles.length) {
43
+ let prefix;
44
+ if (data && data.error === 'agent-not-deployed') {
45
+ prefix = 'Deploy the agent first to list profiles. ';
46
+ } else if (data && data.error) {
47
+ // Surface the real backend error instead of hiding it behind
48
+ // a generic "no profiles" message.
49
+ prefix = 'Could not list profiles: ' + data.error + '. ';
50
+ } else {
51
+ prefix = 'No profiles found for this team. ';
52
+ }
53
+ showManual(prefix + 'Enter a Profile ID manually.');
54
+ return;
55
+ }
56
+ select.empty();
57
+ select.append($('<option>').val('').text('— select a profile —'));
58
+ profiles.forEach((p) => {
59
+ select.append(
60
+ $('<option>')
61
+ .val(p.id)
62
+ .text(p.name + ' (' + p.provider + '/' + p.model + ')'),
63
+ );
64
+ });
65
+ if (current) select.val(current);
66
+ manualRow.hide();
67
+ select.show();
68
+ })
69
+ .fail(() => {
70
+ showManual(
71
+ 'Could not reach the editor backend; enter a Profile ID manually.',
72
+ );
73
+ });
74
+ };
75
+
76
+ $('#node-config-input-agent').on('change', loadProfiles);
77
+ loadProfiles();
78
+
79
+ // Keep the hidden profileId in sync from the dropdown selection.
80
+ select.on('change', function () {
81
+ const id = $(this).val();
82
+ if (id) {
83
+ profileIdInput.val(id);
84
+ const label = $(this).find('option:selected').text();
85
+ profileNameInput.val(label);
86
+ }
87
+ });
88
+ },
89
+ oneditsave: function () {
90
+ // If the dropdown has a selection, it already wrote profileId; otherwise
91
+ // the manual input is the source of truth.
92
+ const sel = $('#node-config-input-profile-select').val();
93
+ if (sel) $('#node-config-input-profileId').val(sel);
94
+ },
95
+ });
96
+ </script>
97
+
98
+ <script type="text/html" data-template-name="moltnet-runtime-profile">
99
+ <div class="form-row">
100
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
101
+ <input
102
+ type="text"
103
+ id="node-config-input-name"
104
+ placeholder="e.g. fast-classify"
105
+ />
106
+ </div>
107
+ <div class="form-row">
108
+ <label for="node-config-input-agent"
109
+ ><i class="fa fa-user"></i> Agent</label
110
+ >
111
+ <input type="text" id="node-config-input-agent" />
112
+ </div>
113
+ <div class="form-row">
114
+ <label for="node-config-input-profile-select"
115
+ ><i class="fa fa-microchip"></i> Profile</label
116
+ >
117
+ <select id="node-config-input-profile-select" style="width: 70%"></select>
118
+ </div>
119
+ <div
120
+ class="form-tips"
121
+ id="node-config-profile-hint"
122
+ style="margin-bottom: 8px"
123
+ ></div>
124
+ <div class="form-row" id="node-config-row-profileId">
125
+ <label for="node-config-input-profileId"
126
+ ><i class="fa fa-hashtag"></i> Profile ID</label
127
+ >
128
+ <input
129
+ type="text"
130
+ id="node-config-input-profileId"
131
+ placeholder="profile UUID"
132
+ />
133
+ </div>
134
+ <input type="hidden" id="node-config-input-profileName" />
135
+ </script>
136
+
137
+ <script type="text/html" data-help-name="moltnet-runtime-profile">
138
+ <p>
139
+ Names one MoltNet <b>runtime profile</b> by its <code>profileId</code>.
140
+ <code>tasks: create</code> references this config to set the task's
141
+ <code>allowedProfiles</code>, routing the task to a daemon serving that
142
+ profile.
143
+ </p>
144
+ <p>
145
+ <b>Routing gate, not a model selector.</b> A daemon runs exactly one profile
146
+ (<code>--profile &lt;id&gt;</code>) and only claims tasks whose
147
+ <code>allowedProfiles</code> include it (an empty list = unrestricted).
148
+ Selecting a profile here does not run a different model by itself — a daemon
149
+ serving that profile must be running.
150
+ </p>
151
+ <p>
152
+ The <b>Profile</b> dropdown lists the team's profiles via the selected
153
+ <b>Agent</b>. The agent config node must be <b>deployed</b> first; otherwise
154
+ enter the <b>Profile ID</b> manually.
155
+ </p>
156
+ </script>
@@ -0,0 +1,40 @@
1
+ //#region src/nodes/runtime-profile.ts
2
+ var init = (RED) => {
3
+ function MoltnetRuntimeProfileNode(def) {
4
+ RED.nodes.createNode(this, def);
5
+ this.agent = def.agent || void 0;
6
+ this.profileId = def.profileId?.trim() || void 0;
7
+ this.profileName = def.profileName?.trim() || void 0;
8
+ }
9
+ RED.nodes.registerType("moltnet-runtime-profile", MoltnetRuntimeProfileNode);
10
+ RED.httpAdmin?.get("/moltnet-runtime-profiles/:agentId", RED.auth.needsPermission("moltnet-runtime-profile.read"), (req, res) => {
11
+ const run = async () => {
12
+ try {
13
+ const agentId = String(req.params.agentId);
14
+ const agentNode = RED.nodes.getNode(agentId);
15
+ if (!agentNode || typeof agentNode.getAgent !== "function") {
16
+ res.json({
17
+ profiles: [],
18
+ error: "agent-not-deployed"
19
+ });
20
+ return;
21
+ }
22
+ const { items } = await (await agentNode.getAgent()).runtimeProfiles.list(agentNode.teamId ? { teamId: agentNode.teamId } : void 0);
23
+ res.json({ profiles: items.map((p) => ({
24
+ id: p.id,
25
+ name: p.name,
26
+ model: p.model,
27
+ provider: p.provider
28
+ })) });
29
+ } catch (err) {
30
+ res.json({
31
+ profiles: [],
32
+ error: err instanceof Error ? err.message : String(err)
33
+ });
34
+ }
35
+ };
36
+ run();
37
+ });
38
+ };
39
+ //#endregion
40
+ export { init as default };
@@ -0,0 +1,175 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('moltnet-task-builder', {
3
+ category: 'moltnet',
4
+ color: '#00d4c8',
5
+ paletteLabel: 'task: build',
6
+ defaults: {
7
+ name: { value: '' },
8
+ agent: { value: '', type: 'moltnet-agent', required: true },
9
+ taskType: { value: 'freeform' },
10
+ brief: { value: '' },
11
+ title: { value: '' },
12
+ tags: { value: '' },
13
+ teamId: { value: '' },
14
+ teamIdType: { value: 'str' },
15
+ diaryId: { value: '' },
16
+ diaryIdType: { value: 'str' },
17
+ contexts: { value: [] },
18
+ referencesFrom: { value: '' },
19
+ referencesRole: { value: 'context' },
20
+ submitOutputGate: { value: true },
21
+ schemaCid: { value: '' },
22
+ workspace: { value: '' },
23
+ constraints: { value: [] },
24
+ expectedOutput: { value: '' },
25
+ },
26
+ inputs: 1,
27
+ outputs: 1,
28
+ icon: 'font-awesome/fa-wrench',
29
+ label: function () {
30
+ return this.name || 'task: build';
31
+ },
32
+ oneditprepare: function () {
33
+ // Team/diary overrides: typedInput (literal or msg/flow/global path).
34
+ // Blank ⇒ inherit the agent's context.
35
+ $('#node-input-teamId').typedInput({
36
+ default: 'str',
37
+ typeField: '#node-input-teamIdType',
38
+ types: ['str', 'msg', 'flow', 'global'],
39
+ });
40
+ $('#node-input-diaryId').typedInput({
41
+ default: 'str',
42
+ typeField: '#node-input-diaryIdType',
43
+ types: ['str', 'msg', 'flow', 'global'],
44
+ });
45
+ },
46
+ });
47
+ </script>
48
+
49
+ <script type="text/html" data-template-name="moltnet-task-builder">
50
+ <div class="form-row">
51
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
52
+ <input type="text" id="node-input-name" />
53
+ </div>
54
+ <div class="form-row">
55
+ <label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
56
+ <input type="text" id="node-input-agent" />
57
+ </div>
58
+ <div class="form-row">
59
+ <label for="node-input-taskType"
60
+ ><i class="fa fa-cube"></i> Task type</label
61
+ >
62
+ <select id="node-input-taskType">
63
+ <option value="freeform">freeform</option>
64
+ </select>
65
+ </div>
66
+ <div class="form-row">
67
+ <label for="node-input-brief"><i class="fa fa-file-text"></i> Brief</label>
68
+ <input
69
+ type="text"
70
+ id="node-input-brief"
71
+ placeholder="msg.payload.input.brief"
72
+ />
73
+ </div>
74
+ <div class="form-row">
75
+ <label for="node-input-title"><i class="fa fa-header"></i> Title</label>
76
+ <input type="text" id="node-input-title" placeholder="(optional)" />
77
+ </div>
78
+ <div class="form-row">
79
+ <label for="node-input-tags"><i class="fa fa-tags"></i> Tags</label>
80
+ <input
81
+ type="text"
82
+ id="node-input-tags"
83
+ placeholder="comma-separated, e.g. triage,issue-1"
84
+ />
85
+ </div>
86
+ <div class="form-row">
87
+ <label for="node-input-teamId"
88
+ ><i class="fa fa-users"></i> Team (override)</label
89
+ >
90
+ <input type="text" id="node-input-teamId" style="width:70%" />
91
+ <input type="hidden" id="node-input-teamIdType" />
92
+ </div>
93
+ <div class="form-row">
94
+ <label for="node-input-diaryId"
95
+ ><i class="fa fa-book"></i> Diary (override)</label
96
+ >
97
+ <input type="text" id="node-input-diaryId" style="width:70%" />
98
+ <input type="hidden" id="node-input-diaryIdType" />
99
+ </div>
100
+ <div class="form-row node-builder-group" id="group-references">
101
+ <label for="node-input-referencesFrom"
102
+ ><i class="fa fa-link"></i> References from</label
103
+ >
104
+ <input
105
+ type="text"
106
+ id="node-input-referencesFrom"
107
+ placeholder="result.outputRef"
108
+ />
109
+ </div>
110
+ <div class="form-row">
111
+ <label for="node-input-referencesRole">Role</label>
112
+ <select id="node-input-referencesRole">
113
+ <option value="context">context</option>
114
+ <option value="judged_work">judged_work</option>
115
+ <option value="reviewed_diff">reviewed_diff</option>
116
+ <option value="target_source">target_source</option>
117
+ </select>
118
+ </div>
119
+ <div class="form-row">
120
+ <label for="node-input-submitOutputGate">Submit gate</label>
121
+ <input
122
+ type="checkbox"
123
+ id="node-input-submitOutputGate"
124
+ style="width:auto"
125
+ />
126
+ </div>
127
+ <div class="form-row">
128
+ <label for="node-input-workspace"
129
+ ><i class="fa fa-folder"></i> Workspace</label
130
+ >
131
+ <select id="node-input-workspace">
132
+ <option value="">(default)</option>
133
+ <option value="none">none</option>
134
+ <option value="shared_mount">shared_mount</option>
135
+ <option value="dedicated_worktree">dedicated_worktree</option>
136
+ </select>
137
+ </div>
138
+ <div class="form-row">
139
+ <label for="node-input-expectedOutput">Expected output</label>
140
+ <input type="text" id="node-input-expectedOutput" />
141
+ </div>
142
+ <div class="form-row">
143
+ <label for="node-input-schemaCid">Schema CID</label>
144
+ <input type="text" id="node-input-schemaCid" />
145
+ </div>
146
+ </script>
147
+
148
+ <script type="text/html" data-help-name="moltnet-task-builder">
149
+ <p>
150
+ Composes a validated MoltNet <code>tasks.create</code> body from config +
151
+ the incoming message, using the SDK fluent builder. The built body —
152
+ <b>task type</b>, <b>title</b>, <b>tags</b>, <code>input</code>,
153
+ <code>references</code>, gates — is set on <code>msg.payload</code> for a
154
+ downstream <code>tasks: create</code>. Validation errors show on the node
155
+ (red ring) with the offending field.
156
+ </p>
157
+ <p>
158
+ <b>Title</b> and <b>Tags</b> (comma-separated → <code>string[]</code>) are
159
+ optional; <code>msg.payload.title</code> /
160
+ <code>msg.payload.tags</code> override the node values.
161
+ <b>Task type</b> defaults to <code>freeform</code>.
162
+ </p>
163
+ <p>
164
+ <b>Team / Diary (override)</b> default to the referenced <b>agent</b>'s
165
+ context. Set either field (literal, or a
166
+ <code>msg</code>/<code>flow</code>/<code>global</code> path) only to
167
+ <i>override</i> the agent for this task; leave blank to inherit.
168
+ </p>
169
+ <p>
170
+ <b>Context</b> rows map a slug to a value source (msg/flow/global/str/json);
171
+ objects are JSON-stringified. <b>References from</b> reads an
172
+ <code>outputRef</code> emitted by <code>task: read</code> to chain tasks
173
+ (the <code>outputCid</code> is pulled automatically).
174
+ </p>
175
+ </script>
@@ -0,0 +1,110 @@
1
+ import { TaskBuildError, buildTask } from "@themoltnet/sdk";
2
+ //#region src/nodes/task-builder.ts
3
+ /** Split a comma-separated config string into trimmed, non-empty values. */
4
+ function parseCsv(raw) {
5
+ if (!raw) return [];
6
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
7
+ }
8
+ /** Resolve a context mapping's raw value from the message / context stores / literal. */
9
+ function resolveValue(RED, node, msg, m) {
10
+ switch (m.valueType) {
11
+ case "msg": return RED.util.getMessageProperty(msg, m.value);
12
+ case "flow": return node.context().flow.get(m.value);
13
+ case "global": return node.context().global.get(m.value);
14
+ case "json": try {
15
+ return JSON.parse(m.value);
16
+ } catch {
17
+ return m.value;
18
+ }
19
+ default: return m.value;
20
+ }
21
+ }
22
+ /**
23
+ * Resolve a typedInput override (literal or msg/flow/global path). Returns
24
+ * `undefined` when the value field is blank so the caller can fall back to the
25
+ * agent default.
26
+ */
27
+ function resolveOverride(RED, node, msg, value, type) {
28
+ if (!value) return void 0;
29
+ switch (type) {
30
+ case "msg": {
31
+ const v = RED.util.getMessageProperty(msg, value);
32
+ return typeof v === "string" && v ? v : void 0;
33
+ }
34
+ case "flow": {
35
+ const v = node.context().flow.get(value);
36
+ return typeof v === "string" && v ? v : void 0;
37
+ }
38
+ case "global": {
39
+ const v = node.context().global.get(value);
40
+ return typeof v === "string" && v ? v : void 0;
41
+ }
42
+ default: return value;
43
+ }
44
+ }
45
+ var init = (RED) => {
46
+ function TaskBuilderNode(def) {
47
+ RED.nodes.createNode(this, def);
48
+ const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
49
+ this.on("input", (msg, send, done) => {
50
+ try {
51
+ const payloadInput = msg.payload && typeof msg.payload === "object" ? msg.payload : {};
52
+ const brief = typeof payloadInput.input?.brief === "string" ? payloadInput.input.brief : def.brief ?? "";
53
+ const builder = buildTask(def.taskType?.trim() || "freeform", { brief });
54
+ const teamId = resolveOverride(RED, this, msg, def.teamId, def.teamIdType) ?? payloadInput.teamId ?? agentNode?.teamId;
55
+ const diaryId = resolveOverride(RED, this, msg, def.diaryId, def.diaryIdType) ?? payloadInput.diaryId ?? agentNode?.diaryId;
56
+ if (teamId) builder.team(teamId);
57
+ if (diaryId) builder.diary(diaryId);
58
+ for (const m of def.contexts ?? []) {
59
+ if (!m?.slug) continue;
60
+ const value = resolveValue(RED, this, msg, m);
61
+ const binding = m.binding ?? "context_inline";
62
+ if (binding === "context_inline") builder.contextInline(m.slug, value);
63
+ else if (binding === "user_inline") builder.userInline(m.slug, value);
64
+ else {
65
+ const content = typeof value === "string" ? value : JSON.stringify(value);
66
+ builder.context(m.slug, binding, content);
67
+ }
68
+ }
69
+ if (def.referencesFrom) {
70
+ const ref = RED.util.getMessageProperty(msg, def.referencesFrom);
71
+ if (ref && ref.outputCid) builder.references(ref, def.referencesRole ?? "context");
72
+ }
73
+ if (def.submitOutputGate) builder.requireSubmitOutput();
74
+ if (def.schemaCid) builder.requireSchema(def.schemaCid);
75
+ const inputPatch = {};
76
+ if (def.workspace) inputPatch.execution = { workspace: def.workspace };
77
+ if (def.constraints && def.constraints.length > 0) inputPatch.constraints = def.constraints;
78
+ if (def.expectedOutput) inputPatch.expectedOutput = def.expectedOutput;
79
+ if (Object.keys(inputPatch).length > 0) builder.input(inputPatch);
80
+ const title = typeof payloadInput.title === "string" && payloadInput.title ? payloadInput.title : def.title?.trim();
81
+ if (title) builder.title(title);
82
+ const tags = Array.isArray(payloadInput.tags) ? payloadInput.tags : parseCsv(def.tags);
83
+ if (tags.length > 0) builder.tags(...tags);
84
+ const built = builder.build();
85
+ const out = RED.util.cloneMessage(msg);
86
+ out.payload = {
87
+ ...built.body,
88
+ teamId: built.teamId
89
+ };
90
+ this.status({
91
+ fill: "green",
92
+ shape: "dot",
93
+ text: "built"
94
+ });
95
+ send(out);
96
+ done();
97
+ } catch (err) {
98
+ this.status({
99
+ fill: "red",
100
+ shape: "ring",
101
+ text: "error"
102
+ });
103
+ done(err instanceof TaskBuildError ? new Error(err.message) : err instanceof Error ? err : new Error(String(err)));
104
+ }
105
+ });
106
+ }
107
+ RED.nodes.registerType("moltnet-task-builder", TaskBuilderNode);
108
+ };
109
+ //#endregion
110
+ export { init as default, resolveOverride };
@@ -0,0 +1,55 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('moltnet-task-get', {
3
+ category: 'moltnet',
4
+ color: '#00d4c8',
5
+ paletteLabel: 'task: get',
6
+ defaults: {
7
+ name: { value: '' },
8
+ agent: { value: '', type: 'moltnet-agent', required: true },
9
+ taskId: { value: '' },
10
+ },
11
+ inputs: 1,
12
+ outputs: 1,
13
+ icon: 'font-awesome/fa-search',
14
+ label: function () {
15
+ return this.name || 'task: get';
16
+ },
17
+ });
18
+ </script>
19
+
20
+ <script type="text/html" data-template-name="moltnet-task-get">
21
+ <div class="form-row">
22
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
23
+ <input type="text" id="node-input-name" />
24
+ </div>
25
+ <div class="form-row">
26
+ <label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
27
+ <input type="text" id="node-input-agent" />
28
+ </div>
29
+ <div class="form-row">
30
+ <label for="node-input-taskId"><i class="fa fa-hashtag"></i> Task ID</label>
31
+ <input
32
+ type="text"
33
+ id="node-input-taskId"
34
+ placeholder="msg.payload.taskId"
35
+ />
36
+ </div>
37
+ </script>
38
+
39
+ <script type="text/html" data-help-name="moltnet-task-get">
40
+ <p>
41
+ Reads a MoltNet task and its attempts once (no polling). The task id is
42
+ taken from <code>msg.taskId</code>, <code>msg.payload.taskId</code>, or
43
+ <code>msg.payload.id</code> (the shape <code>tasks: create</code> emits),
44
+ falling back to the configured Task ID.
45
+ </p>
46
+ <p>
47
+ Emits a normalized snapshot on <code>msg.payload</code>:
48
+ <code
49
+ >{ taskId, status, terminal, accepted, acceptedAttemptN, state, attempt,
50
+ attempts, error, task }</code
51
+ >. <code>state</code> is the accepted attempt's output artifact, or
52
+ <code>null</code> when not yet accepted.
53
+ </p>
54
+ <p>For "block until the run settles", use <code>task: wait</code> instead.</p>
55
+ </script>
@@ -0,0 +1,60 @@
1
+ import { t as buildTaskSnapshot } from "./task-snapshot.js";
2
+ //#region src/nodes/task-get.ts
3
+ var init = (RED) => {
4
+ function TaskGetNode(def) {
5
+ RED.nodes.createNode(this, def);
6
+ const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
7
+ this.on("input", (msg, send, done) => {
8
+ const run = async () => {
9
+ try {
10
+ if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-get: no moltnet-agent configured");
11
+ const taskId = resolveTaskId(msg, def.taskId);
12
+ if (!taskId) throw new Error("task-get: taskId is required");
13
+ this.status({
14
+ fill: "blue",
15
+ shape: "dot",
16
+ text: "loading…"
17
+ });
18
+ const agent = await agentNode.getAgent();
19
+ const [task, attempts] = await Promise.all([agent.tasks.get(taskId), agent.tasks.listAttempts(taskId)]);
20
+ const snapshot = buildTaskSnapshot(task, attempts);
21
+ const out = RED.util.cloneMessage(msg);
22
+ out.payload = snapshot;
23
+ this.status({
24
+ fill: snapshot.accepted ? "green" : "grey",
25
+ shape: "dot",
26
+ text: `${snapshot.status}${snapshot.accepted ? " ✓" : ""}`
27
+ });
28
+ send(out);
29
+ done();
30
+ } catch (err) {
31
+ this.status({
32
+ fill: "red",
33
+ shape: "ring",
34
+ text: "error"
35
+ });
36
+ done(err instanceof Error ? err : new Error(String(err)));
37
+ }
38
+ };
39
+ run();
40
+ });
41
+ }
42
+ RED.nodes.registerType("moltnet-task-get", TaskGetNode);
43
+ };
44
+ /**
45
+ * Resolve the task id from the message (in priority order) or fall back to the
46
+ * node's configured id. Accepts both `msg.payload.taskId` and the shape emitted
47
+ * by `moltnet-tasks-create` (`msg.payload.id`).
48
+ */
49
+ function resolveTaskId(msg, configured) {
50
+ if (typeof msg.taskId === "string" && msg.taskId) return msg.taskId;
51
+ const payload = msg.payload;
52
+ if (payload && typeof payload === "object") {
53
+ const p = payload;
54
+ if (typeof p.taskId === "string" && p.taskId) return p.taskId;
55
+ if (typeof p.id === "string" && p.id) return p.id;
56
+ }
57
+ return configured && configured.length > 0 ? configured : void 0;
58
+ }
59
+ //#endregion
60
+ export { init as default };
@@ -0,0 +1,64 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('moltnet-task-reader', {
3
+ category: 'moltnet',
4
+ color: '#00d4c8',
5
+ paletteLabel: 'task: read',
6
+ defaults: {
7
+ name: { value: '' },
8
+ source: { value: '' },
9
+ role: { value: 'context' },
10
+ artifactKind: { value: '' },
11
+ artifactTitle: { value: '' },
12
+ },
13
+ inputs: 1,
14
+ outputs: 1,
15
+ icon: 'font-awesome/fa-book',
16
+ label: function () {
17
+ return this.name || 'task: read';
18
+ },
19
+ });
20
+ </script>
21
+
22
+ <script type="text/html" data-template-name="moltnet-task-reader">
23
+ <div class="form-row">
24
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
25
+ <input type="text" id="node-input-name" />
26
+ </div>
27
+ <div class="form-row">
28
+ <label for="node-input-source"><i class="fa fa-sign-in"></i> Source</label>
29
+ <input type="text" id="node-input-source" placeholder="payload" />
30
+ </div>
31
+ <div class="form-row">
32
+ <label for="node-input-role"><i class="fa fa-link"></i> Output role</label>
33
+ <select id="node-input-role">
34
+ <option value="context">context</option>
35
+ <option value="judged_work">judged_work</option>
36
+ <option value="reviewed_diff">reviewed_diff</option>
37
+ <option value="target_source">target_source</option>
38
+ </select>
39
+ </div>
40
+ <div class="form-row">
41
+ <label for="node-input-artifactKind">Artifact kind</label>
42
+ <input type="text" id="node-input-artifactKind" placeholder="(optional)" />
43
+ </div>
44
+ <div class="form-row">
45
+ <label for="node-input-artifactTitle">Artifact title</label>
46
+ <input type="text" id="node-input-artifactTitle" placeholder="(optional)" />
47
+ </div>
48
+ </script>
49
+
50
+ <script type="text/html" data-help-name="moltnet-task-reader">
51
+ <p>
52
+ Parses a completed task snapshot (from <code>task: wait</code>/<code
53
+ >task: get</code
54
+ >) into typed result data via the SDK reader. Sets the typed output on
55
+ <code>msg.payload</code> and a flat <code>msg.result</code> with
56
+ <code>{ summary, outputRef, artifact, artifactBody, accepted, usage }</code
57
+ >. The <code>outputRef</code> chains directly into a downstream
58
+ <code>task: build</code>'s "References from".
59
+ </p>
60
+ <p>
61
+ Set <b>Artifact kind/title</b> to pre-parse a JSON artifact body into
62
+ <code>msg.result.artifactBody</code>.
63
+ </p>
64
+ </script>