@themoltnet/node-red-contrib-core 0.3.3 → 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
package/dist/nodes/src.js CHANGED
@@ -2271,6 +2271,55 @@ var claimTask = (options) => (options.client ?? client).post({
2271
2271
  }
2272
2272
  });
2273
2273
  /**
2274
+ * List task artifact metadata for the current team.
2275
+ */
2276
+ var listTaskArtifacts = (options) => (options.client ?? client).get({
2277
+ security: [
2278
+ {
2279
+ scheme: "bearer",
2280
+ type: "http"
2281
+ },
2282
+ {
2283
+ name: "X-Moltnet-Session-Token",
2284
+ type: "apiKey"
2285
+ },
2286
+ {
2287
+ in: "cookie",
2288
+ name: "ory_kratos_session",
2289
+ type: "apiKey"
2290
+ }
2291
+ ],
2292
+ url: "/tasks/{taskId}/artifacts",
2293
+ ...options
2294
+ });
2295
+ /**
2296
+ * Upload immutable content-addressed artifact content for a task attempt.
2297
+ */
2298
+ var uploadTaskArtifact = (options) => (options.client ?? client).put({
2299
+ bodySerializer: null,
2300
+ security: [
2301
+ {
2302
+ scheme: "bearer",
2303
+ type: "http"
2304
+ },
2305
+ {
2306
+ name: "X-Moltnet-Session-Token",
2307
+ type: "apiKey"
2308
+ },
2309
+ {
2310
+ in: "cookie",
2311
+ name: "ory_kratos_session",
2312
+ type: "apiKey"
2313
+ }
2314
+ ],
2315
+ url: "/tasks/{taskId}/attempts/{attemptN}/artifacts",
2316
+ ...options,
2317
+ headers: {
2318
+ "Content-Type": "application/octet-stream",
2319
+ ...options.headers
2320
+ }
2321
+ });
2322
+ /**
2274
2323
  * List teams the caller belongs to.
2275
2324
  */
2276
2325
  var listTeams = (options) => (options?.client ?? client).get({
@@ -10036,6 +10085,97 @@ var VerificationRecord = _Object_({
10036
10085
  $id: "VerificationRecord",
10037
10086
  additionalProperties: false
10038
10087
  });
10088
+ _Object_({
10089
+ artifacts: _Array_(_Object_({
10090
+ id: String$1({ format: "uuid" }),
10091
+ teamId: String$1({ format: "uuid" }),
10092
+ taskId: String$1({ format: "uuid" }),
10093
+ attemptN: Integer({ minimum: 1 }),
10094
+ kind: String$1({
10095
+ minLength: 1,
10096
+ maxLength: 100
10097
+ }),
10098
+ title: String$1({
10099
+ minLength: 1,
10100
+ maxLength: 255
10101
+ }),
10102
+ contentType: String$1({
10103
+ minLength: 1,
10104
+ maxLength: 200
10105
+ }),
10106
+ contentEncoding: Union([String$1({
10107
+ minLength: 1,
10108
+ maxLength: 100
10109
+ }), Null()]),
10110
+ sizeBytes: Integer({ minimum: 0 }),
10111
+ cid: String$1({
10112
+ minLength: 1,
10113
+ maxLength: 100
10114
+ }),
10115
+ createdByAgentId: String$1({ format: "uuid" }),
10116
+ expiresAt: Union([String$1({ format: "date-time" }), Null()]),
10117
+ createdAt: String$1({ format: "date-time" })
10118
+ }, { $id: "TaskArtifact" })),
10119
+ nextCursor: Union([String$1({ minLength: 1 }), Null()])
10120
+ }, { $id: "TaskArtifactList" });
10121
+ _Object_({
10122
+ limit: Optional(Integer({
10123
+ minimum: 1,
10124
+ maximum: 100
10125
+ })),
10126
+ cursor: Optional(String$1({ minLength: 1 }))
10127
+ }, {
10128
+ $id: "ListTaskArtifactsQuery",
10129
+ additionalProperties: false
10130
+ });
10131
+ _Object_({
10132
+ kind: String$1({
10133
+ minLength: 1,
10134
+ maxLength: 100
10135
+ }),
10136
+ title: String$1({
10137
+ minLength: 1,
10138
+ maxLength: 255
10139
+ }),
10140
+ contentType: Optional(String$1({
10141
+ minLength: 1,
10142
+ maxLength: 200
10143
+ })),
10144
+ contentEncoding: Optional(String$1({
10145
+ minLength: 1,
10146
+ maxLength: 100
10147
+ }))
10148
+ }, {
10149
+ $id: "UploadTaskArtifactQuery",
10150
+ additionalProperties: false
10151
+ });
10152
+ String$1({
10153
+ $id: "TaskArtifactContent",
10154
+ description: "Task artifact content stream.",
10155
+ format: "binary"
10156
+ });
10157
+ _Object_({ taskId: String$1({ format: "uuid" }) }, {
10158
+ $id: "TaskArtifactTaskParams",
10159
+ additionalProperties: false
10160
+ });
10161
+ _Object_({
10162
+ taskId: String$1({ format: "uuid" }),
10163
+ attemptN: Integer({ minimum: 1 })
10164
+ }, {
10165
+ $id: "TaskArtifactAttemptParams",
10166
+ additionalProperties: false
10167
+ });
10168
+ _Object_({
10169
+ taskId: String$1({ format: "uuid" }),
10170
+ attemptN: Integer({ minimum: 1 }),
10171
+ cid: String$1({
10172
+ minLength: 1,
10173
+ maxLength: 100
10174
+ })
10175
+ }, {
10176
+ $id: "TaskArtifactContentParams",
10177
+ additionalProperties: false
10178
+ });
10039
10179
  new TextEncoder();
10040
10180
  new TextDecoder();
10041
10181
  //#endregion
@@ -10295,6 +10435,10 @@ var FreeformArtifact = _Object_({
10295
10435
  description: Optional(String$1({ minLength: 1 })),
10296
10436
  url: Optional(String$1({ minLength: 1 })),
10297
10437
  path: Optional(String$1({ minLength: 1 })),
10438
+ cid: Optional(String$1({ minLength: 1 })),
10439
+ contentType: Optional(String$1({ minLength: 1 })),
10440
+ contentEncoding: Optional(String$1({ minLength: 1 })),
10441
+ sizeBytes: Optional(Integer({ minimum: 0 })),
10298
10442
  body: Optional(String$1({ maxLength: 65536 }))
10299
10443
  }, {
10300
10444
  $id: "FreeformArtifact",
@@ -13765,7 +13909,23 @@ var TaskRef = _Object_({
13765
13909
  url: Optional(String$1()),
13766
13910
  commit_sha: Optional(String$1()),
13767
13911
  snapshot_cid: Optional(Cid)
13768
- }))
13912
+ })),
13913
+ artifact: Optional(_Object_({
13914
+ cid: Cid,
13915
+ attemptN: Integer({ minimum: 1 }),
13916
+ kind: Optional(String$1({
13917
+ minLength: 1,
13918
+ maxLength: 100
13919
+ })),
13920
+ title: Optional(String$1({
13921
+ minLength: 1,
13922
+ maxLength: 255
13923
+ })),
13924
+ contentType: Optional(String$1({
13925
+ minLength: 1,
13926
+ maxLength: 200
13927
+ }))
13928
+ }, { additionalProperties: false }))
13769
13929
  }, {
13770
13930
  $id: "TaskRef",
13771
13931
  additionalProperties: false
@@ -14110,6 +14270,7 @@ var TaskBuilder = class {
14110
14270
  message: "reference is missing required outputCid"
14111
14271
  }]);
14112
14272
  ref = {
14273
+ ...s,
14113
14274
  taskId: s.taskId ?? null,
14114
14275
  outputCid: s.outputCid,
14115
14276
  role
@@ -14119,6 +14280,60 @@ var TaskBuilder = class {
14119
14280
  return this;
14120
14281
  }
14121
14282
  /**
14283
+ * Add a reference to a persistent task artifact while retaining the accepted
14284
+ * output CID as the provenance anchor.
14285
+ *
14286
+ * @param source - A result reader, raw artifact reference, or `TaskRef`.
14287
+ * @param role - The role the referenced artifact plays.
14288
+ * @returns This builder, for chaining.
14289
+ * @throws {TaskBuildError} when output or artifact CID is missing.
14290
+ */
14291
+ artifactReference(source, role) {
14292
+ let ref;
14293
+ if ("artifactRef" in source && typeof source.artifactRef === "function") ref = source.artifactRef(role);
14294
+ else if ("artifact" in source && source.artifact?.cid) {
14295
+ if (typeof source.artifact.attemptN !== "number" || !Number.isInteger(source.artifact.attemptN) || source.artifact.attemptN < 1) throw new TaskBuildError([{
14296
+ field: "references/artifact/attemptN",
14297
+ message: "artifact reference is missing required attemptN"
14298
+ }]);
14299
+ ref = {
14300
+ ...source,
14301
+ role
14302
+ };
14303
+ } else {
14304
+ const s = source;
14305
+ const errors = [];
14306
+ if (!s.outputCid) errors.push({
14307
+ field: "references/outputCid",
14308
+ message: "reference is missing required outputCid"
14309
+ });
14310
+ if (!s.artifactCid) errors.push({
14311
+ field: "references/artifact/cid",
14312
+ message: "artifact reference is missing required cid"
14313
+ });
14314
+ if (typeof s.attemptN !== "number" || !Number.isInteger(s.attemptN) || s.attemptN < 1) errors.push({
14315
+ field: "references/artifact/attemptN",
14316
+ message: "artifact reference is missing required attemptN"
14317
+ });
14318
+ if (errors.length > 0) throw new TaskBuildError(errors);
14319
+ const attemptN = s.attemptN;
14320
+ ref = {
14321
+ taskId: s.taskId ?? null,
14322
+ outputCid: s.outputCid,
14323
+ role,
14324
+ artifact: {
14325
+ cid: s.artifactCid,
14326
+ attemptN,
14327
+ ...s.kind ? { kind: s.kind } : {},
14328
+ ...s.title ? { title: s.title } : {},
14329
+ ...s.contentType ? { contentType: s.contentType } : {}
14330
+ }
14331
+ };
14332
+ }
14333
+ this.refs.push(ref);
14334
+ return this;
14335
+ }
14336
+ /**
14122
14337
  * Set the owning team (required by the wire schema).
14123
14338
  *
14124
14339
  * @param teamId - Team UUID.
@@ -14496,6 +14711,34 @@ var TaskResultReader = class {
14496
14711
  role
14497
14712
  };
14498
14713
  }
14714
+ /**
14715
+ * Build a `TaskRef` that anchors a downstream task to this accepted output
14716
+ * and points at one persistent task artifact by CID.
14717
+ *
14718
+ * @param filter - Artifact object or a filter resolved against output artifacts.
14719
+ * @param role - The role this artifact plays in the downstream task.
14720
+ * @returns A `TaskRef` with `artifact.cid` populated.
14721
+ * @throws {TaskResultError} if no matching artifact has a CID.
14722
+ */
14723
+ artifactRef(filter, role) {
14724
+ const artifact = typeof filter === "object" && "cid" in filter && "kind" in filter ? filter : this.artifact(filter);
14725
+ if (!artifact?.cid) throw new TaskResultError([{
14726
+ field: "artifacts/cid",
14727
+ message: "no matching artifact with a cid"
14728
+ }]);
14729
+ return {
14730
+ taskId: this.taskId,
14731
+ outputCid: this.outputCid,
14732
+ role,
14733
+ artifact: {
14734
+ cid: artifact.cid,
14735
+ attemptN: this.accepted.attemptN,
14736
+ kind: artifact.kind,
14737
+ title: artifact.title,
14738
+ ...artifact.contentType ? { contentType: artifact.contentType } : {}
14739
+ }
14740
+ };
14741
+ }
14499
14742
  };
14500
14743
  /**
14501
14744
  * Validate and construct a {@link TaskResultReader} from a task and its
@@ -14520,6 +14763,63 @@ function createTasksNamespace(context) {
14520
14763
  auth
14521
14764
  }));
14522
14765
  },
14766
+ artifacts: {
14767
+ async upload(path, body, query, options) {
14768
+ return unwrapResult(await uploadTaskArtifact({
14769
+ auth,
14770
+ body,
14771
+ client,
14772
+ duplex: "half",
14773
+ headers: {
14774
+ ...requiredTeamHeaders(options),
14775
+ "content-type": "application/octet-stream"
14776
+ },
14777
+ path,
14778
+ query
14779
+ }));
14780
+ },
14781
+ async list(taskId, options, query) {
14782
+ return unwrapResult(await listTaskArtifacts({
14783
+ client,
14784
+ auth,
14785
+ headers: requiredTeamHeaders(options),
14786
+ path: { taskId },
14787
+ query
14788
+ })).artifacts;
14789
+ },
14790
+ async listPage(taskId, query, options) {
14791
+ return unwrapResult(await listTaskArtifacts({
14792
+ client,
14793
+ auth,
14794
+ headers: requiredTeamHeaders(options),
14795
+ path: { taskId },
14796
+ query
14797
+ }));
14798
+ },
14799
+ async download(path, options) {
14800
+ const result = await client.request({
14801
+ auth,
14802
+ headers: requiredTeamHeaders(options),
14803
+ method: "GET",
14804
+ parseAs: "stream",
14805
+ path,
14806
+ security: [{
14807
+ scheme: "bearer",
14808
+ type: "http"
14809
+ }],
14810
+ url: "/tasks/{taskId}/attempts/{attemptN}/artifacts/{cid}/content"
14811
+ });
14812
+ const normalizedStream = normalizeDownloadStream(unwrapResult(result));
14813
+ if (normalizedStream) return {
14814
+ artifactId: header(result.response, "x-moltnet-task-artifact-id"),
14815
+ cid: header(result.response, "x-moltnet-task-artifact-cid"),
14816
+ contentEncoding: header(result.response, "x-moltnet-task-artifact-content-encoding"),
14817
+ contentType: header(result.response, "x-moltnet-task-artifact-content-type"),
14818
+ stream: normalizedStream
14819
+ };
14820
+ throw new MoltNetError("Unexpected task artifact download response stream", { code: "INVALID_RESPONSE" });
14821
+ }
14822
+ },
14523
14823
  async list(query, options) {
14524
14824
  return unwrapResult(await listTasks({
14525
14825
  client,
@@ -14688,6 +14988,33 @@ function createTasksNamespace(context) {
14688
14988
  }
14689
14989
  };
14690
14990
  }
14991
+ function header(response, name) {
14992
+ const value = response?.headers.get(name) ?? null;
14993
+ return value === "" ? null : value;
14994
+ }
14995
+ function normalizeDownloadStream(stream) {
14996
+ if (isAsyncIterable(stream)) return stream;
14997
+ if (isReadableStream(stream)) return readableStreamToAsyncIterable(stream);
14998
+ return null;
14999
+ }
15000
+ function isAsyncIterable(value) {
15001
+ return typeof value === "object" && value !== null && Symbol.asyncIterator in value && typeof value[Symbol.asyncIterator] === "function";
15002
+ }
15003
+ function isReadableStream(value) {
15004
+ return typeof value === "object" && value !== null && "getReader" in value && typeof value.getReader === "function";
15005
+ }
15006
+ async function* readableStreamToAsyncIterable(stream) {
15007
+ const reader = stream.getReader();
15008
+ try {
15009
+ while (true) {
15010
+ const result = await reader.read();
15011
+ if (result.done) return;
15012
+ yield result.value;
15013
+ }
15014
+ } finally {
15015
+ reader.releaseLock();
15016
+ }
15017
+ }
14691
15018
  //#endregion
14692
15019
  //#region ../sdk/src/namespaces/teams.ts
14693
15020
  function createTeamsNamespace(context) {
@@ -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 };
@@ -19,16 +19,13 @@ var init = (RED) => {
19
19
  } : def.artifactKind : void 0;
20
20
  let artifactBody;
21
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
- }
22
+ if (filter && artifact && typeof artifact.body === "string") artifactBody = reader.artifactBody(filter);
27
23
  const out = RED.util.cloneMessage(msg);
28
24
  out.payload = reader.output;
29
25
  out.result = {
30
26
  summary: reader.summary,
31
27
  outputRef: reader.outputRef(role),
28
+ artifactRef: artifact?.cid !== void 0 ? reader.artifactRef(artifact, role) : void 0,
32
29
  artifact,
33
30
  artifactBody,
34
31
  accepted: reader.accepted,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/node-red-contrib-core",
3
- "version": "0.3.3",
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",
@@ -38,7 +41,7 @@
38
41
  },
39
42
  "main": "dist/nodes/agent.js",
40
43
  "dependencies": {
41
- "@themoltnet/sdk": "0.114.0"
44
+ "@themoltnet/sdk": "0.115.0"
42
45
  },
43
46
  "devDependencies": {
44
47
  "@types/node": "^22.19.0",