@themoltnet/node-red-contrib-core 0.4.0 → 0.7.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
@@ -198,6 +220,74 @@ Fill the `moltnet-agent` config after import. Runtime-profile config nodes are
198
220
  included but blank: leave them blank for any eligible daemon to claim both
199
221
  tasks, or set producer/judge profile IDs and run one daemon per profile.
200
222
 
223
+ ## Freeform deep review workflow
224
+
225
+ [`examples/deep-review-freeform.flow.json`](./examples/deep-review-freeform.flow.json)
226
+ is a freeform-only code review workflow inspired by the `deep-review` agent
227
+ skill. It keeps MoltNet generic: the workflow is opinionated, but every agent
228
+ step is a `freeform` task.
229
+
230
+ The flow shape is:
231
+
232
+ `inject` -> `entries: search` -> **FREEZE** -> **PREFLIGHT** -> stock `switch`
233
+ on `PROCEED | PIVOT | ASK`. `PIVOT` becomes a design-review publish task.
234
+ `PROCEED` fans out one specialist task per review dimension, so multiple
235
+ daemons serving the specialist profile can claim work in parallel. Each
236
+ completed task reads its `specialist-findings` artifact, appends it to the
237
+ correlation-scoped `flow` state at `deepReview:<correlationId>`, and a small
238
+ completion gate runs **AGGREGATE** exactly once when all expected specialist
239
+ results have arrived. Tail events from wait nodes use link nodes into one
240
+ observability/debug lane so the main review path stays readable.
241
+
242
+ The **FREEZE** task asks the agent to create a review bundle artifact containing
243
+ target metadata, changed files, stats, and patch/file-list CIDs. The flow
244
+ keeps control flow deterministic by parsing the compact inline JSON
245
+ (`artifact kind=json`, `title=review-bundle`) with `task: read`, then uploading
246
+ that parsed bundle through `task artifact: upload` before using
247
+ `task artifacts: list` and `task artifact: download` to inspect the persistent
248
+ CID-backed artifact path.
249
+
250
+ The example also includes runtime-profile config nodes, so each stage can be
251
+ routed to a daemon serving the right Ollama Cloud model. These profiles were
252
+ created through the Runtime Profiles API for team
253
+ `6743b4b1-6b93-46e2-a048-19490f04f91a`; recreate them or replace the IDs when
254
+ running the flow in another team.
255
+
256
+ | Stage | Profile ID | Provider / model | Settings |
257
+ | ---------- | -------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
258
+ | FREEZE | `1a653eb9-7bfa-475f-b517-c070c9c25b5e` | `ollama-cloud/qwen3-coder:480b-cloud` | `thinkingLevel=high`, `temperature=0.1`, `maxOutputTokens=12000`, `maxTurns=60`, `maxBashTimeouts=5`, `defaultWorkspaceMode=dedicated_worktree`, `allowedWorkspaceModes=[dedicated_worktree]`, requires `git`, `gh`, `rg` |
259
+ | PREFLIGHT | `f4bb1d9b-6281-4158-ad88-cbcb1198c3dc` | `ollama-cloud/qwen3-coder:480b-cloud` | `thinkingLevel=high`, `temperature=0.1`, `maxOutputTokens=10000`, `maxTurns=16`, `maxBashTimeouts=2`, `defaultWorkspaceMode=shared_mount`, `allowedWorkspaceModes=[shared_mount]`, requires `git`, `rg` |
260
+ | SPECIALIST | `f50e9c58-4180-4e07-b120-08b6097c13d5` | `ollama-cloud/qwen3-coder:480b-cloud` | `thinkingLevel=high`, `temperature=0.15`, `maxOutputTokens=18000`, `maxTurns=24`, `maxBashTimeouts=3`, `defaultWorkspaceMode=shared_mount`, `allowedWorkspaceModes=[shared_mount]`, requires `git`, `rg` |
261
+ | AGGREGATE | `29db793d-3ad9-420b-96e7-df5356b3d19b` | `ollama-cloud/kimi-k2.7-code:cloud` | `thinkingLevel=high`, `temperature=0.2`, `maxOutputTokens=24000`, `maxTurns=16`, `maxBashTimeouts=2`, `defaultWorkspaceMode=none`, `allowedWorkspaceModes=[none]` |
262
+
263
+ All four profiles set `sandbox.env.NODE_OPTIONS=--dns-result-order=ipv4first`
264
+ to match the live Ollama daemon smoke pattern and require `OLLAMA_API_KEY`.
265
+ Repo-aware profiles deny `.env`, `.env.local`, and `.moltnet` through VFS
266
+ shadowing with `shadowMode=deny` and `hostExec.autoApprove=false`. FREEZE uses a
267
+ dedicated worktree and GitHub snapshot hosts `api.github.com`, `github.com`,
268
+ `objects.githubusercontent.com`, `codeload.github.com`, and
269
+ `raw.githubusercontent.com`; PREFLIGHT and SPECIALIST use `shared_mount` so they
270
+ can inspect code around the frozen bundle without mutating it. AGGREGATE stays
271
+ repo-free with workspace `none`.
272
+
273
+ Run one daemon per profile, or one daemon process configured with all four
274
+ profiles. Repeated `--profile` flags declare the daemon's priority order:
275
+
276
+ ```bash
277
+ export OLLAMA_API_KEY=...
278
+
279
+ npx @themoltnet/agent-daemon@latest poll \
280
+ --agent <agent-name> \
281
+ --team 6743b4b1-6b93-46e2-a048-19490f04f91a \
282
+ --profile deep-review-freeze-ollama-qwen-coder-v1 \
283
+ --profile deep-review-preflight-ollama-qwen-coder-v1 \
284
+ --profile deep-review-specialist-ollama-qwen-coder-v1 \
285
+ --profile deep-review-aggregate-ollama-kimi-v1
286
+ ```
287
+
288
+ The daemon host must have matching Pi model registry entries. This repo's
289
+ `.pi/models.json` already declares the Ollama Cloud provider and these models.
290
+
201
291
  ## Reproducing the issue-lifecycle shape
202
292
 
203
293
  [`examples/issue-lifecycle.flow.json`](./examples/issue-lifecycle.flow.json)
@@ -254,20 +344,41 @@ Three layers, increasing fidelity:
254
344
  ## Run Node-RED with these nodes (one command)
255
345
 
256
346
  ```bash
257
- pnpm --filter @themoltnet/node-red-contrib-core dev # → http://localhost:1880
258
- PORT=1881 pnpm --filter @themoltnet/node-red-contrib-core dev
347
+ pnpm exec nx run @themoltnet/node-red-contrib-core:dev # → http://localhost:1880
348
+ PORT=1881 pnpm exec nx run @themoltnet/node-red-contrib-core:dev
259
349
  ```
260
350
 
261
- `scripts/dev.mjs` builds the nodes, links this package into a local
262
- `.node-red-dev/` userDir (gitignored), and starts Node-RED 5 (fetched via `npx`
351
+ The Nx `dev` target builds this package and `@themoltnet/node-red-theme` through
352
+ target `dependsOn`, then `scripts/dev.mjs` links this package into a local
353
+ `.node-red-dev/` userDir (gitignored) and starts Node-RED 5 (fetched via `npx`
263
354
  on first run). The MoltNet nodes appear under the **moltnet** palette category.
264
- After editing a node, stop (Ctrl-C) and re-run — Node-RED does not hot-reload
265
- custom nodes.
355
+ After editing a node or the theme, stop (Ctrl-C) and re-run — Node-RED does not
356
+ hot-reload custom nodes.
357
+
358
+ To skin a local or hosted Node-RED instance with the MoltNet editor theme,
359
+ install `@themoltnet/node-red-theme` next to Node-RED and set:
360
+
361
+ ```js
362
+ import { moltnetEditorTheme } from '@themoltnet/node-red-theme';
363
+
364
+ export default {
365
+ editorTheme: moltnetEditorTheme({ title: 'MoltNet Flow Studio' }),
366
+ };
367
+ ```
266
368
 
267
369
  Open the editor, drag in `agent` + the task nodes, or import
268
370
  [`examples/issue-lifecycle.flow.json`](./examples/issue-lifecycle.flow.json) or
269
- [`examples/cockpit.flow.json`](./examples/cockpit.flow.json), then fill the
270
- agent's `clientId`/`clientSecret`.
371
+ [`examples/cockpit.flow.json`](./examples/cockpit.flow.json) or
372
+ [`examples/deep-review-freeform.flow.json`](./examples/deep-review-freeform.flow.json),
373
+ then fill the agent's `clientId`/`clientSecret`.
374
+
375
+ If Node-RED crashes in `@node-red/editor-api/lib/auth/tokens.js` with
376
+ `Cannot read properties of undefined (reading 'getSessions')`, the browser is
377
+ usually sending a stale `auth-tokens*` localStorage value from an older
378
+ authenticated editor on the same origin. Clear site data for
379
+ `http://localhost:1880`, remove localStorage keys beginning with `auth-tokens`,
380
+ or restart this dev harness on a fresh port such as
381
+ `PORT=1881 pnpm exec nx run @themoltnet/node-red-contrib-core:dev`.
271
382
 
272
383
  <details>
273
384
  <summary>Manual harness (if you prefer to drive it yourself)</summary>
@@ -0,0 +1,19 @@
1
+ //#region src/nodes/agent-call.ts
2
+ async function withAgent(agentNode, operation) {
3
+ try {
4
+ return await operation(await agentNode.getAgent());
5
+ } catch (err) {
6
+ if (!isUnauthorizedError(err)) throw err;
7
+ agentNode.resetAgent();
8
+ return operation(await agentNode.getAgent());
9
+ }
10
+ }
11
+ function isUnauthorizedError(err) {
12
+ if (!err || typeof err !== "object") return false;
13
+ const record = err;
14
+ if (record.statusCode === 401 || record.status === 401) return true;
15
+ if (record.code === "https://themolt.net/problems/unauthorized") return true;
16
+ return typeof record.message === "string" && /\bunauthorized\b/i.test(record.message) && /\btoken\b/i.test(record.message);
17
+ }
18
+ //#endregion
19
+ export { withAgent as t };
@@ -8,6 +8,9 @@ var init = (RED) => {
8
8
  this.teamId = def.teamId?.trim() || void 0;
9
9
  this.diaryId = def.diaryId?.trim() || void 0;
10
10
  let agentPromise = null;
11
+ this.resetAgent = function resetAgent() {
12
+ agentPromise = null;
13
+ };
11
14
  this.getAgent = function getAgent() {
12
15
  const clientSecret = this.credentials?.clientSecret;
13
16
  if (!this.clientId || !clientSecret) return Promise.reject(/* @__PURE__ */ new Error("moltnet-agent: clientId and clientSecret are required"));
@@ -1,3 +1,4 @@
1
+ import { t as withAgent } from "./agent-call.js";
1
2
  import { a as nonEmpty, c as positiveInt, i as finiteNumber, n as compact, o as nonNegativeInt, r as csv, s as normalizeAliases, t as bool } from "./query-utils.js";
2
3
  //#region src/nodes/entries-search.ts
3
4
  var init = (RED) => {
@@ -15,7 +16,7 @@ var init = (RED) => {
15
16
  shape: "dot",
16
17
  text: "searching…"
17
18
  });
18
- const result = await (await agentNode.getAgent()).entries.search(body);
19
+ const result = await withAgent(agentNode, (agent) => agent.entries.search(body));
19
20
  const out = RED.util.cloneMessage(msg);
20
21
  out.payload = result.results;
21
22
  out.entries = {
@@ -1,3 +1,4 @@
1
+ import { t as withAgent } from "./agent-call.js";
1
2
  //#region src/nodes/runtime-profile.ts
2
3
  var init = (RED) => {
3
4
  function MoltnetRuntimeProfileNode(def) {
@@ -19,7 +20,7 @@ var init = (RED) => {
19
20
  });
20
21
  return;
21
22
  }
22
- const { items } = await (await agentNode.getAgent()).runtimeProfiles.list(agentNode.teamId ? { teamId: agentNode.teamId } : void 0);
23
+ const { items } = await withAgent(agentNode, (agent) => agent.runtimeProfiles.list(agentNode.teamId ? { teamId: agentNode.teamId } : void 0));
23
24
  res.json({ profiles: items.map((p) => ({
24
25
  id: p.id,
25
26
  name: p.name,
package/dist/nodes/src.js CHANGED
@@ -2836,6 +2836,15 @@ function unwrapResult(result) {
2836
2836
  networkError.stack = error.stack;
2837
2837
  throw networkError;
2838
2838
  }
2839
+ const responseSummary = summarizeResponse(result.response);
2840
+ if (responseSummary) {
2841
+ const detail = stringifyUnknown(error);
2842
+ throw new MoltNetError(`MoltNet API request failed with HTTP ${responseSummary.status} ${responseSummary.statusText}: ${detail}`, {
2843
+ code: `HTTP_${responseSummary.status}`,
2844
+ detail,
2845
+ statusCode: responseSummary.status
2846
+ });
2847
+ }
2839
2848
  throw new MoltNetError(`Unexpected error from MoltNet API: ${stringifyUnknown(error)}`, { code: "UNKNOWN" });
2840
2849
  }
2841
2850
  if (result.data === void 0) throw new MoltNetError("Unexpected empty response from MoltNet API", { code: "EMPTY_RESPONSE" });
@@ -2853,6 +2862,15 @@ function stringifyUnknown(value) {
2853
2862
  return String(value);
2854
2863
  }
2855
2864
  }
2865
+ function summarizeResponse(response) {
2866
+ if (!response || typeof response !== "object") return null;
2867
+ const candidate = response;
2868
+ if (typeof candidate.status !== "number") return null;
2869
+ return {
2870
+ status: candidate.status,
2871
+ statusText: typeof candidate.statusText === "string" && candidate.statusText ? candidate.statusText : "Error"
2872
+ };
2873
+ }
2856
2874
  function unwrapRequired(result, message, code) {
2857
2875
  if (result.error || !result.data) throw new MoltNetError(message, { code });
2858
2876
  return result.data;
@@ -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,76 @@
1
+ import { t as withAgent } from "./agent-call.js";
2
+ import { a as nonEmpty, t as bool } from "./query-utils.js";
3
+ import { a as requireAttemptContext, n as payloadRecord, r as recordField, s as resolveMaxBytes, t as collectArtifactBody } from "./task-artifact-utils.js";
4
+ //#region src/nodes/task-artifact-download.ts
5
+ var init = (RED) => {
6
+ function TaskArtifactDownloadNode(def) {
7
+ RED.nodes.createNode(this, def);
8
+ const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
9
+ this.on("input", (msg, send, done) => {
10
+ const run = async () => {
11
+ try {
12
+ if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-artifact-download: no moltnet-agent configured");
13
+ const { taskId, teamId, attemptN } = requireAttemptContext("task-artifact-download", msg, def.taskId, def.teamId, def.attemptN, agentNode, bool(def.allowMsgTeamOverride) ?? false);
14
+ const cid = resolveCid(msg, def.cid);
15
+ if (!cid) throw new Error("task-artifact-download: cid is required");
16
+ this.status({
17
+ fill: "blue",
18
+ shape: "dot",
19
+ text: "downloading…"
20
+ });
21
+ const result = await withAgent(agentNode, (agent) => agent.tasks.artifacts.download({
22
+ taskId,
23
+ attemptN,
24
+ cid
25
+ }, { teamId }));
26
+ const body = await collectArtifactBody(result, resolveMaxBytes(def.maxBytes));
27
+ const out = RED.util.cloneMessage({
28
+ ...msg,
29
+ payload: void 0
30
+ });
31
+ out.payload = body;
32
+ out.taskId = taskId;
33
+ out.artifact = {
34
+ taskId,
35
+ teamId,
36
+ attemptN,
37
+ cid,
38
+ ...metadataFromResult(result)
39
+ };
40
+ this.status({
41
+ fill: "green",
42
+ shape: "dot",
43
+ text: `${body.byteLength} byte(s)`
44
+ });
45
+ send(out);
46
+ done();
47
+ } catch (err) {
48
+ this.status({
49
+ fill: "red",
50
+ shape: "ring",
51
+ text: "error"
52
+ });
53
+ done(err instanceof Error ? err : new Error(String(err)));
54
+ }
55
+ };
56
+ run();
57
+ });
58
+ }
59
+ RED.nodes.registerType("moltnet-task-artifact-download", TaskArtifactDownloadNode);
60
+ };
61
+ function resolveCid(msg, configured) {
62
+ if (typeof msg.cid === "string" && msg.cid) return msg.cid;
63
+ const payload = payloadRecord(msg);
64
+ return nonEmpty(payload.cid) ?? nonEmpty(recordField(payload.artifact, "cid")) ?? nonEmpty(configured);
65
+ }
66
+ function metadataFromResult(result) {
67
+ if (!result || typeof result !== "object") return {};
68
+ const record = result;
69
+ return {
70
+ artifactId: record.artifactId,
71
+ contentType: record.contentType,
72
+ contentEncoding: record.contentEncoding
73
+ };
74
+ }
75
+ //#endregion
76
+ 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,62 @@
1
+ import { t as withAgent } from "./agent-call.js";
2
+ import { t as bool } from "./query-utils.js";
3
+ import { a as requireAttemptContext, c as resolveUploadBody, o as resolveField, s as resolveMaxBytes } from "./task-artifact-utils.js";
4
+ //#region src/nodes/task-artifact-upload.ts
5
+ var init = (RED) => {
6
+ function TaskArtifactUploadNode(def) {
7
+ RED.nodes.createNode(this, def);
8
+ const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
9
+ this.on("input", (msg, send, done) => {
10
+ const run = async () => {
11
+ try {
12
+ if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-artifact-upload: no moltnet-agent configured");
13
+ const { taskId, teamId, attemptN } = requireAttemptContext("task-artifact-upload", msg, def.taskId, def.teamId, def.attemptN, agentNode, bool(def.allowMsgTeamOverride) ?? false);
14
+ const body = resolveUploadBody(msg, resolveMaxBytes(def.maxBytes));
15
+ const query = buildUploadQuery(def, msg);
16
+ this.status({
17
+ fill: "blue",
18
+ shape: "dot",
19
+ text: "uploading…"
20
+ });
21
+ const artifact = await withAgent(agentNode, (agent) => agent.tasks.artifacts.upload({
22
+ taskId,
23
+ attemptN
24
+ }, body, query, { teamId }));
25
+ const out = RED.util.cloneMessage({
26
+ ...msg,
27
+ payload: void 0
28
+ });
29
+ out.payload = artifact;
30
+ out.taskId = taskId;
31
+ out.artifact = artifact;
32
+ this.status({
33
+ fill: "green",
34
+ shape: "dot",
35
+ text: artifact.cid
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-artifact-upload", TaskArtifactUploadNode);
52
+ };
53
+ function buildUploadQuery(def, msg) {
54
+ return {
55
+ kind: resolveField(msg, "kind", def.kind) ?? "output",
56
+ title: resolveField(msg, "title", def.title) ?? "artifact",
57
+ contentType: resolveField(msg, "contentType", def.contentType) ?? "application/octet-stream",
58
+ contentEncoding: resolveField(msg, "contentEncoding", def.contentEncoding)
59
+ };
60
+ }
61
+ //#endregion
62
+ export { init as default };