@themoltnet/node-red-contrib-core 0.4.0 → 0.6.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.
package/README.md CHANGED
@@ -15,12 +15,19 @@ Empirically validated against **Node-RED 5.0.0** (Node 22):
15
15
  package carries no private-package runtime dependency. `@themoltnet/sdk` is
16
16
  therefore a **devDependency** (bundled, not installed at runtime).
17
17
  - The `.html` editor files are copied to `dist/nodes/` as assets (not compiled).
18
- - Two **config nodes** (`moltnet-agent`, `moltnet-runtime-profile`) and eight
18
+ - Two **config nodes** (`moltnet-agent`, `moltnet-runtime-profile`) and eleven
19
19
  **action nodes** (`moltnet-tasks-create`, `moltnet-task-get`,
20
- `moltnet-task-wait`, `moltnet-workflow-status`, `moltnet-task-builder`,
21
- `moltnet-task-reader`, `moltnet-tasks-list`, `moltnet-entries-search`)
20
+ `moltnet-task-wait`, `moltnet-task-artifacts-list`,
21
+ `moltnet-task-artifact-upload`, `moltnet-task-artifact-download`,
22
+ `moltnet-workflow-status`, `moltnet-task-builder`, `moltnet-task-reader`,
23
+ `moltnet-tasks-list`, `moltnet-entries-search`)
22
24
  register and appear in the palette.
23
25
 
26
+ For editor styling, install the separate companion package
27
+ `@themoltnet/node-red-theme`. The theme is intentionally not bundled with these
28
+ runtime nodes so it can be used independently by Node-RED instances that only
29
+ want the MoltNet editor skin.
30
+
24
31
  ## Nodes
25
32
 
26
33
  - **`moltnet-agent`** (config) — holds one MoltNet agent identity (OAuth2 client
@@ -57,6 +64,21 @@ attempts, error, task }`. `state` is the accepted attempt's output artifact
57
64
  snapshot once. On failure the snapshot's `error` carries the last attempt's
58
65
  error for an agent/human to interpret (retry vs. escalate) — the same hook the
59
66
  `issue-lifecycle` supervisor uses.
67
+ - **`moltnet-task-artifacts-list`** (palette: _task artifacts: list_) — lists a
68
+ task's artifacts for the configured team. Reads task id from
69
+ `msg.taskId`/`msg.payload.taskId`/`msg.payload.id` or the node field, supports
70
+ `limit`/`cursor`, emits artifact rows on `msg.payload`, and places
71
+ pagination/query metadata on `msg.artifacts`.
72
+ - **`moltnet-task-artifact-upload`** (palette: _task artifact: upload_) —
73
+ uploads bytes for a task attempt. Reads bytes from `msg.payload` when it is a
74
+ string/Buffer/Uint8Array/ArrayBuffer, or from object payload fields
75
+ `content`, `body`, or base64 `contentBase64`. Team context is the configured
76
+ node/agent by default; message team overrides require an explicit checkbox.
77
+ Emits artifact metadata on `msg.payload` and `msg.artifact`.
78
+ - **`moltnet-task-artifact-download`** (palette: _task artifact: download_) —
79
+ downloads an artifact by task id, attempt number, and CID. Emits the artifact
80
+ bytes as a Buffer on `msg.payload` and metadata on `msg.artifact`. Upload and
81
+ download nodes enforce a local 25 MiB byte limit by default.
60
82
  - **`moltnet-workflow-status`** (palette: _workflow: status_) — reads the tasks of
61
83
  one workflow run (by `correlationId`) and emits a table-shaped `msg.payload`
62
84
  (array of `{ taskId, type, title, status, queuedAt, completedAt }`) plus
@@ -0,0 +1,86 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('moltnet-task-artifact-download', {
3
+ category: 'moltnet',
4
+ color: '#00d4c8',
5
+ paletteLabel: 'task artifact: download',
6
+ defaults: {
7
+ name: { value: '' },
8
+ agent: { value: '', type: 'moltnet-agent', required: true },
9
+ taskId: { value: '' },
10
+ teamId: { value: '' },
11
+ allowMsgTeamOverride: { value: false },
12
+ attemptN: { value: '', validate: RED.validators.number() },
13
+ maxBytes: { value: 26214400, validate: RED.validators.number() },
14
+ cid: { value: '' },
15
+ },
16
+ inputs: 1,
17
+ outputs: 1,
18
+ icon: 'font-awesome/fa-download',
19
+ label: function () {
20
+ return this.name || 'task artifact: download';
21
+ },
22
+ });
23
+ </script>
24
+
25
+ <script type="text/html" data-template-name="moltnet-task-artifact-download">
26
+ <div class="form-row">
27
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
28
+ <input type="text" id="node-input-name" />
29
+ </div>
30
+ <div class="form-row">
31
+ <label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
32
+ <input type="text" id="node-input-agent" />
33
+ </div>
34
+ <div class="form-row">
35
+ <label for="node-input-taskId"><i class="fa fa-hashtag"></i> Task ID</label>
36
+ <input
37
+ type="text"
38
+ id="node-input-taskId"
39
+ placeholder="msg.payload.taskId"
40
+ />
41
+ </div>
42
+ <div class="form-row">
43
+ <label for="node-input-teamId"><i class="fa fa-users"></i> Team ID</label>
44
+ <input type="text" id="node-input-teamId" placeholder="agent teamId" />
45
+ </div>
46
+ <div class="form-row">
47
+ <label for="node-input-allowMsgTeamOverride"
48
+ ><i class="fa fa-random"></i> Team override</label
49
+ >
50
+ <input
51
+ type="checkbox"
52
+ id="node-input-allowMsgTeamOverride"
53
+ style="display: inline-block; width: auto; vertical-align: top"
54
+ />
55
+ <span>Allow <code>msg.teamId</code></span>
56
+ </div>
57
+ <div class="form-row">
58
+ <label for="node-input-attemptN"
59
+ ><i class="fa fa-history"></i> Attempt</label
60
+ >
61
+ <input type="number" id="node-input-attemptN" placeholder="msg.attemptN" />
62
+ </div>
63
+ <div class="form-row">
64
+ <label for="node-input-cid"><i class="fa fa-fingerprint"></i> CID</label>
65
+ <input type="text" id="node-input-cid" placeholder="msg.payload.cid" />
66
+ </div>
67
+ <div class="form-row">
68
+ <label for="node-input-maxBytes"><i class="fa fa-database"></i> Max</label>
69
+ <input type="number" id="node-input-maxBytes" />
70
+ </div>
71
+ </script>
72
+
73
+ <script type="text/html" data-help-name="moltnet-task-artifact-download">
74
+ <p>
75
+ Downloads a task artifact by task id, attempt number, and CID. CID is taken
76
+ from <code>msg.cid</code>, <code>msg.payload.cid</code>,
77
+ <code>msg.payload.artifact.cid</code>, or the configured CID. Team ID is
78
+ taken from this node or the configured agent unless Team override is
79
+ enabled.
80
+ </p>
81
+ <p>
82
+ Emits the artifact bytes as a Buffer on <code>msg.payload</code>. Download
83
+ metadata is placed on <code>msg.artifact</code>. The local byte limit
84
+ defaults to 25 MiB.
85
+ </p>
86
+ </script>
@@ -0,0 +1,75 @@
1
+ import { a as nonEmpty, t as bool } from "./query-utils.js";
2
+ import { a as requireAttemptContext, n as payloadRecord, r as recordField, s as resolveMaxBytes, t as collectArtifactBody } from "./task-artifact-utils.js";
3
+ //#region src/nodes/task-artifact-download.ts
4
+ var init = (RED) => {
5
+ function TaskArtifactDownloadNode(def) {
6
+ RED.nodes.createNode(this, def);
7
+ const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
8
+ this.on("input", (msg, send, done) => {
9
+ const run = async () => {
10
+ try {
11
+ if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-artifact-download: no moltnet-agent configured");
12
+ const { taskId, teamId, attemptN } = requireAttemptContext("task-artifact-download", msg, def.taskId, def.teamId, def.attemptN, agentNode, bool(def.allowMsgTeamOverride) ?? false);
13
+ const cid = resolveCid(msg, def.cid);
14
+ if (!cid) throw new Error("task-artifact-download: cid is required");
15
+ this.status({
16
+ fill: "blue",
17
+ shape: "dot",
18
+ text: "downloading…"
19
+ });
20
+ const result = await (await agentNode.getAgent()).tasks.artifacts.download({
21
+ taskId,
22
+ attemptN,
23
+ cid
24
+ }, { teamId });
25
+ const body = await collectArtifactBody(result, resolveMaxBytes(def.maxBytes));
26
+ const out = RED.util.cloneMessage({
27
+ ...msg,
28
+ payload: void 0
29
+ });
30
+ out.payload = body;
31
+ out.taskId = taskId;
32
+ out.artifact = {
33
+ taskId,
34
+ teamId,
35
+ attemptN,
36
+ cid,
37
+ ...metadataFromResult(result)
38
+ };
39
+ this.status({
40
+ fill: "green",
41
+ shape: "dot",
42
+ text: `${body.byteLength} byte(s)`
43
+ });
44
+ send(out);
45
+ done();
46
+ } catch (err) {
47
+ this.status({
48
+ fill: "red",
49
+ shape: "ring",
50
+ text: "error"
51
+ });
52
+ done(err instanceof Error ? err : new Error(String(err)));
53
+ }
54
+ };
55
+ run();
56
+ });
57
+ }
58
+ RED.nodes.registerType("moltnet-task-artifact-download", TaskArtifactDownloadNode);
59
+ };
60
+ function resolveCid(msg, configured) {
61
+ if (typeof msg.cid === "string" && msg.cid) return msg.cid;
62
+ const payload = payloadRecord(msg);
63
+ return nonEmpty(payload.cid) ?? nonEmpty(recordField(payload.artifact, "cid")) ?? nonEmpty(configured);
64
+ }
65
+ function metadataFromResult(result) {
66
+ if (!result || typeof result !== "object") return {};
67
+ const record = result;
68
+ return {
69
+ artifactId: record.artifactId,
70
+ contentType: record.contentType,
71
+ contentEncoding: record.contentEncoding
72
+ };
73
+ }
74
+ //#endregion
75
+ export { init as default };
@@ -0,0 +1,118 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('moltnet-task-artifact-upload', {
3
+ category: 'moltnet',
4
+ color: '#00d4c8',
5
+ paletteLabel: 'task artifact: upload',
6
+ defaults: {
7
+ name: { value: '' },
8
+ agent: { value: '', type: 'moltnet-agent', required: true },
9
+ taskId: { value: '' },
10
+ teamId: { value: '' },
11
+ allowMsgTeamOverride: { value: false },
12
+ attemptN: { value: '', validate: RED.validators.number() },
13
+ maxBytes: { value: 26214400, validate: RED.validators.number() },
14
+ kind: { value: 'output' },
15
+ title: { value: '' },
16
+ contentType: { value: 'application/octet-stream' },
17
+ contentEncoding: { value: '' },
18
+ },
19
+ inputs: 1,
20
+ outputs: 1,
21
+ icon: 'font-awesome/fa-upload',
22
+ label: function () {
23
+ return this.name || 'task artifact: upload';
24
+ },
25
+ });
26
+ </script>
27
+
28
+ <script type="text/html" data-template-name="moltnet-task-artifact-upload">
29
+ <div class="form-row">
30
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
31
+ <input type="text" id="node-input-name" />
32
+ </div>
33
+ <div class="form-row">
34
+ <label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
35
+ <input type="text" id="node-input-agent" />
36
+ </div>
37
+ <div class="form-row">
38
+ <label for="node-input-taskId"><i class="fa fa-hashtag"></i> Task ID</label>
39
+ <input
40
+ type="text"
41
+ id="node-input-taskId"
42
+ placeholder="msg.payload.taskId"
43
+ />
44
+ </div>
45
+ <div class="form-row">
46
+ <label for="node-input-teamId"><i class="fa fa-users"></i> Team ID</label>
47
+ <input type="text" id="node-input-teamId" placeholder="agent teamId" />
48
+ </div>
49
+ <div class="form-row">
50
+ <label for="node-input-allowMsgTeamOverride"
51
+ ><i class="fa fa-random"></i> Team override</label
52
+ >
53
+ <input
54
+ type="checkbox"
55
+ id="node-input-allowMsgTeamOverride"
56
+ style="display: inline-block; width: auto; vertical-align: top"
57
+ />
58
+ <span>Allow <code>msg.teamId</code></span>
59
+ </div>
60
+ <div class="form-row">
61
+ <label for="node-input-attemptN"
62
+ ><i class="fa fa-history"></i> Attempt</label
63
+ >
64
+ <input type="number" id="node-input-attemptN" placeholder="msg.attemptN" />
65
+ </div>
66
+ <div class="form-row">
67
+ <label for="node-input-kind"><i class="fa fa-archive"></i> Kind</label>
68
+ <select id="node-input-kind">
69
+ <option value="output">Output</option>
70
+ <option value="input">Input</option>
71
+ <option value="log">Log</option>
72
+ <option value="other">Other</option>
73
+ </select>
74
+ </div>
75
+ <div class="form-row">
76
+ <label for="node-input-title"><i class="fa fa-font"></i> Title</label>
77
+ <input type="text" id="node-input-title" placeholder="summary.md" />
78
+ </div>
79
+ <div class="form-row">
80
+ <label for="node-input-maxBytes"><i class="fa fa-database"></i> Max</label>
81
+ <input type="number" id="node-input-maxBytes" />
82
+ </div>
83
+ <div class="form-row">
84
+ <label for="node-input-contentType"
85
+ ><i class="fa fa-file-o"></i> Type</label
86
+ >
87
+ <input
88
+ type="text"
89
+ id="node-input-contentType"
90
+ placeholder="text/markdown"
91
+ />
92
+ </div>
93
+ <div class="form-row">
94
+ <label for="node-input-contentEncoding"
95
+ ><i class="fa fa-compress"></i> Encoding</label
96
+ >
97
+ <input type="text" id="node-input-contentEncoding" placeholder="gzip" />
98
+ </div>
99
+ </script>
100
+
101
+ <script type="text/html" data-help-name="moltnet-task-artifact-upload">
102
+ <p>
103
+ Uploads bytes as an artifact for a task attempt. Task id, attempt, kind,
104
+ title, content type, and content encoding can be supplied by the node fields
105
+ or matching fields on <code>msg.payload</code>. Team ID is taken from this
106
+ node or the configured agent unless Team override is enabled.
107
+ </p>
108
+ <p>
109
+ The body is read from <code>msg.payload</code> when it is a string, Buffer,
110
+ Uint8Array, or ArrayBuffer. Object payloads may provide
111
+ <code>content</code>, <code>body</code>, or base64
112
+ <code>contentBase64</code>. The local byte limit defaults to 25 MiB.
113
+ </p>
114
+ <p>
115
+ Emits the artifact metadata on <code>msg.payload</code> and
116
+ <code>msg.artifact</code>.
117
+ </p>
118
+ </script>
@@ -0,0 +1,61 @@
1
+ import { t as bool } from "./query-utils.js";
2
+ import { a as requireAttemptContext, c as resolveUploadBody, o as resolveField, s as resolveMaxBytes } from "./task-artifact-utils.js";
3
+ //#region src/nodes/task-artifact-upload.ts
4
+ var init = (RED) => {
5
+ function TaskArtifactUploadNode(def) {
6
+ RED.nodes.createNode(this, def);
7
+ const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
8
+ this.on("input", (msg, send, done) => {
9
+ const run = async () => {
10
+ try {
11
+ if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-artifact-upload: no moltnet-agent configured");
12
+ const { taskId, teamId, attemptN } = requireAttemptContext("task-artifact-upload", msg, def.taskId, def.teamId, def.attemptN, agentNode, bool(def.allowMsgTeamOverride) ?? false);
13
+ const body = resolveUploadBody(msg, resolveMaxBytes(def.maxBytes));
14
+ const query = buildUploadQuery(def, msg);
15
+ this.status({
16
+ fill: "blue",
17
+ shape: "dot",
18
+ text: "uploading…"
19
+ });
20
+ const artifact = await (await agentNode.getAgent()).tasks.artifacts.upload({
21
+ taskId,
22
+ attemptN
23
+ }, body, query, { teamId });
24
+ const out = RED.util.cloneMessage({
25
+ ...msg,
26
+ payload: void 0
27
+ });
28
+ out.payload = artifact;
29
+ out.taskId = taskId;
30
+ out.artifact = artifact;
31
+ this.status({
32
+ fill: "green",
33
+ shape: "dot",
34
+ text: artifact.cid
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-task-artifact-upload", TaskArtifactUploadNode);
51
+ };
52
+ function buildUploadQuery(def, msg) {
53
+ return {
54
+ kind: resolveField(msg, "kind", def.kind) ?? "output",
55
+ title: resolveField(msg, "title", def.title) ?? "artifact",
56
+ contentType: resolveField(msg, "contentType", def.contentType) ?? "application/octet-stream",
57
+ contentEncoding: resolveField(msg, "contentEncoding", def.contentEncoding)
58
+ };
59
+ }
60
+ //#endregion
61
+ export { init as default };
@@ -0,0 +1,149 @@
1
+ import { a as nonEmpty, c as positiveInt } from "./query-utils.js";
2
+ import { Readable } from "node:stream";
3
+ import { Buffer } from "node:buffer";
4
+ function payloadRecord(msg) {
5
+ if (!msg.payload || typeof msg.payload !== "object") return {};
6
+ if (Buffer.isBuffer(msg.payload)) return {};
7
+ return msg.payload;
8
+ }
9
+ function resolveTaskId(msg, configured) {
10
+ if (typeof msg.taskId === "string" && msg.taskId) return msg.taskId;
11
+ const payload = payloadRecord(msg);
12
+ return nonEmpty(payload.taskId) ?? nonEmpty(payload.id) ?? nonEmpty(recordField(payload.task, "id")) ?? nonEmpty(configured);
13
+ }
14
+ function resolveTeamId(msg, configured, agentNode, allowMsgTeamOverride) {
15
+ if (!allowMsgTeamOverride) return nonEmpty(configured) ?? agentNode.teamId;
16
+ if (typeof msg.teamId === "string" && msg.teamId) return msg.teamId;
17
+ return nonEmpty(payloadRecord(msg).teamId) ?? nonEmpty(configured) ?? agentNode.teamId;
18
+ }
19
+ function resolveAttemptN(msg, configured) {
20
+ const payload = payloadRecord(msg);
21
+ return positiveInt(msg.attemptN) ?? positiveInt(payload.attemptN) ?? positiveInt(recordField(payload.attempt, "attemptN")) ?? positiveInt(recordField(payload.artifact, "attemptN")) ?? positiveInt(configured);
22
+ }
23
+ function requireArtifactContext(nodeName, msg, configuredTaskId, configuredTeamId, agentNode, allowMsgTeamOverride) {
24
+ const taskId = resolveTaskId(msg, configuredTaskId);
25
+ if (!taskId) throw new Error(`${nodeName}: taskId is required`);
26
+ const teamId = resolveTeamId(msg, configuredTeamId, agentNode, allowMsgTeamOverride);
27
+ if (!teamId) throw new Error(`${nodeName}: teamId is required`);
28
+ return {
29
+ taskId,
30
+ teamId
31
+ };
32
+ }
33
+ function requireAttemptContext(nodeName, msg, configuredTaskId, configuredTeamId, configuredAttemptN, agentNode, allowMsgTeamOverride) {
34
+ const context = requireArtifactContext(nodeName, msg, configuredTaskId, configuredTeamId, agentNode, allowMsgTeamOverride);
35
+ const attemptN = resolveAttemptN(msg, configuredAttemptN);
36
+ if (!attemptN) throw new Error(`${nodeName}: attemptN is required`);
37
+ return {
38
+ ...context,
39
+ attemptN
40
+ };
41
+ }
42
+ function resolveField(msg, name, configured) {
43
+ return nonEmpty(payloadRecord(msg)[name]) ?? nonEmpty(configured);
44
+ }
45
+ function resolveMaxBytes(configured) {
46
+ return positiveInt(configured) ?? 26214400;
47
+ }
48
+ function resolveUploadBody(msg, maxBytes) {
49
+ const payload = msg.payload;
50
+ if (Buffer.isBuffer(payload)) return enforceMaxBytes(payload, maxBytes);
51
+ if (payload instanceof Uint8Array) return enforceMaxBytes(payload, maxBytes);
52
+ if (payload instanceof ArrayBuffer) {
53
+ if (payload.byteLength > maxBytes) throw tooLarge("upload", maxBytes);
54
+ return new Uint8Array(payload);
55
+ }
56
+ if (typeof payload === "string") {
57
+ if (Buffer.byteLength(payload) > maxBytes) throw tooLarge("upload", maxBytes);
58
+ return new TextEncoder().encode(payload);
59
+ }
60
+ const record = payloadRecord(msg);
61
+ if (typeof record.contentBase64 === "string") {
62
+ const normalized = record.contentBase64.replace(/\s/g, "");
63
+ if (decodedBase64Length(normalized) > maxBytes) throw tooLarge("upload", maxBytes);
64
+ return Buffer.from(normalized, "base64");
65
+ }
66
+ const content = record.content ?? record.body;
67
+ if (Buffer.isBuffer(content)) return enforceMaxBytes(content, maxBytes);
68
+ if (content instanceof Uint8Array) return enforceMaxBytes(content, maxBytes);
69
+ if (content instanceof ArrayBuffer) {
70
+ if (content.byteLength > maxBytes) throw tooLarge("upload", maxBytes);
71
+ return new Uint8Array(content);
72
+ }
73
+ if (typeof content === "string") {
74
+ if (Buffer.byteLength(content) > maxBytes) throw tooLarge("upload", maxBytes);
75
+ return new TextEncoder().encode(content);
76
+ }
77
+ throw new Error("task-artifact-upload: payload content is required");
78
+ }
79
+ async function collectArtifactBody(value, maxBytes) {
80
+ const source = value && typeof value === "object" && "stream" in value ? value.stream : value;
81
+ if (Buffer.isBuffer(source)) {
82
+ if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes);
83
+ return source;
84
+ }
85
+ if (source instanceof Uint8Array) {
86
+ if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes);
87
+ return Buffer.from(source);
88
+ }
89
+ if (source instanceof ArrayBuffer) {
90
+ if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes);
91
+ return Buffer.from(source);
92
+ }
93
+ if (typeof source === "string") {
94
+ if (Buffer.byteLength(source) > maxBytes) throw tooLarge("download", maxBytes);
95
+ return Buffer.from(source);
96
+ }
97
+ if (source instanceof Readable) {
98
+ const chunks = [];
99
+ let bytes = 0;
100
+ for await (const chunk of source) bytes = pushChunk(chunks, chunk, bytes, maxBytes);
101
+ return Buffer.concat(chunks);
102
+ }
103
+ if (source && typeof source === "object" && Symbol.asyncIterator in source) {
104
+ const chunks = [];
105
+ let bytes = 0;
106
+ for await (const chunk of source) bytes = pushChunk(chunks, chunk, bytes, maxBytes);
107
+ return Buffer.concat(chunks);
108
+ }
109
+ if (source && typeof source === "object" && "arrayBuffer" in source) {
110
+ const size = source.size;
111
+ if (typeof size === "number" && size > maxBytes) throw tooLarge("download", maxBytes);
112
+ const arrayBuffer = await source.arrayBuffer();
113
+ if (arrayBuffer.byteLength > maxBytes) throw tooLarge("download", maxBytes);
114
+ return Buffer.from(arrayBuffer);
115
+ }
116
+ throw new Error("task-artifact-download: unsupported artifact body");
117
+ }
118
+ function toBuffer(value) {
119
+ if (Buffer.isBuffer(value)) return value;
120
+ if (value instanceof Uint8Array) return Buffer.from(value);
121
+ if (value instanceof ArrayBuffer) return Buffer.from(value);
122
+ if (typeof value === "string") return Buffer.from(value);
123
+ return Buffer.from(String(value));
124
+ }
125
+ function recordField(value, key) {
126
+ if (!value || typeof value !== "object") return void 0;
127
+ return value[key];
128
+ }
129
+ function enforceMaxBytes(value, maxBytes) {
130
+ if (value.byteLength > maxBytes) throw tooLarge("upload", maxBytes);
131
+ return value;
132
+ }
133
+ function pushChunk(chunks, chunk, bytes, maxBytes) {
134
+ const next = toBuffer(chunk);
135
+ const total = bytes + next.byteLength;
136
+ if (total > maxBytes) throw tooLarge("download", maxBytes);
137
+ chunks.push(next);
138
+ return total;
139
+ }
140
+ function decodedBase64Length(value) {
141
+ if (!value) return 0;
142
+ const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
143
+ return Math.floor(value.length * 3 / 4) - padding;
144
+ }
145
+ function tooLarge(operation, maxBytes) {
146
+ return /* @__PURE__ */ new Error(`task-artifact-${operation}: artifact body exceeds ${maxBytes} bytes`);
147
+ }
148
+ //#endregion
149
+ export { requireAttemptContext as a, resolveUploadBody as c, requireArtifactContext as i, payloadRecord as n, resolveField as o, recordField as r, resolveMaxBytes as s, collectArtifactBody as t };
@@ -0,0 +1,78 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('moltnet-task-artifacts-list', {
3
+ category: 'moltnet',
4
+ color: '#00d4c8',
5
+ paletteLabel: 'task artifacts: list',
6
+ defaults: {
7
+ name: { value: '' },
8
+ agent: { value: '', type: 'moltnet-agent', required: true },
9
+ taskId: { value: '' },
10
+ teamId: { value: '' },
11
+ allowMsgTeamOverride: { value: false },
12
+ limit: { value: 20, validate: RED.validators.number() },
13
+ cursor: { value: '' },
14
+ },
15
+ inputs: 1,
16
+ outputs: 1,
17
+ icon: 'font-awesome/fa-list',
18
+ label: function () {
19
+ return this.name || 'task artifacts: list';
20
+ },
21
+ });
22
+ </script>
23
+
24
+ <script type="text/html" data-template-name="moltnet-task-artifacts-list">
25
+ <div class="form-row">
26
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
27
+ <input type="text" id="node-input-name" />
28
+ </div>
29
+ <div class="form-row">
30
+ <label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
31
+ <input type="text" id="node-input-agent" />
32
+ </div>
33
+ <div class="form-row">
34
+ <label for="node-input-taskId"><i class="fa fa-hashtag"></i> Task ID</label>
35
+ <input
36
+ type="text"
37
+ id="node-input-taskId"
38
+ placeholder="msg.payload.taskId"
39
+ />
40
+ </div>
41
+ <div class="form-row">
42
+ <label for="node-input-teamId"><i class="fa fa-users"></i> Team ID</label>
43
+ <input type="text" id="node-input-teamId" placeholder="agent teamId" />
44
+ </div>
45
+ <div class="form-row">
46
+ <label for="node-input-allowMsgTeamOverride"
47
+ ><i class="fa fa-random"></i> Team override</label
48
+ >
49
+ <input
50
+ type="checkbox"
51
+ id="node-input-allowMsgTeamOverride"
52
+ style="display: inline-block; width: auto; vertical-align: top"
53
+ />
54
+ <span>Allow <code>msg.teamId</code></span>
55
+ </div>
56
+ <div class="form-row">
57
+ <label for="node-input-limit"><i class="fa fa-hashtag"></i> Limit</label>
58
+ <input type="number" id="node-input-limit" />
59
+ </div>
60
+ <div class="form-row">
61
+ <label for="node-input-cursor"><i class="fa fa-forward"></i> Cursor</label>
62
+ <input type="text" id="node-input-cursor" />
63
+ </div>
64
+ </script>
65
+
66
+ <script type="text/html" data-help-name="moltnet-task-artifacts-list">
67
+ <p>
68
+ Lists artifacts for a MoltNet task. The task id is taken from
69
+ <code>msg.taskId</code>, <code>msg.payload.taskId</code>,
70
+ <code>msg.payload.id</code>, or the configured Task ID. Team ID is taken
71
+ from this node or the configured agent. Enable Team override to allow
72
+ <code>msg.teamId</code> or <code>msg.payload.teamId</code>.
73
+ </p>
74
+ <p>
75
+ Emits artifact rows on <code>msg.payload</code>. Pagination metadata, the
76
+ final query, and the full page are on <code>msg.artifacts</code>.
77
+ </p>
78
+ </script>
@@ -0,0 +1,61 @@
1
+ import { a as nonEmpty, c as positiveInt, n as compact, t as bool } from "./query-utils.js";
2
+ import { i as requireArtifactContext, n as payloadRecord } from "./task-artifact-utils.js";
3
+ //#region src/nodes/task-artifacts-list.ts
4
+ var init = (RED) => {
5
+ function TaskArtifactsListNode(def) {
6
+ RED.nodes.createNode(this, def);
7
+ const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
8
+ this.on("input", (msg, send, done) => {
9
+ const run = async () => {
10
+ try {
11
+ if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-artifacts-list: no moltnet-agent configured");
12
+ const { taskId, teamId } = requireArtifactContext("task-artifacts-list", msg, def.taskId, def.teamId, agentNode, bool(def.allowMsgTeamOverride) ?? false);
13
+ this.status({
14
+ fill: "blue",
15
+ shape: "dot",
16
+ text: "loading…"
17
+ });
18
+ const agent = await agentNode.getAgent();
19
+ const query = buildQuery(def, msg);
20
+ const page = await agent.tasks.artifacts.listPage(taskId, query, { teamId });
21
+ const out = RED.util.cloneMessage(msg);
22
+ out.payload = page.artifacts;
23
+ out.taskId = taskId;
24
+ out.artifacts = {
25
+ taskId,
26
+ teamId,
27
+ query,
28
+ count: page.artifacts.length,
29
+ nextCursor: page.nextCursor,
30
+ page
31
+ };
32
+ this.status({
33
+ fill: "green",
34
+ shape: "dot",
35
+ text: `${page.artifacts.length} artifact(s)`
36
+ });
37
+ send(out);
38
+ done();
39
+ } catch (err) {
40
+ this.status({
41
+ fill: "red",
42
+ shape: "ring",
43
+ text: "error"
44
+ });
45
+ done(err instanceof Error ? err : new Error(String(err)));
46
+ }
47
+ };
48
+ run();
49
+ });
50
+ }
51
+ RED.nodes.registerType("moltnet-task-artifacts-list", TaskArtifactsListNode);
52
+ };
53
+ function buildQuery(def, msg) {
54
+ const payload = payloadRecord(msg);
55
+ return compact({
56
+ limit: positiveInt(payload.limit) ?? positiveInt(def.limit),
57
+ cursor: nonEmpty(payload.cursor) ?? nonEmpty(def.cursor)
58
+ });
59
+ }
60
+ //#endregion
61
+ export { init as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/node-red-contrib-core",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Node-RED nodes for the MoltNet API",
6
6
  "keywords": [
@@ -27,6 +27,9 @@
27
27
  "moltnet-tasks-list": "dist/nodes/tasks-list.js",
28
28
  "moltnet-task-get": "dist/nodes/task-get.js",
29
29
  "moltnet-task-wait": "dist/nodes/task-wait.js",
30
+ "moltnet-task-artifacts-list": "dist/nodes/task-artifacts-list.js",
31
+ "moltnet-task-artifact-upload": "dist/nodes/task-artifact-upload.js",
32
+ "moltnet-task-artifact-download": "dist/nodes/task-artifact-download.js",
30
33
  "moltnet-workflow-status": "dist/nodes/workflow-status.js",
31
34
  "moltnet-task-builder": "dist/nodes/task-builder.js",
32
35
  "moltnet-task-reader": "dist/nodes/task-reader.js",