@nodetool-ai/code-nodes 0.8.1

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 ADDED
@@ -0,0 +1,25 @@
1
+ # @nodetool-ai/code-nodes
2
+
3
+ Code-execution nodes for [NodeTool](https://nodetool.ai).
4
+
5
+ Run JavaScript inside visual AI workflows in a QuickJS WASM sandbox.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @nodetool-ai/code-nodes
11
+ ```
12
+
13
+ ## Nodes
14
+
15
+ **Code execution** — `nodetool.code.Code` runs JavaScript in a QuickJS WASM
16
+ sandbox with `fetch()`, workspace file access, secrets, and media helpers.
17
+ Dynamic inputs arrive on the `inputs` object; the returned object becomes the
18
+ outputs. Libraries are sandbox packs the body imports — the node declares no
19
+ packages; the host resolves each specifier against the installed catalog. Full
20
+ reference: [JavaScript Sandbox](https://docs.nodetool.ai/javascript-sandbox).
21
+
22
+ ## Links
23
+
24
+ - [NodeTool](https://nodetool.ai)
25
+ - [GitHub](https://github.com/nodetool-ai/nodetool)
@@ -0,0 +1 @@
1
+ 2026-09-28T09:19:16.401Z
@@ -0,0 +1,2 @@
1
+ export * from "./nodes/code-node.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./nodes/code-node.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC"}
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Universal Code Node — sandboxed JavaScript execution via QuickJS WASM.
3
+ *
4
+ * Runs user code in an isolated QuickJS WebAssembly guest (see
5
+ * `@nodetool-ai/agents/js-sandbox`) with standard JavaScript plus the bridge
6
+ * APIs: fetch(), workspace (text/bytes/stat/mkdir/remove/copy/move/root),
7
+ * getSecret(), sleep(), progress(), crypto (randomUUID/digest/hmac), format,
8
+ * media (read a media input's bytes, build a media output) and the base64/hex
9
+ * helpers. Dynamic inputs arrive on the `inputs` object.
10
+ *
11
+ * On a server host the code also gets the `nodetool` object model — the
12
+ * platform as objects (`nodetool.workflows`, `nodetool.assets`, …), backed by
13
+ * the agent toolbelt through imported capability modules. The belt is loaded
14
+ * lazily and only on Node: the in-browser runner bundles this module, so the
15
+ * toolbelt (native canvas, IMAP, execution) must never sit in its static
16
+ * import graph. Without a belt, `nodetool.capabilities()` reports `{}` and
17
+ * every method throws naming its missing tool instead of a ReferenceError.
18
+ *
19
+ * Outputs leave the body through two host calls: `await emit(name, value)`
20
+ * streams one value downstream immediately, `await output(name, value)` sets a
21
+ * handle's final value, delivered as one bag when the body completes. The
22
+ * body's return value carries no outputs.
23
+ *
24
+ * Example:
25
+ * // inputs: { x: 5, text: "hello" }
26
+ * // code:
27
+ * for (const word of inputs.text.split(" ")) await emit("word", word);
28
+ * await output("sum", inputs.x + 10);
29
+ * // streams { word: … } per word, then the final bag { sum: 15 }
30
+ *
31
+ * A body that calls neither runs the deprecated return/yield contract.
32
+ *
33
+ * Inputs arrive one of two ways. A body that never mentions `stream` runs once
34
+ * per incoming item with `inputs` holding that item's snapshot. A body that
35
+ * calls `stream` runs once for the whole stream and pulls values itself
36
+ * (`stream(name)`, `stream.any()`, `stream.first`, `stream.open`), executing
37
+ * through {@link CodeNode.run} — the kernel's streaming-input mode, which
38
+ * hydration selects per node from the body itself.
39
+ */
40
+ import { BaseNode, type JsScriptLink } from "@nodetool-ai/node-sdk";
41
+ import type { ProcessingContext, StreamingInputs, StreamingOutputs } from "@nodetool-ai/runtime";
42
+ import { type RunSandboxResult } from "@nodetool-ai/agents/js-sandbox";
43
+ /** This node's output bag is the sandbox's: handle name → the guest's value. */
44
+ type CodeOutputs = NonNullable<RunSandboxResult["outputs"]>;
45
+ /** Type-only view of the agents package index; erased at compile time. */
46
+ type AgentsModule = typeof import("@nodetool-ai/agents");
47
+ type AgentTool = InstanceType<AgentsModule["Tool"]>;
48
+ /**
49
+ * Hand the Code node the agents package a host already holds.
50
+ *
51
+ * The bare specifier below resolves in a workspace checkout and in the CLI,
52
+ * and resolves nowhere in the bundled backend: `bundle-backend.mjs` inlines
53
+ * every workspace package into `server.mjs` and stages only third-party
54
+ * externals, so `node_modules/@nodetool-ai/agents` does not exist in the
55
+ * packaged desktop app or in the Docker/Fly image. Without this, a Code node
56
+ * body in production runs with no toolbelt (`nodetool.capabilities()` is `{}`,
57
+ * `yt_dlp` is missing) and cannot import
58
+ * `@nodetool-ai/sandbox-nodetool/*` at all.
59
+ *
60
+ * The server calls this at bootstrap with the copy esbuild already inlined,
61
+ * which also keeps the run on one module instance — a staged second copy would
62
+ * carry its own capability registry.
63
+ */
64
+ export declare function setCodeNodeAgentsModule(mod: AgentsModule | null): void;
65
+ /**
66
+ * Test/host injection point: replace the assembled toolbelt with a fixed set
67
+ * (`[]` simulates a beltless host). Pass `null` to restore the default
68
+ * assembly.
69
+ */
70
+ export declare function setCodeNodeTools(tools: AgentTool[] | null): void;
71
+ /**
72
+ * Bags a node invocation may hold before `emit` blocks the guest. Exported so
73
+ * a test can drive the producer past the bound without guessing the number.
74
+ */
75
+ export declare const EMIT_CHANNEL_CAPACITY = 64;
76
+ export declare class CodeNode extends BaseNode {
77
+ static readonly nodeType = "nodetool.code.Code";
78
+ static readonly platforms: readonly import("@nodetool-ai/protocol").Platform[];
79
+ static readonly title = "Code";
80
+ static readonly description: string;
81
+ static readonly inlineFields: string[];
82
+ static readonly inputFields: never[];
83
+ static readonly supportsDynamicInputs = true;
84
+ static readonly supportsDynamicOutputs = true;
85
+ /**
86
+ * Whether this node streams its inputs is a property of its body, not of the
87
+ * type: a body that calls `stream` drains its own inbox in {@link run},
88
+ * every other body keeps the buffered per-item invocation. Both hydration
89
+ * paths re-read the body through this hook, so editing the last `stream`
90
+ * call out flips the mode back with no saved flag involved.
91
+ */
92
+ static readonly resolveStreamingInput: NonNullable<typeof BaseNode.resolveStreamingInput>;
93
+ /**
94
+ * Persistent state across streaming invocations; reset each workflow run.
95
+ * The guest owns the contents and the sandbox syncs them back marshalled, so
96
+ * what survives a call is what survives a JSON round trip.
97
+ */
98
+ private _state;
99
+ code: string;
100
+ secrets: string[];
101
+ script: JsScriptLink | Record<string, never>;
102
+ timeout: number;
103
+ max_response_mb: number;
104
+ allow_local_network: boolean;
105
+ allow_host_filesystem: boolean;
106
+ initialize(): Promise<void>;
107
+ /**
108
+ * Forward guest `progress(percent, message)` calls to the kernel as
109
+ * `node_progress` messages — the same channel the Python worker and the
110
+ * ComfyUI node use, so the editor's node progress bar picks them up.
111
+ */
112
+ private progressSink;
113
+ /**
114
+ * Forward guest `console.*` calls as `log_update` messages — the same
115
+ * channel package-resolution warnings already use, so the editor's log
116
+ * panel shows them as they happen.
117
+ */
118
+ private logSink;
119
+ /** Post on the node's log channel, where the run has one. */
120
+ private postLog;
121
+ /**
122
+ * Sandbox policy from the node's props. The sandbox clamps
123
+ * `maxResponseBodyBytes` to its own ceiling, so no clamping here.
124
+ *
125
+ * The two capability switches default to the restrictive value and are only
126
+ * ever widened by an explicit choice on this node — nothing the guest code
127
+ * can reach.
128
+ */
129
+ private sandboxLimits;
130
+ /**
131
+ * Resolve the packs the body imports through the host's catalog.
132
+ *
133
+ * The imports are the declaration: a static `import` of an installed pack is
134
+ * served, and nothing else is — same rule a JS script runs under, so a body
135
+ * moved between the two behaves identically. A body that imports no pack gets
136
+ * no loader at all. An import that cannot be served — no catalog in this
137
+ * process, an uninstalled pack, a module the host refused — fails the node
138
+ * here rather than as a resolve error inside the guest. Version and digest
139
+ * drift only warn: resolution uses what is installed, and saying so on the
140
+ * node's log is the whole remedy.
141
+ *
142
+ * NodeTool's own guest modules (`@nodetool-ai/sandbox-nodetool/*`) are not
143
+ * packs; {@link CodeNode.resolveCapabilities} mounts those.
144
+ */
145
+ private resolveModules;
146
+ /** Post a warning on the node's log channel, where the run has one. */
147
+ private logWarning;
148
+ /**
149
+ * The globals every run gets, on either contract. Declared inputs arrive on
150
+ * one `inputs` object rather than as globals of their own name: sharing the
151
+ * global namespace with the sandbox API let an input called `env` or `data`
152
+ * shadow a bridge, and made every undeclared identifier ambiguous between a
153
+ * typo and a missing slot. State stays a direct reference so mutations
154
+ * persist across calls.
155
+ */
156
+ private sandboxGlobals;
157
+ /** Milliseconds the guest may run, or undefined for no limit. */
158
+ private timeoutMs;
159
+ /**
160
+ * What this invocation runs: the node's own properties, always.
161
+ *
162
+ * Linking a script *materializes* it — the pinned version's code, secrets
163
+ * and timeout are copied onto the node when the link is made or
164
+ * updated, and the `script` property records only which version they came
165
+ * from. So execution needs no database, no resolver, and no await: the body
166
+ * a run executes is the body the graph carries, which is also the body
167
+ * hydration read to decide whether this node streams its inputs.
168
+ */
169
+ private envelope;
170
+ /**
171
+ * Everything a run of this node's body needs regardless of contract: the
172
+ * prelude, the node's own policy, its globals and its module resolution.
173
+ * Every execution path starts from this and adds only what its own contract
174
+ * needs (an emit sink, an input bridge, a cancellation signal).
175
+ */
176
+ private sandboxOptions;
177
+ /**
178
+ * NodeTool's own guest modules (`@nodetool-ai/sandbox-nodetool/*`) for the
179
+ * namespaces this body imports, or nothing when it imports none.
180
+ *
181
+ * They are not packs, so they never reach the catalog: the host decides what
182
+ * a run can reach, and it says so with the gate it put on the context. The
183
+ * run carries that gate, exactly as the belt does — the import path and the
184
+ * belt bridge are two doors onto the same capabilities, so one gated and the
185
+ * other not would be no gate at all. It is the wiring
186
+ * `mountJsScriptSandbox` builds for a JS script, over the Code node's own
187
+ * module resolution.
188
+ */
189
+ private resolveCapabilities;
190
+ process(context?: ProcessingContext): Promise<CodeOutputs>;
191
+ /**
192
+ * Streaming-input execution: one invocation that drains its own inbox.
193
+ *
194
+ * The kernel calls this instead of the per-item path when the hydrated
195
+ * `is_streaming_input` flag is true, which for this node means the body
196
+ * calls `stream` (see {@link CodeNode.resolveStreamingInput}). The body runs
197
+ * once; `stream(name)` / `stream.any()` pull values out of `inputs` one take
198
+ * at a time, `emit` routes live through `outputs.emit`, and the `output`
199
+ * finals post as one bag when the body ends.
200
+ */
201
+ run(inputs: StreamingInputs, outputs: StreamingOutputs, context?: ProcessingContext): Promise<void>;
202
+ /** The deprecated return-bag path: implicit return, normalized output. */
203
+ private runLegacySingleShot;
204
+ genProcess(context?: ProcessingContext): AsyncGenerator<CodeOutputs>;
205
+ /**
206
+ * The emit/output pump: start the run, hand every `emit` bag downstream as
207
+ * it happens, then post the final `output` bag once the run succeeds.
208
+ *
209
+ * The guest's `emit` promise resolves only when the channel has accepted its
210
+ * bag, so a producer faster than the kernel blocks at the channel bound —
211
+ * backpressure with no new kernel concept.
212
+ */
213
+ private pumpEmitContract;
214
+ /** The deprecated `yield` path: the guest collects, the host replays. */
215
+ private runLegacyStream;
216
+ }
217
+ export {};
218
+ //# sourceMappingURL=code-node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code-node.d.ts","sourceRoot":"","sources":["../../src/nodes/code-node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,EACL,QAAQ,EAYR,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EACV,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAML,KAAK,gBAAgB,EAKtB,MAAM,gCAAgC,CAAC;AAWxC,gFAAgF;AAChF,KAAK,WAAW,GAAG,WAAW,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAC;AAyF5D,0EAA0E;AAC1E,KAAK,YAAY,GAAG,cAAc,qBAAqB,CAAC,CAAC;AACzD,KAAK,SAAS,GAAG,YAAY,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;AAkBpD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI,CAGtE;AAqBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,IAAI,GAAG,IAAI,CAEhE;AAoDD;;;GAGG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAqJxC,qBAAa,QAAS,SAAQ,QAAQ;IACpC,MAAM,CAAC,QAAQ,CAAC,QAAQ,wBAAwB;IAChD,MAAM,CAAC,QAAQ,CAAC,SAAS,sDAAiB;IAC1C,MAAM,CAAC,QAAQ,CAAC,KAAK,UAAU;IAC/B,MAAM,CAAC,QAAQ,CAAC,WAAW,SAqB8C;IACzE,MAAM,CAAC,QAAQ,CAAC,YAAY,WAAY;IACxC,MAAM,CAAC,QAAQ,CAAC,WAAW,UAAM;IACjC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,QAAQ;IAC7C,MAAM,CAAC,QAAQ,CAAC,sBAAsB,QAAQ;IAC9C;;;;;;OAMG;IACH,MAAM,CAAC,QAAQ,CAAC,qBAAqB,EAAE,WAAW,CAChD,OAAO,QAAQ,CAAC,qBAAqB,CACtC,CAA2E;IAE5E;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAsB;IA+C5B,IAAI,EAAE,MAAM,CAAC;IAab,OAAO,EAAE,MAAM,EAAE,CAAC;IAiBlB,MAAM,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAQ7C,OAAO,EAAE,MAAM,CAAC;IAShB,eAAe,EAAE,MAAM,CAAC;IAWxB,mBAAmB,EAAE,OAAO,CAAC;IAY7B,qBAAqB,EAAE,OAAO,CAAC;IAEjC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAEhC;IAED;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAgBpB;;;;OAIG;IACH,OAAO,CAAC,OAAO;IASf,6DAA6D;IAC7D,OAAO,CAAC,OAAO;IAgBf;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IAkBrB;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,cAAc;IA8CtB,uEAAuE;IACvE,OAAO,CAAC,UAAU;IAOlB;;;;;;;OAOG;YACW,cAAc;IAc5B,iEAAiE;IACjE,OAAO,CAAC,SAAS;IAMjB;;;;;;;;;OASG;IACH,OAAO,CAAC,QAAQ;IAQhB;;;;;OAKG;YACW,cAAc;IAkB5B;;;;;;;;;;;OAWG;YACW,mBAAmB;IAqB3B,OAAO,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CA0B/D;IAED;;;;;;;;;OASG;IACG,GAAG,CACP,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,gBAAgB,EACzB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAwDf;IAED,0EAA0E;YAC5D,mBAAmB;IAe1B,UAAU,CACf,OAAO,CAAC,EAAE,iBAAiB,GAC1B,cAAc,CAAC,WAAW,CAAC,CAa7B;IAED;;;;;;;OAOG;YACY,gBAAgB;IAuD/B,yEAAyE;YAC1D,eAAe;CAoC/B"}