@nodetool-ai/nodes-utils 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,74 @@
1
+ # @nodetool-ai/nodes-utils
2
+
3
+ Shared helpers for node packages: platform tags and lazy Node-only module loaders for [NodeTool](https://nodetool.ai).
4
+
5
+ A small utility crate every `*-nodes` package depends on instead of duplicating helpers or pulling the whole base-nodes barrel: platform-tagging helpers that stamp `static platforms` onto node arrays, lazy loaders for `node:` built-ins so non-portable paths don't block module init on non-Node runtimes, template variable substitution, Buffer-free base64, and the destination rules every `Save*File` node shares.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @nodetool-ai/nodes-utils
11
+ ```
12
+
13
+ ## Exported symbols
14
+
15
+ | Symbol | Kind | Description |
16
+ | --- | --- | --- |
17
+ | `tagAsServer` | function | Mark a `_NODES` array as server-only |
18
+ | `tagAsNode` | function | Mark nodes as requiring the Node runtime |
19
+ | `tagAsHybrid` | function | Mark nodes as running on server or browser |
20
+ | `tagAsBrowserGpu` | function | Mark nodes as needing browser WebGPU |
21
+ | `tagAsUniversal` | function | Mark nodes as runnable on any platform |
22
+ | `tagAsContentCard` | function | Tag nodes surfaced as content cards |
23
+ | `loadNodeFsPromises` | function | Lazily import `node:fs/promises` |
24
+ | `loadNodeFsSync` | function | Lazily import `node:fs` |
25
+ | `loadNodePath` | function | Lazily import `node:path` |
26
+ | `loadNodeOs` | function | Lazily import `node:os` |
27
+ | `loadNodeUrl` | function | Lazily import `node:url` |
28
+ | `renderTemplate` | function | Substitute `{{ variable }}` / `{variable}` placeholders |
29
+ | `referencedVariables` | function | List variable names referenced in a template |
30
+ | `base64ToBytes` | function | Decode base64 to `Uint8Array` (Node + browser) |
31
+ | `bytesToBase64` | function | Encode bytes to base64 (Node + browser) |
32
+ | `resolveSaveTarget` | function | Folder + filename → an absolute path, directory created, name numbered on collision |
33
+ | `resolveSaveFolder` | function | Pick the workspace folder or the node's own `folder` property |
34
+ | `uniqueFilePath` | function | `name.ext` → `name_1.ext`, `name_2.ext`, … past what is already there |
35
+ | `folderPathOf` | function | Read a folder property (path, `file://` URI or folder ref) |
36
+ | `SAVE_TO_WORKSPACE_TITLE` | const | Title every save node shows on the workspace toggle |
37
+ | `SAVE_TO_WORKSPACE_DESCRIPTION` | const | Description every save node shows on the workspace toggle |
38
+ | `VISIBLE_WHEN_NOT_SAVING_TO_WORKSPACE` | const | `json_schema_extra` (`visible_when`) that shows the folder field only while the toggle is off |
39
+
40
+ ## Usage
41
+
42
+ ```ts
43
+ import {
44
+ tagAsServer,
45
+ renderTemplate,
46
+ loadNodePath
47
+ } from "@nodetool-ai/nodes-utils";
48
+
49
+ tagAsServer(_NODES);
50
+
51
+ const text = renderTemplate("Hello {{ name }}", { name: "world" });
52
+
53
+ // Only touches node:path when this path actually runs
54
+ const path = await loadNodePath();
55
+ const full = path.join(dir, "file.txt");
56
+ ```
57
+
58
+ ### Saving a file from a node
59
+
60
+ ```ts
61
+ // Writes into the run's workspace folder while `save_to_workspace` is on, and
62
+ // numbers the name (`out_1.png`) rather than overwriting what is there.
63
+ const target = await resolveSaveTarget({
64
+ folder: this.folder,
65
+ filename: dateName(String(this.filename || "out.png")),
66
+ saveToWorkspace: this.save_to_workspace,
67
+ workspaceDir: context?.workspaceDir
68
+ });
69
+ ```
70
+
71
+ ## Links
72
+
73
+ - [NodeTool](https://nodetool.ai)
74
+ - [GitHub](https://github.com/nodetool-ai/nodetool)
@@ -0,0 +1 @@
1
+ 2026-09-28T09:19:08.910Z
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Buffer-free base64 helpers shared by the `*-nodes` packages.
3
+ *
4
+ * `atob`/`btoa` are global on Node ≥ 16 and in every browser, so these work
5
+ * in both environments without touching `node:buffer` (which the web bundle
6
+ * stubs out).
7
+ */
8
+ /** Decode a base64 string to bytes without `Buffer` (works on Node and browser). */
9
+ export declare function base64ToBytes(b64: string): Uint8Array;
10
+ /** Encode bytes to a base64 string without `Buffer`. */
11
+ export declare function bytesToBase64(bytes: Uint8Array): string;
12
+ //# sourceMappingURL=base64.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base64.d.ts","sourceRoot":"","sources":["../src/base64.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,oFAAoF;AACpF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAKrD;AAED,wDAAwD;AACxD,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAOvD"}
package/dist/base64.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Buffer-free base64 helpers shared by the `*-nodes` packages.
3
+ *
4
+ * `atob`/`btoa` are global on Node ≥ 16 and in every browser, so these work
5
+ * in both environments without touching `node:buffer` (which the web bundle
6
+ * stubs out).
7
+ */
8
+ /** Decode a base64 string to bytes without `Buffer` (works on Node and browser). */
9
+ export function base64ToBytes(b64) {
10
+ const binary = atob(b64);
11
+ const out = new Uint8Array(binary.length);
12
+ for (let i = 0; i < binary.length; i++)
13
+ out[i] = binary.charCodeAt(i);
14
+ return out;
15
+ }
16
+ /** Encode bytes to a base64 string without `Buffer`. */
17
+ export function bytesToBase64(bytes) {
18
+ let binary = "";
19
+ const chunk = 0x8000;
20
+ for (let i = 0; i < bytes.length; i += chunk) {
21
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
22
+ }
23
+ return btoa(binary);
24
+ }
25
+ //# sourceMappingURL=base64.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base64.js","sourceRoot":"","sources":["../src/base64.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,oFAAoF;AACpF,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IACzB,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC1C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IACtE,OAAO,GAAG,CAAC;AACb,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,aAAa,CAAC,KAAiB;IAC7C,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,MAAM,KAAK,GAAG,MAAM,CAAC;IACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC;QAC7C,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @nodetool-ai/nodes-utils — shared helpers for every `*-nodes` package.
3
+ *
4
+ * A few small concerns live here so each per-domain package can depend on
5
+ * a single utility crate instead of duplicating these helpers or pulling
6
+ * the entire base-nodes barrel just to access them:
7
+ *
8
+ * - `platform-tags.ts` — `tagAsServer` / `tagAsHybrid` / `tagAsUniversal`
9
+ * helpers that stamp `static platforms` on `_NODES` arrays.
10
+ * - `node-only-modules.ts` — lazy loaders for `node:fs` / `node:path` /
11
+ * etc. so non-portable code paths don't block module init on
12
+ * non-Node runtimes.
13
+ * - `template.ts` — `{{ variable }}` / `{variable}` substitution shared by
14
+ * the Prompt, Format Text and Agent nodes.
15
+ * - `base64.ts` — Buffer-free base64 encode/decode usable in Node and the
16
+ * browser bundle alike.
17
+ * - `save-target.ts` — the `save_to_workspace` toggle and the numbered
18
+ * filenames every `Save*File` node shares.
19
+ */
20
+ export { tagAsServer, tagAsNode, tagAsHybrid, tagAsBrowserGpu, tagAsUniversal, tagAsContentCard } from "./platform-tags.js";
21
+ export { loadNodeFsPromises, loadNodeFsSync, loadNodePath, loadNodeOs, loadNodeUrl } from "./node-only-modules.js";
22
+ export { renderTemplate, referencedVariables } from "./template.js";
23
+ export type { TemplateValue, TemplateVars } from "./template.js";
24
+ export { base64ToBytes, bytesToBase64 } from "./base64.js";
25
+ export { resolveModelBytes } from "./model-bytes.js";
26
+ export type { ModelBytesRefLike } from "./model-bytes.js";
27
+ export { SAVE_TO_WORKSPACE_TITLE, SAVE_TO_WORKSPACE_DESCRIPTION, VISIBLE_WHEN_NOT_SAVING_TO_WORKSPACE, folderPathOf, resolveSaveFolder, saveFilename, uniqueFilePath, resolveSaveTarget, writeSavedFile } from "./save-target.js";
28
+ export type { SaveFilenameOptions, SaveFolderOptions, SaveTargetOptions, SaveWorkspace } from "./save-target.js";
29
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EACL,WAAW,EACX,SAAS,EACT,WAAW,EACX,eAAe,EACf,cAAc,EACd,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,YAAY,EACZ,UAAU,EACV,WAAW,EACZ,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAEjE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,EACL,uBAAuB,EACvB,6BAA6B,EAC7B,oCAAoC,EACpC,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,cAAc,EACf,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,EACd,MAAM,kBAAkB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @nodetool-ai/nodes-utils — shared helpers for every `*-nodes` package.
3
+ *
4
+ * A few small concerns live here so each per-domain package can depend on
5
+ * a single utility crate instead of duplicating these helpers or pulling
6
+ * the entire base-nodes barrel just to access them:
7
+ *
8
+ * - `platform-tags.ts` — `tagAsServer` / `tagAsHybrid` / `tagAsUniversal`
9
+ * helpers that stamp `static platforms` on `_NODES` arrays.
10
+ * - `node-only-modules.ts` — lazy loaders for `node:fs` / `node:path` /
11
+ * etc. so non-portable code paths don't block module init on
12
+ * non-Node runtimes.
13
+ * - `template.ts` — `{{ variable }}` / `{variable}` substitution shared by
14
+ * the Prompt, Format Text and Agent nodes.
15
+ * - `base64.ts` — Buffer-free base64 encode/decode usable in Node and the
16
+ * browser bundle alike.
17
+ * - `save-target.ts` — the `save_to_workspace` toggle and the numbered
18
+ * filenames every `Save*File` node shares.
19
+ */
20
+ export { tagAsServer, tagAsNode, tagAsHybrid, tagAsBrowserGpu, tagAsUniversal, tagAsContentCard } from "./platform-tags.js";
21
+ export { loadNodeFsPromises, loadNodeFsSync, loadNodePath, loadNodeOs, loadNodeUrl } from "./node-only-modules.js";
22
+ export { renderTemplate, referencedVariables } from "./template.js";
23
+ export { base64ToBytes, bytesToBase64 } from "./base64.js";
24
+ export { resolveModelBytes } from "./model-bytes.js";
25
+ export { SAVE_TO_WORKSPACE_TITLE, SAVE_TO_WORKSPACE_DESCRIPTION, VISIBLE_WHEN_NOT_SAVING_TO_WORKSPACE, folderPathOf, resolveSaveFolder, saveFilename, uniqueFilePath, resolveSaveTarget, writeSavedFile } from "./save-target.js";
26
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EACL,WAAW,EACX,SAAS,EACT,WAAW,EACX,eAAe,EACf,cAAc,EACd,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,YAAY,EACZ,UAAU,EACV,WAAW,EACZ,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAGpE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAGrD,OAAO,EACL,uBAAuB,EACvB,6BAA6B,EAC7B,oCAAoC,EACpC,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,cAAc,EACf,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Browser-safe model-bytes resolution (inline data → storage → uri fetch).
3
+ *
4
+ * Moved here from `packages/video-nodes/src/nodes/model3d/render.ts` so the
5
+ * Blender nodes and `RenderToImage` share it. Stays browser-safe: no
6
+ * `node:path` at module scope — the one Node-only read (`file://`) loads
7
+ * `node:fs/promises` and `node:url` through a dynamic import that only runs
8
+ * when `IS_NODE` is set.
9
+ */
10
+ import type { ProcessingContext } from "@nodetool-ai/runtime";
11
+ /**
12
+ * The fields of a model ref this resolution reads. Mirrors
13
+ * `Model3DRefLike` in video-nodes; node props arrive as `any`, so anything
14
+ * outside this shape is a runtime edge the null checks below absorb.
15
+ */
16
+ export interface ModelBytesRefLike {
17
+ data?: Uint8Array | string;
18
+ uri?: string;
19
+ }
20
+ /** Browser-safe model-bytes resolution (inline data → storage → uri fetch). */
21
+ export declare function resolveModelBytes(model: ModelBytesRefLike | null | undefined, context?: ProcessingContext): Promise<Uint8Array>;
22
+ //# sourceMappingURL=model-bytes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-bytes.d.ts","sourceRoot":"","sources":["../src/model-bytes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAI9D;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,+EAA+E;AAC/E,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,EAC3C,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,UAAU,CAAC,CAyCrB"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Browser-safe model-bytes resolution (inline data → storage → uri fetch).
3
+ *
4
+ * Moved here from `packages/video-nodes/src/nodes/model3d/render.ts` so the
5
+ * Blender nodes and `RenderToImage` share it. Stays browser-safe: no
6
+ * `node:path` at module scope — the one Node-only read (`file://`) loads
7
+ * `node:fs/promises` and `node:url` through a dynamic import that only runs
8
+ * when `IS_NODE` is set.
9
+ */
10
+ import { IS_NODE } from "@nodetool-ai/config";
11
+ import { isNonEmptyString } from "@nodetool-ai/protocol";
12
+ import { fetchExternalMedia } from "@nodetool-ai/runtime";
13
+ import { base64ToBytes } from "./base64.js";
14
+ /** Browser-safe model-bytes resolution (inline data → storage → uri fetch). */
15
+ export async function resolveModelBytes(model, context) {
16
+ if (model === null || model === undefined)
17
+ return new Uint8Array();
18
+ const ref = model;
19
+ if (ref.data instanceof Uint8Array && ref.data.length > 0) {
20
+ return ref.data;
21
+ }
22
+ if (ref.data !== undefined && isNonEmptyString(ref.data)) {
23
+ return base64ToBytes(ref.data);
24
+ }
25
+ const uri = ref.uri ?? "";
26
+ if (uri === "")
27
+ return new Uint8Array();
28
+ if (uri.startsWith("data:")) {
29
+ const comma = uri.indexOf(",");
30
+ if (comma === -1)
31
+ throw new Error(`Malformed data URI: ${uri.slice(0, 32)}…`);
32
+ return base64ToBytes(uri.slice(comma + 1));
33
+ }
34
+ if (context?.storage) {
35
+ const stored = await context.storage.retrieve(uri);
36
+ if (stored && stored.length > 0)
37
+ return new Uint8Array(stored);
38
+ }
39
+ if (uri.startsWith("file://") && IS_NODE) {
40
+ const { readFile } = await import("node:fs/promises");
41
+ const { fileURLToPath } = await import("node:url");
42
+ return new Uint8Array(await readFile(fileURLToPath(uri)));
43
+ }
44
+ if (uri.startsWith("http://") || uri.startsWith("https://")) {
45
+ // Caller-supplied media uri — the media-ref egress policy decides.
46
+ const res = await fetchExternalMedia(uri);
47
+ if (!res.ok) {
48
+ throw new Error(`Failed to fetch model (${res.status}): ${uri}`);
49
+ }
50
+ return new Uint8Array(await res.arrayBuffer());
51
+ }
52
+ return new Uint8Array();
53
+ }
54
+ //# sourceMappingURL=model-bytes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-bytes.js","sourceRoot":"","sources":["../src/model-bytes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG1D,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAY5C,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,KAA2C,EAC3C,OAA2B;IAE3B,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,UAAU,EAAE,CAAC;IACnE,MAAM,GAAG,GAAG,KAAK,CAAC;IAElB,IAAI,GAAG,CAAC,IAAI,YAAY,UAAU,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1D,OAAO,GAAG,CAAC,IAAI,CAAC;IAClB,CAAC;IACD,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,IAAI,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QACzD,OAAO,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAED,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC;IAC1B,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,IAAI,UAAU,EAAE,CAAC;IAExC,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,KAAK,KAAK,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;QAC9E,OAAO,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC;IAED,IAAI,OAAO,EAAE,OAAO,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACnD,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;IACjE,CAAC;IAED,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,OAAO,EAAE,CAAC;QACzC,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;QACtD,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;QACnD,OAAO,IAAI,UAAU,CAAC,MAAM,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5D,mEAAmE;QACnE,MAAM,GAAG,GAAG,MAAM,kBAAkB,CAAC,GAAG,CAAC,CAAC;QAC1C,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,0BAA0B,GAAG,CAAC,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC;QACnE,CAAC;QACD,OAAO,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;IACjD,CAAC;IAED,OAAO,IAAI,UAAU,EAAE,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Lazy loaders for Node.js built-in modules that block static portability.
3
+ *
4
+ * Nodes that call into these should declare `static platforms = ["node"]`
5
+ * — the module remains loadable on every runtime (the dynamic import is
6
+ * never evaluated at module-init time), but the specific node fails at
7
+ * call time if invoked off-Node. The platform filter prevents that from
8
+ * happening in normal use.
9
+ *
10
+ * Centralised here so the classifier can attribute the dynamic-import
11
+ * blocker to a single source path; per-module imports of these helpers
12
+ * are relative and don't appear in the external-import set.
13
+ */
14
+ export declare function loadNodeFsPromises(): Promise<typeof import("node:fs").promises>;
15
+ export declare function loadNodeFsSync(): Promise<typeof import("node:fs")>;
16
+ export declare function loadNodePath(): Promise<typeof import("node:path")>;
17
+ export declare function loadNodeOs(): Promise<typeof import("node:os")>;
18
+ export declare function loadNodeUrl(): Promise<typeof import("node:url")>;
19
+ //# sourceMappingURL=node-only-modules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-only-modules.d.ts","sourceRoot":"","sources":["../src/node-only-modules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,wBAAsB,kBAAkB,IAAI,OAAO,CACjD,cAAc,SAAS,EAAE,QAAQ,CAClC,CAGA;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,cAAc,SAAS,CAAC,CAAC,CAExE;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,cAAc,WAAW,CAAC,CAAC,CAExE;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,cAAc,SAAS,CAAC,CAAC,CAEpE;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,cAAc,UAAU,CAAC,CAAC,CAEtE"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Lazy loaders for Node.js built-in modules that block static portability.
3
+ *
4
+ * Nodes that call into these should declare `static platforms = ["node"]`
5
+ * — the module remains loadable on every runtime (the dynamic import is
6
+ * never evaluated at module-init time), but the specific node fails at
7
+ * call time if invoked off-Node. The platform filter prevents that from
8
+ * happening in normal use.
9
+ *
10
+ * Centralised here so the classifier can attribute the dynamic-import
11
+ * blocker to a single source path; per-module imports of these helpers
12
+ * are relative and don't appear in the external-import set.
13
+ */
14
+ export async function loadNodeFsPromises() {
15
+ const mod = await import("node:fs");
16
+ return mod.promises;
17
+ }
18
+ export async function loadNodeFsSync() {
19
+ return import("node:fs");
20
+ }
21
+ export async function loadNodePath() {
22
+ return import("node:path");
23
+ }
24
+ export async function loadNodeOs() {
25
+ return import("node:os");
26
+ }
27
+ export async function loadNodeUrl() {
28
+ return import("node:url");
29
+ }
30
+ //# sourceMappingURL=node-only-modules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-only-modules.js","sourceRoot":"","sources":["../src/node-only-modules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,CAAC,KAAK,UAAU,kBAAkB;IAGtC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;IACpC,OAAO,GAAG,CAAC,QAAQ,CAAC;AACtB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,OAAO,MAAM,CAAC,WAAW,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,OAAO,MAAM,CAAC,UAAU,CAAC,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Platform tagging helpers for base-nodes.
3
+ *
4
+ * `tagAsServer(classes)` stamps `static platforms = SERVER_PLATFORMS` on
5
+ * every class in a registration array — declaring support for `node`,
6
+ * `workers`, and `edge` but **not** `browser`. This is the right default
7
+ * for nodes that make outbound HTTP calls or read env secrets (most LLM,
8
+ * HTTP, search, and integration nodes) — they can't be safely shipped to
9
+ * a browser tab without a separate CORS/secrets story.
10
+ *
11
+ * `tagAsHybrid(classes)` stamps `["node", "browser"]` — for nodes with a
12
+ * Node execution path AND a browser one but no V8-isolate path, typical
13
+ * of WebGPU shader nodes (Workers and Edge runtimes lack WebGPU).
14
+ *
15
+ * `tagAsUniversal(classes)` stamps `ALL_PLATFORMS` — for nodes that
16
+ * genuinely work on every runtime including the browser. Use sparingly:
17
+ * the node must handle CORS / secret-exposure / bundle-size implications
18
+ * itself.
19
+ *
20
+ * All three helpers leave alone any class that already declares its own
21
+ * `static platforms` — per-class overrides always win. The mutation is a
22
+ * one-time `Object.defineProperty` at module load.
23
+ */
24
+ import type { NodeClass } from "@nodetool-ai/node-sdk";
25
+ /** Tag every class in `classes` as supporting node + workers + edge. */
26
+ export declare function tagAsServer<T extends readonly NodeClass[]>(classes: T): T;
27
+ /**
28
+ * Tag every class in `classes` as supporting **only** the node tier — the
29
+ * (otherwise implicit) most-restrictive default. Use for nodes that spawn a
30
+ * subprocess (ffmpeg, ffprobe, yt-dlp, …) or write os.tmpdir() temp files:
31
+ * the workers (Cloudflare V8) and edge (Vercel/Deno) tiers have no subprocess
32
+ * and no filesystem, so offering them there is a guaranteed runtime failure.
33
+ * Stamping it explicitly documents the intent rather than relying on the
34
+ * unset default. Leaves any class with its own `static platforms` alone.
35
+ */
36
+ export declare function tagAsNode<T extends readonly NodeClass[]>(classes: T): T;
37
+ /**
38
+ * Tag every class in `classes` as supporting node + browser (no V8 server
39
+ * isolates). Use for WebGPU / Canvas / DOM-backed nodes that have both a
40
+ * Node fallback and a browser-native execution path.
41
+ */
42
+ export declare function tagAsHybrid<T extends readonly NodeClass[]>(classes: T): T;
43
+ /**
44
+ * Tag every class in `classes` as supporting every defined platform.
45
+ * Reserve for pure-JS nodes with no network calls, no secrets, and no
46
+ * platform-specific side effects (e.g. text/data transforms).
47
+ */
48
+ export declare function tagAsUniversal<T extends readonly NodeClass[]>(classes: T): T;
49
+ /**
50
+ * Like {@link tagAsHybrid} (node + browser) but additionally marks the classes
51
+ * as requiring a WebGPU device in the browser. WebGPU shader nodes use this so
52
+ * the in-browser runner routes graphs containing them to the server — where a
53
+ * GPU is always available via Dawn — when `navigator.gpu` is missing, instead
54
+ * of failing the run. Server execution is unchanged.
55
+ *
56
+ * `requiresGpu` is stamped only on classes that end up browser-capable: a class
57
+ * already tagged server keeps its platforms and is left unmarked (it can't run
58
+ * in the browser anyway). Per-class `static requiresGpu` overrides win.
59
+ */
60
+ export declare function tagAsBrowserGpu<T extends readonly NodeClass[]>(classes: T): T;
61
+ /**
62
+ * Stamp `static body = "content_card"` on every class in `classes` so the
63
+ * editor renders them as a media content card (the image/video/audio preview
64
+ * body) instead of the generic input/output layout. Leaves any class that
65
+ * already declares its own `static body` alone — per-class overrides win.
66
+ *
67
+ * Apply to media-output node groups, composing with a platform tagger:
68
+ * `tagAsHybrid(tagAsContentCard([...]))`.
69
+ */
70
+ export declare function tagAsContentCard<T extends readonly NodeClass[]>(classes: T): T;
71
+ //# sourceMappingURL=platform-tags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-tags.d.ts","sourceRoot":"","sources":["../src/platform-tags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAyBvD,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,CAAC,SAAS,SAAS,SAAS,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAEzE;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,SAAS,SAAS,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAEvE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,SAAS,SAAS,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAEzE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,SAAS,SAAS,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAE5E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,SAAS,SAAS,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAc7E;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,SAAS,SAAS,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAW9E"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Platform tagging helpers for base-nodes.
3
+ *
4
+ * `tagAsServer(classes)` stamps `static platforms = SERVER_PLATFORMS` on
5
+ * every class in a registration array — declaring support for `node`,
6
+ * `workers`, and `edge` but **not** `browser`. This is the right default
7
+ * for nodes that make outbound HTTP calls or read env secrets (most LLM,
8
+ * HTTP, search, and integration nodes) — they can't be safely shipped to
9
+ * a browser tab without a separate CORS/secrets story.
10
+ *
11
+ * `tagAsHybrid(classes)` stamps `["node", "browser"]` — for nodes with a
12
+ * Node execution path AND a browser one but no V8-isolate path, typical
13
+ * of WebGPU shader nodes (Workers and Edge runtimes lack WebGPU).
14
+ *
15
+ * `tagAsUniversal(classes)` stamps `ALL_PLATFORMS` — for nodes that
16
+ * genuinely work on every runtime including the browser. Use sparingly:
17
+ * the node must handle CORS / secret-exposure / bundle-size implications
18
+ * itself.
19
+ *
20
+ * All three helpers leave alone any class that already declares its own
21
+ * `static platforms` — per-class overrides always win. The mutation is a
22
+ * one-time `Object.defineProperty` at module load.
23
+ */
24
+ import { ALL_PLATFORMS, DEFAULT_PLATFORMS, NODE_AND_BROWSER_PLATFORMS, SERVER_PLATFORMS } from "@nodetool-ai/protocol";
25
+ function tagWith(classes, platforms) {
26
+ for (const cls of classes) {
27
+ if (cls.platforms !== undefined)
28
+ continue;
29
+ Object.defineProperty(cls, "platforms", {
30
+ value: platforms,
31
+ writable: false,
32
+ configurable: true,
33
+ enumerable: true
34
+ });
35
+ }
36
+ return classes;
37
+ }
38
+ /** Tag every class in `classes` as supporting node + workers + edge. */
39
+ export function tagAsServer(classes) {
40
+ return tagWith(classes, SERVER_PLATFORMS);
41
+ }
42
+ /**
43
+ * Tag every class in `classes` as supporting **only** the node tier — the
44
+ * (otherwise implicit) most-restrictive default. Use for nodes that spawn a
45
+ * subprocess (ffmpeg, ffprobe, yt-dlp, …) or write os.tmpdir() temp files:
46
+ * the workers (Cloudflare V8) and edge (Vercel/Deno) tiers have no subprocess
47
+ * and no filesystem, so offering them there is a guaranteed runtime failure.
48
+ * Stamping it explicitly documents the intent rather than relying on the
49
+ * unset default. Leaves any class with its own `static platforms` alone.
50
+ */
51
+ export function tagAsNode(classes) {
52
+ return tagWith(classes, DEFAULT_PLATFORMS);
53
+ }
54
+ /**
55
+ * Tag every class in `classes` as supporting node + browser (no V8 server
56
+ * isolates). Use for WebGPU / Canvas / DOM-backed nodes that have both a
57
+ * Node fallback and a browser-native execution path.
58
+ */
59
+ export function tagAsHybrid(classes) {
60
+ return tagWith(classes, NODE_AND_BROWSER_PLATFORMS);
61
+ }
62
+ /**
63
+ * Tag every class in `classes` as supporting every defined platform.
64
+ * Reserve for pure-JS nodes with no network calls, no secrets, and no
65
+ * platform-specific side effects (e.g. text/data transforms).
66
+ */
67
+ export function tagAsUniversal(classes) {
68
+ return tagWith(classes, ALL_PLATFORMS);
69
+ }
70
+ /**
71
+ * Like {@link tagAsHybrid} (node + browser) but additionally marks the classes
72
+ * as requiring a WebGPU device in the browser. WebGPU shader nodes use this so
73
+ * the in-browser runner routes graphs containing them to the server — where a
74
+ * GPU is always available via Dawn — when `navigator.gpu` is missing, instead
75
+ * of failing the run. Server execution is unchanged.
76
+ *
77
+ * `requiresGpu` is stamped only on classes that end up browser-capable: a class
78
+ * already tagged server keeps its platforms and is left unmarked (it can't run
79
+ * in the browser anyway). Per-class `static requiresGpu` overrides win.
80
+ */
81
+ export function tagAsBrowserGpu(classes) {
82
+ const tagged = tagWith(classes, NODE_AND_BROWSER_PLATFORMS);
83
+ for (const cls of tagged) {
84
+ const platforms = cls.platforms;
85
+ if (!platforms?.includes("browser"))
86
+ continue;
87
+ if (cls.requiresGpu !== undefined)
88
+ continue;
89
+ Object.defineProperty(cls, "requiresGpu", {
90
+ value: true,
91
+ writable: false,
92
+ configurable: true,
93
+ enumerable: true
94
+ });
95
+ }
96
+ return tagged;
97
+ }
98
+ /**
99
+ * Stamp `static body = "content_card"` on every class in `classes` so the
100
+ * editor renders them as a media content card (the image/video/audio preview
101
+ * body) instead of the generic input/output layout. Leaves any class that
102
+ * already declares its own `static body` alone — per-class overrides win.
103
+ *
104
+ * Apply to media-output node groups, composing with a platform tagger:
105
+ * `tagAsHybrid(tagAsContentCard([...]))`.
106
+ */
107
+ export function tagAsContentCard(classes) {
108
+ for (const cls of classes) {
109
+ if (cls.body !== undefined)
110
+ continue;
111
+ Object.defineProperty(cls, "body", {
112
+ value: "content_card",
113
+ writable: false,
114
+ configurable: true,
115
+ enumerable: true
116
+ });
117
+ }
118
+ return classes;
119
+ }
120
+ //# sourceMappingURL=platform-tags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-tags.js","sourceRoot":"","sources":["../src/platform-tags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,0BAA0B,EAC1B,gBAAgB,EAEjB,MAAM,uBAAuB,CAAC;AAE/B,SAAS,OAAO,CACd,OAAU,EACV,SAA8B;IAE9B,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;QAC1B,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS;YAAE,SAAS;QAC1C,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE;YACtC,KAAK,EAAE,SAAS;YAChB,QAAQ,EAAE,KAAK;YACf,YAAY,EAAE,IAAI;YAClB,UAAU,EAAE,IAAI;SACjB,CAAC,CAAC;IACL,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,WAAW,CAAiC,OAAU;IACpE,OAAO,OAAO,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC;AAC5C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,SAAS,CAAiC,OAAU;IAClE,OAAO,OAAO,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC;AAC7C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAiC,OAAU;IACpE,OAAO,OAAO,CAAC,OAAO,EAAE,0BAA0B,CAAC,CAAC;AACtD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAiC,OAAU;IACvE,OAAO,OAAO,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;AACzC,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAAiC,OAAU;IACxE,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,0BAA0B,CAAC,CAAC;IAC5D,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QACzB,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;QAChC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,SAAS,CAAC;YAAE,SAAS;QAC9C,IAAI,GAAG,CAAC,WAAW,KAAK,SAAS;YAAE,SAAS;QAC5C,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,aAAa,EAAE;YACxC,KAAK,EAAE,IAAI;YACX,QAAQ,EAAE,KAAK;YACf,YAAY,EAAE,IAAI;YAClB,UAAU,EAAE,IAAI;SACjB,CAAC,CAAC;IACL,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAiC,OAAU;IACzE,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;QAC1B,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS;QACrC,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE;YACjC,KAAK,EAAE,cAAc;YACrB,QAAQ,EAAE,KAAK;YACf,YAAY,EAAE,IAAI;YAClB,UAAU,EAAE,IAAI;SACjB,CAAC,CAAC;IACL,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,115 @@
1
+ /** Title every save node shows on the workspace toggle. */
2
+ export declare const SAVE_TO_WORKSPACE_TITLE = "Save to workspace";
3
+ /** Description every save node shows on the workspace toggle. */
4
+ export declare const SAVE_TO_WORKSPACE_DESCRIPTION = "Write the file into this workflow's workspace folder and number it (name_1, name_2, \u2026) instead of overwriting. Turn this off to choose a folder yourself.";
5
+ /**
6
+ * `json_schema_extra` for a folder property that only applies while the
7
+ * workspace toggle is off. The editor hides the field rather than showing a
8
+ * path that has no effect.
9
+ */
10
+ export declare const VISIBLE_WHEN_NOT_SAVING_TO_WORKSPACE: {
11
+ readonly visible_when: {
12
+ readonly property: "save_to_workspace";
13
+ readonly equals: false;
14
+ };
15
+ };
16
+ /** A folder ref — what a `folder`-typed property carries. */
17
+ interface FolderRefValue {
18
+ uri?: string;
19
+ }
20
+ /** Every shape a node's `folder` property arrives in. */
21
+ export type FolderValue = string | FolderRefValue | null | undefined;
22
+ /**
23
+ * Read a folder property, which may be a plain path, a `file://` URI or a
24
+ * folder ref object. Returns "" when nothing usable is set.
25
+ */
26
+ export declare function folderPathOf(raw: FolderValue): string;
27
+ /**
28
+ * The slice of the run's workspace a save node needs.
29
+ *
30
+ * Structural rather than imported: `@nodetool-ai/nodes-utils` sits below
31
+ * `@nodetool-ai/runtime` in the dependency order, and a `ProcessingContext`'s
32
+ * `workspace` satisfies this shape.
33
+ */
34
+ export interface SaveWorkspace {
35
+ localDir: string | null;
36
+ exists(path: string): Promise<boolean>;
37
+ write(path: string, data: Uint8Array | string, contentType?: string): Promise<void>;
38
+ }
39
+ export interface SaveFolderOptions {
40
+ /** The node's `folder` property, whatever shape it arrived in. */
41
+ folder: FolderValue;
42
+ /** The node's `save_to_workspace` property. */
43
+ saveToWorkspace: boolean | undefined;
44
+ /** `context.workspace` — null when the run has no workspace. */
45
+ workspace?: SaveWorkspace | null;
46
+ /**
47
+ * `context.workspaceDir`. Only meaningful for a workspace that is a real
48
+ * folder; a cloud run has none and saves through `workspace` instead.
49
+ */
50
+ workspaceDir?: string | null;
51
+ }
52
+ /**
53
+ * Pick the destination folder.
54
+ *
55
+ * With the toggle on, the workspace folder wins over the `folder` property —
56
+ * the editor hides that field while the toggle is on, so a value left there is
57
+ * a leftover, not a choice. A run with no workspace assigned has nowhere to put
58
+ * the file, so it falls back to the folder and finally to the working
59
+ * directory rather than failing the run.
60
+ */
61
+ export declare function resolveSaveFolder(opts: SaveFolderOptions): string;
62
+ /** What a save node calls its file when the user leaves the name empty. */
63
+ export interface SaveFilenameOptions {
64
+ /** The node's filename property, with date tokens already expanded. */
65
+ filename: string;
66
+ /** The name to use when `filename` is blank. Carries its own extension. */
67
+ fallback: string;
68
+ /** Extension (with the dot) to append when the name carries none. */
69
+ extension?: string;
70
+ }
71
+ /**
72
+ * The name a save node writes under.
73
+ *
74
+ * A blank name used to reach `path.resolve(folder, "")`, which is the folder
75
+ * itself: the file landed *beside* the workspace, named after it, with no
76
+ * extension. And a name typed without one ("render") produced a file no image
77
+ * viewer would open. Both are answered here rather than in each node.
78
+ */
79
+ export declare function saveFilename(opts: SaveFilenameOptions): string;
80
+ /**
81
+ * Return a path nothing occupies: `target` itself when free, otherwise
82
+ * `name_1.ext`, `name_2.ext`, … in the same directory.
83
+ */
84
+ export declare function uniqueFilePath(target: string): Promise<string>;
85
+ export interface SaveTargetOptions extends SaveFolderOptions {
86
+ /** The node's filename property, with date tokens already expanded. */
87
+ filename: string;
88
+ /** Skip the numbered suffix and write over an existing file. */
89
+ overwrite?: boolean;
90
+ }
91
+ /**
92
+ * Resolve folder + filename into an absolute path ready to write to: the
93
+ * directory exists when this returns, and the name does not collide unless the
94
+ * caller asked to overwrite.
95
+ */
96
+ export declare function resolveSaveTarget(opts: SaveTargetOptions): Promise<string>;
97
+ /**
98
+ * Write a saved file where the node's properties say it goes, and report the
99
+ * path it landed on.
100
+ *
101
+ * With the workspace toggle on this writes through the workspace, so the same
102
+ * node produces a file in the user's folder locally and an object in their
103
+ * workspace prefix in the cloud. The name is deduplicated the same way in both
104
+ * cases — a run that saves twice in one second must not silently overwrite its
105
+ * first output.
106
+ *
107
+ * With the toggle off the node is naming a host folder, which only exists on a
108
+ * local install; a cloud run has no such folder and says so rather than writing
109
+ * somewhere the user will never find.
110
+ */
111
+ export declare function writeSavedFile(opts: SaveTargetOptions & {
112
+ bytes: Uint8Array | string;
113
+ }): Promise<string>;
114
+ export {};
115
+ //# sourceMappingURL=save-target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"save-target.d.ts","sourceRoot":"","sources":["../src/save-target.ts"],"names":[],"mappings":"AAiBA,2DAA2D;AAC3D,eAAO,MAAM,uBAAuB,sBAAsB,CAAC;AAE3D,iEAAiE;AACjE,eAAO,MAAM,6BAA6B,mKACmH,CAAC;AAE9J;;;;GAIG;AACH,eAAO,MAAM,oCAAoC;aAC/C,YAAY;iBAAI,QAAQ,EAAE,mBAAmB;iBAAE,MAAM;;CAC7C,CAAC;AAaX,6DAA6D;AAC7D,UAAU,cAAc;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,yDAAyD;AACzD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,cAAc,GAAG,IAAI,GAAG,SAAS,CAAC;AAcrE;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAIrD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrF;AAED,MAAM,WAAW,iBAAiB;IAChC,kEAAkE;IAClE,MAAM,EAAE,WAAW,CAAC;IACpB,+CAA+C;IAC/C,eAAe,EAAE,OAAO,GAAG,SAAS,CAAC;IACrC,gEAAgE;IAChE,SAAS,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACjC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAOjE;AAED,2EAA2E;AAC3E,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,MAAM,CAO9D;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAmBpE;AAoBD,MAAM,WAAW,iBAAkB,SAAQ,iBAAiB;IAC1D,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,MAAM,CAAC,CAOjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,iBAAiB,GAAG;IAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAAA;CAAE,GACvD,OAAO,CAAC,MAAM,CAAC,CA4BjB"}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Destination resolution shared by every node that writes a file to disk
3
+ * (`nodetool.image.SaveImageFile`, `SaveAudioFile`, `SaveTextFile`,
4
+ * `SaveVideoFile`, `SaveDocumentFile`, `SaveModel3DFile`).
5
+ *
6
+ * Two things every one of them needs and each used to solve on its own:
7
+ *
8
+ * - **Where to write.** Without a folder they fell back to `.` — the server
9
+ * process working directory, which is almost never where a user wants
10
+ * output. The `save_to_workspace` toggle points them at the run's workspace
11
+ * folder instead.
12
+ * - **What to call it.** Filenames carry second-granularity date tokens, so
13
+ * two saves in the same second collided and one silently overwrote the
14
+ * other. `uniqueFilePath` numbers them `name_1.ext`, `name_2.ext`, …
15
+ */
16
+ import { loadNodeFsPromises, loadNodePath } from "./node-only-modules.js";
17
+ /** Title every save node shows on the workspace toggle. */
18
+ export const SAVE_TO_WORKSPACE_TITLE = "Save to workspace";
19
+ /** Description every save node shows on the workspace toggle. */
20
+ export const SAVE_TO_WORKSPACE_DESCRIPTION = "Write the file into this workflow's workspace folder and number it (name_1, name_2, …) instead of overwriting. Turn this off to choose a folder yourself.";
21
+ /**
22
+ * `json_schema_extra` for a folder property that only applies while the
23
+ * workspace toggle is off. The editor hides the field rather than showing a
24
+ * path that has no effect.
25
+ */
26
+ export const VISIBLE_WHEN_NOT_SAVING_TO_WORKSPACE = {
27
+ visible_when: { property: "save_to_workspace", equals: false }
28
+ };
29
+ /** Turn a `file://` URI into a plain path; leave a plain path alone. */
30
+ function toPath(value) {
31
+ if (!value.startsWith("file://"))
32
+ return value;
33
+ const rest = value.slice("file://".length);
34
+ try {
35
+ return decodeURIComponent(rest);
36
+ }
37
+ catch {
38
+ return rest;
39
+ }
40
+ }
41
+ function isPathString(value) {
42
+ return typeof value === "string";
43
+ }
44
+ function isFolderRef(value) {
45
+ return typeof value === "object" && value !== null;
46
+ }
47
+ function isSetString(value) {
48
+ return value !== undefined && value.trim() !== "";
49
+ }
50
+ /**
51
+ * Read a folder property, which may be a plain path, a `file://` URI or a
52
+ * folder ref object. Returns "" when nothing usable is set.
53
+ */
54
+ export function folderPathOf(raw) {
55
+ if (isPathString(raw))
56
+ return toPath(raw.trim());
57
+ if (isFolderRef(raw) && isSetString(raw.uri))
58
+ return toPath(raw.uri.trim());
59
+ return "";
60
+ }
61
+ /**
62
+ * Pick the destination folder.
63
+ *
64
+ * With the toggle on, the workspace folder wins over the `folder` property —
65
+ * the editor hides that field while the toggle is on, so a value left there is
66
+ * a leftover, not a choice. A run with no workspace assigned has nowhere to put
67
+ * the file, so it falls back to the folder and finally to the working
68
+ * directory rather than failing the run.
69
+ */
70
+ export function resolveSaveFolder(opts) {
71
+ const folder = folderPathOf(opts.folder);
72
+ const workspaceDir = opts.workspace?.localDir ?? opts.workspaceDir ?? null;
73
+ if (opts.saveToWorkspace === true && workspaceDir) {
74
+ return workspaceDir;
75
+ }
76
+ return folder || workspaceDir || ".";
77
+ }
78
+ /**
79
+ * The name a save node writes under.
80
+ *
81
+ * A blank name used to reach `path.resolve(folder, "")`, which is the folder
82
+ * itself: the file landed *beside* the workspace, named after it, with no
83
+ * extension. And a name typed without one ("render") produced a file no image
84
+ * viewer would open. Both are answered here rather than in each node.
85
+ */
86
+ export function saveFilename(opts) {
87
+ const name = opts.filename.trim() || opts.fallback.trim();
88
+ const ext = opts.extension;
89
+ if (!ext)
90
+ return name;
91
+ const dot = name.lastIndexOf(".");
92
+ const hasExtension = dot > 0 && dot < name.length - 1;
93
+ return hasExtension ? name : `${name}${ext}`;
94
+ }
95
+ /**
96
+ * Return a path nothing occupies: `target` itself when free, otherwise
97
+ * `name_1.ext`, `name_2.ext`, … in the same directory.
98
+ */
99
+ export async function uniqueFilePath(target) {
100
+ const fs = await loadNodeFsPromises();
101
+ const path = await loadNodePath();
102
+ const exists = async (p) => {
103
+ try {
104
+ await fs.access(p);
105
+ return true;
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ };
111
+ if (!(await exists(target)))
112
+ return target;
113
+ const dir = path.dirname(target);
114
+ const ext = path.extname(target);
115
+ const base = path.basename(target, ext);
116
+ for (let i = 1;; i++) {
117
+ const candidate = path.join(dir, `${base}_${i}${ext}`);
118
+ if (!(await exists(candidate)))
119
+ return candidate;
120
+ }
121
+ }
122
+ /**
123
+ * Refuse a blank name instead of resolving it to the destination folder.
124
+ *
125
+ * Every save node supplies a fallback via {@link saveFilename}; one that
126
+ * forgets must fail loudly rather than write a file over the folder's own
127
+ * name, one level up from where the user asked for it.
128
+ */
129
+ function requireFilename(filename) {
130
+ const name = filename.trim();
131
+ if (!name) {
132
+ throw new Error("This save node resolved to an empty filename. Set a filename on the " +
133
+ "node.");
134
+ }
135
+ return name;
136
+ }
137
+ /**
138
+ * Resolve folder + filename into an absolute path ready to write to: the
139
+ * directory exists when this returns, and the name does not collide unless the
140
+ * caller asked to overwrite.
141
+ */
142
+ export async function resolveSaveTarget(opts) {
143
+ const fs = await loadNodeFsPromises();
144
+ const path = await loadNodePath();
145
+ const folder = resolveSaveFolder(opts);
146
+ const target = path.resolve(folder, requireFilename(opts.filename));
147
+ await fs.mkdir(path.dirname(target), { recursive: true });
148
+ return opts.overwrite === true ? target : uniqueFilePath(target);
149
+ }
150
+ /**
151
+ * Write a saved file where the node's properties say it goes, and report the
152
+ * path it landed on.
153
+ *
154
+ * With the workspace toggle on this writes through the workspace, so the same
155
+ * node produces a file in the user's folder locally and an object in their
156
+ * workspace prefix in the cloud. The name is deduplicated the same way in both
157
+ * cases — a run that saves twice in one second must not silently overwrite its
158
+ * first output.
159
+ *
160
+ * With the toggle off the node is naming a host folder, which only exists on a
161
+ * local install; a cloud run has no such folder and says so rather than writing
162
+ * somewhere the user will never find.
163
+ */
164
+ export async function writeSavedFile(opts) {
165
+ const workspace = opts.workspace ?? null;
166
+ const isVirtual = workspace !== null && workspace.localDir === null;
167
+ if (opts.saveToWorkspace === true && workspace) {
168
+ if (isVirtual) {
169
+ const filename = requireFilename(opts.filename);
170
+ const target = opts.overwrite === true
171
+ ? filename
172
+ : await uniqueWorkspacePath(workspace, filename);
173
+ await workspace.write(target, opts.bytes);
174
+ return target;
175
+ }
176
+ // A local workspace is a real folder, so the file goes there by path —
177
+ // which is what a user browsing their workspace on disk expects.
178
+ }
179
+ else if (isVirtual) {
180
+ throw new Error("This node writes to a folder on the machine running the workflow, and " +
181
+ "this run's workspace is cloud storage with no such folder. Turn on " +
182
+ '"Save to workspace" to write into the workspace instead.');
183
+ }
184
+ const fs = await loadNodeFsPromises();
185
+ const target = await resolveSaveTarget(opts);
186
+ await fs.writeFile(target, opts.bytes);
187
+ return target;
188
+ }
189
+ /** `name.ext`, `name_1.ext`, … — the first one the workspace does not hold. */
190
+ async function uniqueWorkspacePath(workspace, filename) {
191
+ if (!(await workspace.exists(filename)))
192
+ return filename;
193
+ const dot = filename.lastIndexOf(".");
194
+ const base = dot > 0 ? filename.slice(0, dot) : filename;
195
+ const ext = dot > 0 ? filename.slice(dot) : "";
196
+ for (let i = 1;; i++) {
197
+ const candidate = `${base}_${i}${ext}`;
198
+ if (!(await workspace.exists(candidate)))
199
+ return candidate;
200
+ }
201
+ }
202
+ //# sourceMappingURL=save-target.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"save-target.js","sourceRoot":"","sources":["../src/save-target.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Shared `{{ variable }}` / `{variable}` template substitution.
3
+ *
4
+ * Used by the Prompt and Format Text nodes and by the Agent node so they all
5
+ * resolve placeholders the same way:
6
+ * - `{{ name }}` and `{name}` are replaced with the matching variable value.
7
+ * - `{{ name|filter1|filter2 }}` applies Jinja-style filters in order
8
+ * (upper, lower, capitalize, title, trim, truncate(n), default(val)).
9
+ * - Placeholders whose variable is absent are left intact.
10
+ */
11
+ /**
12
+ * Names of the variables a template references via `{{ name }}` /
13
+ * `{{ name|filter }}` / `{name}`. De-duplicated, in first-seen order. Used by
14
+ * the Prompt node to know which variable channels to await before rendering.
15
+ */
16
+ export declare function referencedVariables(template: string): string[];
17
+ /**
18
+ * One template variable's value. The renderer substitutes `String(value ?? "")`
19
+ * and then runs string filters over the result, so the contract is "a scalar
20
+ * with a meaningful string form"; `null` and `undefined` render as the empty
21
+ * string. Callers holding a richer value stringify it at their own boundary,
22
+ * where they know what its string form should be — the renderer's
23
+ * `[object Object]` is never that.
24
+ */
25
+ export type TemplateValue = string | number | boolean | null | undefined;
26
+ /** The variable bag `renderTemplate` substitutes from. */
27
+ export type TemplateVars = Record<string, TemplateValue>;
28
+ /**
29
+ * Substitute `{{ variable }}` / `{variable}` placeholders in `template` with
30
+ * values from `vars`. Unknown placeholders are left intact.
31
+ */
32
+ export declare function renderTemplate(template: string, vars: TemplateVars): string;
33
+ //# sourceMappingURL=template.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../src/template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAoCH;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAe9D;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;AAEzE,0DAA0D;AAC1D,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAEzD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,MAAM,CA6B3E"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Shared `{{ variable }}` / `{variable}` template substitution.
3
+ *
4
+ * Used by the Prompt and Format Text nodes and by the Agent node so they all
5
+ * resolve placeholders the same way:
6
+ * - `{{ name }}` and `{name}` are replaced with the matching variable value.
7
+ * - `{{ name|filter1|filter2 }}` applies Jinja-style filters in order
8
+ * (upper, lower, capitalize, title, trim, truncate(n), default(val)).
9
+ * - Placeholders whose variable is absent are left intact.
10
+ */
11
+ function toTitleCase(value) {
12
+ return value.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase());
13
+ }
14
+ function applyFilter(value, filter) {
15
+ const trimmed = filter.trim();
16
+ if (trimmed === "upper")
17
+ return value.toUpperCase();
18
+ if (trimmed === "lower")
19
+ return value.toLowerCase();
20
+ if (trimmed === "capitalize") {
21
+ return value.length === 0
22
+ ? ""
23
+ : value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
24
+ }
25
+ if (trimmed === "title")
26
+ return toTitleCase(value);
27
+ if (trimmed === "trim")
28
+ return value.trim();
29
+ const truncateMatch = trimmed.match(/^truncate\((\d+)\)$/);
30
+ if (truncateMatch) {
31
+ const n = Number(truncateMatch[1]);
32
+ return value.length <= n ? value : value.slice(0, n) + "...";
33
+ }
34
+ const defaultMatch = trimmed.match(/^default\((.+)\)$/);
35
+ if (defaultMatch) {
36
+ return value === "" ? defaultMatch[1].replace(/^['"]|['"]$/g, "") : value;
37
+ }
38
+ return value;
39
+ }
40
+ function applyFilters(value, filters) {
41
+ return filters.reduce((result, f) => applyFilter(result, f), value);
42
+ }
43
+ /**
44
+ * Names of the variables a template references via `{{ name }}` /
45
+ * `{{ name|filter }}` / `{name}`. De-duplicated, in first-seen order. Used by
46
+ * the Prompt node to know which variable channels to await before rendering.
47
+ */
48
+ export function referencedVariables(template) {
49
+ const names = new Set();
50
+ // {{ name|filter... }} — leading identifier before any filter.
51
+ for (const match of template.matchAll(/\{\{([^{}]+?)\}\}/g)) {
52
+ const name = match[1].split("|")[0].trim();
53
+ if (name)
54
+ names.add(name);
55
+ }
56
+ // {name} — single brace, not adjacent to another brace.
57
+ for (const match of template.matchAll(/(?<!\{)\{([^{}]+?)\}(?!\})/g)) {
58
+ const name = match[1].trim();
59
+ // Skip anything that looks like a filter expression; single-brace refs are
60
+ // bare names only.
61
+ if (name && !name.includes("|"))
62
+ names.add(name);
63
+ }
64
+ return [...names];
65
+ }
66
+ /**
67
+ * Substitute `{{ variable }}` / `{variable}` placeholders in `template` with
68
+ * values from `vars`. Unknown placeholders are left intact.
69
+ */
70
+ export function renderTemplate(template, vars) {
71
+ let result = template;
72
+ // {{ var|filter1|filter2 }}. The content class excludes both braces so the
73
+ // lazy repetition can't scan across `{`/`}` runs — without that, inputs like
74
+ // `{{{{{{...` give quadratic backtracking (CodeQL polynomial-ReDoS). A
75
+ // placeholder expression never legitimately contains a brace. Surrounding
76
+ // whitespace is handled by the per-part trim() below.
77
+ result = result.replace(/\{\{([^{}]+?)\}\}/g, (match, expr) => {
78
+ const parts = expr.split("|").map((p) => p.trim());
79
+ const varName = parts[0];
80
+ if (varName in vars) {
81
+ const strValue = String(vars[varName] ?? "");
82
+ return parts.length > 1
83
+ ? applyFilters(strValue, parts.slice(1))
84
+ : strValue;
85
+ }
86
+ return match;
87
+ });
88
+ // {var} (single brace, no filters)
89
+ for (const [key, value] of Object.entries(vars)) {
90
+ const strValue = String(value ?? "");
91
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
92
+ const single = new RegExp(`(?<!\\{)\\{${escapedKey}\\}(?!\\})`, "g");
93
+ result = result.replace(single, () => strValue);
94
+ }
95
+ return result;
96
+ }
97
+ //# sourceMappingURL=template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.js","sourceRoot":"","sources":["../src/template.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@nodetool-ai/nodes-utils",
3
+ "version": "0.8.1",
4
+ "description": "Shared helpers for node packages: platform tags and lazy Node-only module loaders",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.0.0"
8
+ },
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "scripts": {
12
+ "build": "node ../../scripts/build-typescript-workspace.mjs",
13
+ "test": "node ../../scripts/run-vitest.mjs run",
14
+ "test:watch": "node ../../scripts/run-vitest.mjs",
15
+ "lint": "node ../../scripts/run-tsc.mjs --noEmit"
16
+ },
17
+ "dependencies": {
18
+ "@nodetool-ai/config": "^0.8.1",
19
+ "@nodetool-ai/node-sdk": "^0.8.1",
20
+ "@nodetool-ai/protocol": "^0.8.1",
21
+ "@nodetool-ai/runtime": "^0.8.1"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.0.0",
25
+ "typescript": "npm:@typescript/typescript6@6.0.2",
26
+ "vitest": "^5.0.1"
27
+ },
28
+ "exports": {
29
+ ".": {
30
+ "nodetool-dev": "./src/index.ts",
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.js",
33
+ "default": "./dist/index.js"
34
+ }
35
+ },
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/nodetool-ai/nodetool.git",
40
+ "directory": "packages/nodes-utils"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "README.md"
48
+ ],
49
+ "homepage": "https://nodetool.ai",
50
+ "bugs": {
51
+ "url": "https://github.com/nodetool-ai/nodetool/issues"
52
+ },
53
+ "keywords": [
54
+ "nodetool",
55
+ "ai",
56
+ "workflow",
57
+ "workflow-automation",
58
+ "typescript",
59
+ "utilities",
60
+ "helpers",
61
+ "platform"
62
+ ]
63
+ }