@nodetool-ai/agents 0.8.4-a24.2 → 0.8.4
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/dist/.nodetool-build-stamp +1 -1
- package/dist/_skills/LICENSE-iart-motion-design-skills +21 -0
- package/dist/_skills/README.md +211 -0
- package/dist/_skills/api-agents/SKILL.md +58 -0
- package/dist/_skills/api-apps/SKILL.md +54 -0
- package/dist/_skills/api-assets/SKILL.md +66 -0
- package/dist/_skills/api-collections/SKILL.md +33 -0
- package/dist/_skills/api-games/SKILL.md +81 -0
- package/dist/_skills/api-media/SKILL.md +131 -0
- package/dist/_skills/api-memory/SKILL.md +72 -0
- package/dist/_skills/api-models/SKILL.md +78 -0
- package/dist/_skills/api-scripts/SKILL.md +79 -0
- package/dist/_skills/api-settings/SKILL.md +49 -0
- package/dist/_skills/api-sketches/SKILL.md +61 -0
- package/dist/_skills/api-storyboards/SKILL.md +94 -0
- package/dist/_skills/api-timelines/SKILL.md +151 -0
- package/dist/_skills/api-web/SKILL.md +57 -0
- package/dist/_skills/api-workflows/SKILL.md +123 -0
- package/dist/_skills/beat-sync-editing/SKILL.md +208 -0
- package/dist/_skills/caption-titles/SKILL.md +94 -0
- package/dist/_skills/color-motion/SKILL.md +185 -0
- package/dist/_skills/commercial-beat-sheet/SKILL.md +338 -0
- package/dist/_skills/elevenlabs-audio-prompting/SKILL.md +163 -0
- package/dist/_skills/explainer-storyboard/SKILL.md +63 -0
- package/dist/_skills/flux-2-klein-prompting/SKILL.md +107 -0
- package/dist/_skills/frame-composition/SKILL.md +291 -0
- package/dist/_skills/game-direction/SKILL.md +99 -0
- package/dist/_skills/gpt-image-2-prompting/SKILL.md +161 -0
- package/dist/_skills/hailuo-prompting/SKILL.md +107 -0
- package/dist/_skills/kling-video-prompting/SKILL.md +188 -0
- package/dist/_skills/launch-commercial/SKILL.md +267 -0
- package/dist/_skills/launch-kit/SKILL.md +120 -0
- package/dist/_skills/logo-reveal/SKILL.md +225 -0
- package/dist/_skills/manimgx/SKILL.md +123 -0
- package/dist/_skills/minimax-h3-prompting/SKILL.md +134 -0
- package/dist/_skills/motion-ad/SKILL.md +281 -0
- package/dist/_skills/motion-background/SKILL.md +145 -0
- package/dist/_skills/motion-curves/SKILL.md +302 -0
- package/dist/_skills/motion-direction/SKILL.md +259 -0
- package/dist/_skills/motion-graphics/SKILL.md +217 -0
- package/dist/_skills/motion-principles/SKILL.md +255 -0
- package/dist/_skills/music-video-treatment/SKILL.md +40 -0
- package/dist/_skills/nano-banana-pro-prompting/SKILL.md +133 -0
- package/dist/_skills/native-game/SKILL.md +310 -0
- package/dist/_skills/native-game-reference/SKILL.md +879 -0
- package/dist/_skills/nodetool-3d-scene/SKILL.md +110 -0
- package/dist/_skills/nodetool-api-reference/SKILL.md +338 -0
- package/dist/_skills/nodetool-app-builder/SKILL.md +291 -0
- package/dist/_skills/nodetool-browser-agent/SKILL.md +119 -0
- package/dist/_skills/nodetool-chat-cli/SKILL.md +184 -0
- package/dist/_skills/nodetool-custom-node-developer/SKILL.md +294 -0
- package/dist/_skills/nodetool-deployment/SKILL.md +195 -0
- package/dist/_skills/nodetool-js-scripting/SKILL.md +223 -0
- package/dist/_skills/nodetool-model-provider-config/SKILL.md +348 -0
- package/dist/_skills/nodetool-rag-indexing/SKILL.md +153 -0
- package/dist/_skills/nodetool-sketch/SKILL.md +138 -0
- package/dist/_skills/nodetool-skill-author/SKILL.md +125 -0
- package/dist/_skills/nodetool-troubleshooter/SKILL.md +370 -0
- package/dist/_skills/nodetool-video-post/SKILL.md +149 -0
- package/dist/_skills/nodetool-workflow-builder/SKILL.md +295 -0
- package/dist/_skills/product-commercial/SKILL.md +132 -0
- package/dist/_skills/qwen-image-prompting/SKILL.md +122 -0
- package/dist/_skills/script-video/SKILL.md +106 -0
- package/dist/_skills/seedance-2-prompting/SKILL.md +194 -0
- package/dist/_skills/seedream-prompting/SKILL.md +117 -0
- package/dist/_skills/short-film/SKILL.md +123 -0
- package/dist/_skills/stable-audio-prompting/SKILL.md +130 -0
- package/dist/_skills/storyboard-core/SKILL.md +716 -0
- package/dist/_skills/timeline-edit-ops/SKILL.md +736 -0
- package/dist/_skills/trailer-template/SKILL.md +291 -0
- package/dist/_skills/ugc-video/SKILL.md +186 -0
- package/dist/_skills/veo-3-prompting/SKILL.md +140 -0
- package/dist/_skills/video-audio-continuity/SKILL.md +90 -0
- package/dist/_skills/video-clone/SKILL.md +96 -0
- package/dist/_skills/video-workflow/SKILL.md +119 -0
- package/dist/_skills/wan-2-6-prompting/SKILL.md +107 -0
- package/dist/app-build/bridge.d.ts.map +1 -1
- package/dist/app-build/bridge.js +44 -17
- package/dist/app-build/bridge.js.map +1 -1
- package/dist/capabilities/apps.d.ts.map +1 -1
- package/dist/capabilities/apps.js +55 -13
- package/dist/capabilities/apps.js.map +1 -1
- package/dist/capabilities/apps.specs.d.ts.map +1 -1
- package/dist/capabilities/apps.specs.js +5 -3
- package/dist/capabilities/apps.specs.js.map +1 -1
- package/dist/capabilities/game-asset-browser.d.ts.map +1 -1
- package/dist/capabilities/game-asset-browser.js +6 -1
- package/dist/capabilities/game-asset-browser.js.map +1 -1
- package/dist/capabilities/game.d.ts +1 -1
- package/dist/capabilities/game.d.ts.map +1 -1
- package/dist/capabilities/game.js +71 -56
- package/dist/capabilities/game.js.map +1 -1
- package/dist/capabilities/game.specs.js +1 -1
- package/dist/capabilities/game.specs.js.map +1 -1
- package/dist/capabilities/game3d.d.ts.map +1 -1
- package/dist/capabilities/game3d.js +14 -19
- package/dist/capabilities/game3d.js.map +1 -1
- package/dist/capabilities/gameAssertions.d.ts +52 -0
- package/dist/capabilities/gameAssertions.d.ts.map +1 -0
- package/dist/capabilities/gameAssertions.js +32 -0
- package/dist/capabilities/gameAssertions.js.map +1 -0
- package/dist/capabilities/models.d.ts.map +1 -1
- package/dist/capabilities/models.js +9 -0
- package/dist/capabilities/models.js.map +1 -1
- package/dist/capabilities/storyboards.d.ts.map +1 -1
- package/dist/capabilities/storyboards.js +36 -11
- package/dist/capabilities/storyboards.js.map +1 -1
- package/dist/capabilities/storyboards.specs.d.ts.map +1 -1
- package/dist/capabilities/storyboards.specs.js +11 -1
- package/dist/capabilities/storyboards.specs.js.map +1 -1
- package/dist/capabilities/workflows.d.ts.map +1 -1
- package/dist/capabilities/workflows.js +1 -0
- package/dist/capabilities/workflows.js.map +1 -1
- package/dist/capabilities/workflows.specs.d.ts.map +1 -1
- package/dist/capabilities/workflows.specs.js +4 -3
- package/dist/capabilities/workflows.specs.js.map +1 -1
- package/dist/evals/surfaces/game.d.ts.map +1 -1
- package/dist/evals/surfaces/game.js +29 -1
- package/dist/evals/surfaces/game.js.map +1 -1
- package/dist/game-authoring.d.ts +120 -0
- package/dist/game-authoring.d.ts.map +1 -1
- package/dist/game-build-integrity.d.ts +12 -0
- package/dist/game-build-integrity.d.ts.map +1 -0
- package/dist/game-build-integrity.js +78 -0
- package/dist/game-build-integrity.js.map +1 -0
- package/dist/game-examples.d.ts +44 -3
- package/dist/game-examples.d.ts.map +1 -1
- package/dist/game-examples.js +60 -10
- package/dist/game-examples.js.map +1 -1
- package/dist/system-skills.d.ts +1 -1
- package/dist/system-skills.js +1 -1
- package/dist/tools/builtin-tools.d.ts.map +1 -1
- package/dist/tools/builtin-tools.js +2 -0
- package/dist/tools/builtin-tools.js.map +1 -1
- package/package.json +19 -19
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodetool-custom-node-developer
|
|
3
|
+
description: "Create or update TypeScript NodeTool nodes and node packs in a NodeTool checkout or an npm package: decorated properties, process methods, streaming, registration. Needs a shell. For custom logic inside the product, use nodetool-js-scripting."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are a NodeTool node developer. You create TypeScript nodes that extend `BaseNode` from `@nodetool-ai/node-sdk`.
|
|
7
|
+
|
|
8
|
+
This is shell work: writing TypeScript, building and installing an npm pack,
|
|
9
|
+
and restarting the server. The in-product agent cannot do it. Inside the
|
|
10
|
+
product, custom logic is a Code node or a saved JS script in the QuickJS
|
|
11
|
+
sandbox (`nodetool-js-scripting`).
|
|
12
|
+
|
|
13
|
+
# Package Layout
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
my-nodes/
|
|
17
|
+
├── package.json # depends on @nodetool-ai/node-sdk
|
|
18
|
+
├── tsconfig.json # extends ../../tsconfig.base.json
|
|
19
|
+
├── src/
|
|
20
|
+
│ ├── index.ts # exports registration function + ALL_NODES array
|
|
21
|
+
│ └── nodes/
|
|
22
|
+
│ ├── category-a.ts # node classes
|
|
23
|
+
│ └── category-b.ts
|
|
24
|
+
└── tests/
|
|
25
|
+
└── nodes.test.ts
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
# Node Template
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { BaseNode, prop } from "@nodetool-ai/node-sdk";
|
|
32
|
+
|
|
33
|
+
export class MyNode extends BaseNode {
|
|
34
|
+
// REQUIRED static fields
|
|
35
|
+
static readonly nodeType = "mypack.category.NodeName"; // namespace.category.Name
|
|
36
|
+
static readonly title = "Human-Readable Title";
|
|
37
|
+
static readonly description = "Brief description.\n search, keywords, here";
|
|
38
|
+
static readonly metadataOutputTypes = { output: "str" }; // declare all outputs
|
|
39
|
+
|
|
40
|
+
// Inputs via @prop decorator
|
|
41
|
+
@prop({ type: "str", default: "", title: "Input Text" })
|
|
42
|
+
declare input_text: any;
|
|
43
|
+
|
|
44
|
+
@prop({ type: "int", default: 10, min: 1, max: 100, title: "Count" })
|
|
45
|
+
declare count: any;
|
|
46
|
+
|
|
47
|
+
// Process method — MUST return all keys from metadataOutputTypes
|
|
48
|
+
async process(): Promise<Record<string, unknown>> {
|
|
49
|
+
const text = String(this.input_text ?? "");
|
|
50
|
+
const count = Number(this.count ?? 10);
|
|
51
|
+
return { output: text.repeat(count) };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
# Input Resolution Pattern
|
|
57
|
+
|
|
58
|
+
Property values **and** connected input values are assigned to instance fields
|
|
59
|
+
*before* `process()` is called, so always read inputs from `this`:
|
|
60
|
+
```typescript
|
|
61
|
+
const value = this.field ?? defaultValue;
|
|
62
|
+
```
|
|
63
|
+
- `this.field` — resolved value (connection value if connected, else the
|
|
64
|
+
user-set property, else the `@prop` default)
|
|
65
|
+
- `defaultValue` — extra in-code fallback for `null`/`undefined`
|
|
66
|
+
|
|
67
|
+
Do **not** declare a `process(inputs: Record<string, unknown>)` parameter — the
|
|
68
|
+
runtime calls `process(context?: ProcessingContext)`, so an `inputs` parameter
|
|
69
|
+
typed that way fails strict type-checking.
|
|
70
|
+
|
|
71
|
+
# @prop Decorator Options
|
|
72
|
+
|
|
73
|
+
| Option | Type | Notes |
|
|
74
|
+
|--------|------|-------|
|
|
75
|
+
| `type` | string | **Required**: `"int"`, `"float"`, `"str"`, `"bool"`, `"list[any]"`, `"image"`, `"audio"`, `"video"`, `"document"`, `"dataframe"`, `"enum"`, `"any"` |
|
|
76
|
+
| `default` | unknown | Default value |
|
|
77
|
+
| `title` | string | Display name in UI |
|
|
78
|
+
| `description` | string | Tooltip text |
|
|
79
|
+
| `min` / `max` | number | Numeric bounds |
|
|
80
|
+
| `required` | boolean | Validation flag |
|
|
81
|
+
| `values` | (string\|number)[] | Enum options |
|
|
82
|
+
|
|
83
|
+
# Patterns
|
|
84
|
+
|
|
85
|
+
## Multi-Output Node
|
|
86
|
+
```typescript
|
|
87
|
+
static readonly metadataOutputTypes = { text: "str", confidence: "float" };
|
|
88
|
+
|
|
89
|
+
async process(): Promise<Record<string, unknown>> {
|
|
90
|
+
return { text: "result", confidence: 0.95 }; // ALL keys must be returned
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Streaming Output (genProcess)
|
|
95
|
+
|
|
96
|
+
Streaming is structural. Overriding `genProcess` makes the node emit many
|
|
97
|
+
values, and `outputCorrelation` says how each output relates to the run:
|
|
98
|
+
`iteration` for one value per yield, `single` for one final value. There is no
|
|
99
|
+
streaming flag.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import type { OutputCorrelation } from "@nodetool-ai/protocol";
|
|
103
|
+
|
|
104
|
+
static readonly metadataOutputTypes = { output: "any", index: "int" };
|
|
105
|
+
static readonly outputCorrelation: Record<string, OutputCorrelation> = {
|
|
106
|
+
output: { kind: "iteration", source: "__execution__", group: "items" },
|
|
107
|
+
index: { kind: "iteration", source: "__execution__", group: "items" }
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
@prop({ type: "list[any]", default: [], title: "Input List" })
|
|
111
|
+
declare input_list: unknown[];
|
|
112
|
+
|
|
113
|
+
async process(): Promise<Record<string, unknown>> {
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async *genProcess(): AsyncGenerator<Record<string, unknown>> {
|
|
118
|
+
const items = this.input_list ?? [];
|
|
119
|
+
for (let i = 0; i < items.length; i++) {
|
|
120
|
+
yield { output: items[i], index: i };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
A `chunk` output must come from `genProcess` with an `iteration` correlation,
|
|
126
|
+
or it stays empty.
|
|
127
|
+
|
|
128
|
+
## Streaming Input (collector)
|
|
129
|
+
|
|
130
|
+
A node that consumes a stream sets `isStreamingInput` and implements `run()`,
|
|
131
|
+
which reads the input stream and emits when it ends. `process()` alone fires
|
|
132
|
+
once with buffered inputs and collects nothing across a stream.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import type { StreamingInputs, StreamingOutputs } from "@nodetool-ai/node-sdk";
|
|
136
|
+
import type { InputMode, OutputCorrelation } from "@nodetool-ai/protocol";
|
|
137
|
+
|
|
138
|
+
static readonly metadataOutputTypes = { output: "list[any]" };
|
|
139
|
+
static readonly isStreamingInput = true;
|
|
140
|
+
static readonly inputMode: InputMode = "stream";
|
|
141
|
+
static readonly outputCorrelation: Record<string, OutputCorrelation> = {
|
|
142
|
+
output: { kind: "aggregate", source: "input_item", collapse: "innermost" }
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
@prop({ type: "any", default: [], title: "Input Item" })
|
|
146
|
+
declare input_item: unknown;
|
|
147
|
+
|
|
148
|
+
async process(): Promise<Record<string, unknown>> {
|
|
149
|
+
return { output: [] };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async run(inputs: StreamingInputs, outputs: StreamingOutputs): Promise<void> {
|
|
153
|
+
const items: unknown[] = [];
|
|
154
|
+
for await (const item of inputs.stream("input_item")) {
|
|
155
|
+
items.push(item);
|
|
156
|
+
}
|
|
157
|
+
await outputs.emit("output", items);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Enum Input
|
|
162
|
+
```typescript
|
|
163
|
+
@prop({ type: "enum", values: ["low", "medium", "high"], default: "medium", title: "Quality" })
|
|
164
|
+
declare quality: any;
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Secrets / API Keys
|
|
168
|
+
```typescript
|
|
169
|
+
static readonly requiredSettings = ["MY_API_KEY"];
|
|
170
|
+
|
|
171
|
+
async process(): Promise<Record<string, unknown>> {
|
|
172
|
+
// Resolved secrets are available via the `_secrets` getter.
|
|
173
|
+
const apiKey = this._secrets.MY_API_KEY || process.env.MY_API_KEY;
|
|
174
|
+
if (!apiKey) throw new Error("MY_API_KEY not configured");
|
|
175
|
+
// use apiKey...
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Media Refs (Image/Audio/Video)
|
|
180
|
+
|
|
181
|
+
A ref may carry inline `data`, or only a `uri` (`asset://`, `file://`, storage,
|
|
182
|
+
`http`). Read bytes with `loadMediaRefBytes`, which resolves every form. Return
|
|
183
|
+
a ref with its `type` discriminator and raw bytes or raw base64 in `data`,
|
|
184
|
+
never a `data:` URI.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { loadMediaRefBytes, type ProcessingContext } from "@nodetool-ai/runtime";
|
|
188
|
+
|
|
189
|
+
@prop({ type: "image", title: "Input Image" })
|
|
190
|
+
declare image: { uri?: string; data?: string | Uint8Array; asset_id?: string } | undefined;
|
|
191
|
+
|
|
192
|
+
async process(context?: ProcessingContext): Promise<Record<string, unknown>> {
|
|
193
|
+
if (!this.image) throw new Error("No image provided");
|
|
194
|
+
const bytes = await loadMediaRefBytes(this.image, context);
|
|
195
|
+
if (!bytes) throw new Error("Image has no readable bytes");
|
|
196
|
+
const result: Uint8Array = await processImage(bytes);
|
|
197
|
+
return { output: { type: "image", data: result, content_type: "image/png" } };
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Lifecycle Hooks
|
|
202
|
+
```typescript
|
|
203
|
+
async initialize(): Promise<void> { } // once at start of run
|
|
204
|
+
async preProcess(): Promise<void> { } // before each process() call
|
|
205
|
+
async finalize(): Promise<void> { } // after all processing
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
# Optional Static Properties
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
static readonly supportsDynamicInputs = true; // user can add input slots
|
|
212
|
+
static readonly supportsDynamicOutputs = true; // user can add output slots
|
|
213
|
+
static readonly inputFields = ["prompt"]; // shown as handles on the node body
|
|
214
|
+
static readonly inlineFields = ["model"]; // edited inline on the node
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Props in neither list appear only in the Inspector.
|
|
218
|
+
|
|
219
|
+
# Scaffold a Pack
|
|
220
|
+
|
|
221
|
+
The fastest start is the scaffolder, which generates a self-contained pack
|
|
222
|
+
(package.json with the `nodetool` manifest field, tsconfig, an example node, a
|
|
223
|
+
test, and a README):
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
npm run create:pack -- @myorg/my-nodes # in the nodetool repo
|
|
227
|
+
# or directly: node scripts/create-pack.mjs @myorg/my-nodes ./my-nodes
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
# Registration
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
// src/index.ts
|
|
234
|
+
import type { NodeClass, NodeRegistry } from "@nodetool-ai/node-sdk";
|
|
235
|
+
import { MyNode, OtherNode } from "./nodes/my-nodes.js";
|
|
236
|
+
|
|
237
|
+
export const ALL_NODES: readonly NodeClass[] = [MyNode, OtherNode];
|
|
238
|
+
|
|
239
|
+
export function register(registry: NodeRegistry): void {
|
|
240
|
+
for (const nodeClass of ALL_NODES) {
|
|
241
|
+
registry.register(nodeClass);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The server auto-loads packs — no need to edit its source. Mark the package as a
|
|
247
|
+
pack with a `nodetool` field in `package.json`, naming the export above:
|
|
248
|
+
|
|
249
|
+
```jsonc
|
|
250
|
+
{
|
|
251
|
+
"name": "@myorg/my-nodes",
|
|
252
|
+
"main": "dist/index.js",
|
|
253
|
+
"nodetool": { "apiVersion": 1, "register": "register" }
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
On startup the server scans installed dependencies, imports any package with a
|
|
258
|
+
`nodetool` field, and calls the named export with the registry. Install the
|
|
259
|
+
built pack where the server can resolve it (`npm install <pack>`, or `npm link`
|
|
260
|
+
for local dev) and restart. The export may be sync or `async`.
|
|
261
|
+
|
|
262
|
+
**Trust model.** Custom nodes run in-process as the server user (full
|
|
263
|
+
filesystem/network/secret access, no sandbox), so loading is gated. In
|
|
264
|
+
development unlisted packs load automatically. A production server
|
|
265
|
+
(`NODETOOL_ENV=production`) and the desktop app
|
|
266
|
+
(`NODETOOL_PACKS_REQUIRE_ALLOWLIST=1`) load only packs on the allowlist
|
|
267
|
+
(`NODETOOL_PACKS_ALLOWLIST` or `~/.config/nodetool/packs.json`). An unlisted
|
|
268
|
+
pack is skipped, so on Desktop add it to the allowlist before restarting. Packs also
|
|
269
|
+
cannot register under reserved namespaces (`nodetool.`, `lib.`, provider names)
|
|
270
|
+
or shadow an existing node type. Only install packs you trust.
|
|
271
|
+
|
|
272
|
+
# Testing
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { describe, it, expect } from "vitest";
|
|
276
|
+
import { MyNode } from "../src/nodes/my-nodes.js";
|
|
277
|
+
|
|
278
|
+
describe("MyNode", () => {
|
|
279
|
+
it("processes input", async () => {
|
|
280
|
+
// Constructor assigns properties to instance fields.
|
|
281
|
+
const node = new MyNode({ input_text: "hello", count: 3 });
|
|
282
|
+
const result = await node.process();
|
|
283
|
+
expect(result.output).toBe("hellohellohello");
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
# Common Pitfalls
|
|
289
|
+
|
|
290
|
+
- **Forgetting output keys**: Every key in `metadataOutputTypes` must appear in the return object
|
|
291
|
+
- **Wrong input access**: Read inputs from `this.field` (assigned before `process()`); do not add an `inputs` parameter to `process()`
|
|
292
|
+
- **nodeType format**: Must be `namespace.category.Name` with dots as separators
|
|
293
|
+
- **Mutable state without initialize()**: If using instance fields, reset them in `initialize()`
|
|
294
|
+
- **Missing secrets declaration**: Add to `requiredSettings` array for proper UI prompting
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodetool-deployment
|
|
3
|
+
description: "Self-host a NodeTool server with Docker Compose or nodetool deploy, rent a Runpod or Vast GPU worker with nodetool worker, or configure server auth. Everything here runs in the user's shell: the in-product agent explains the steps and does not run them."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You help users run NodeTool somewhere other than their desktop. Every command
|
|
7
|
+
below runs in the user's shell. The in-product agent has no shell and no
|
|
8
|
+
deployment tools, so it explains the steps and leaves running them to the user.
|
|
9
|
+
|
|
10
|
+
# Which one
|
|
11
|
+
|
|
12
|
+
| Need | Use |
|
|
13
|
+
|------|-----|
|
|
14
|
+
| A long-lived server that people connect in to | Docker: `docker-compose.yml` from the repository, or `nodetool deploy` for a remote host over SSH |
|
|
15
|
+
| A rented GPU that the user's NodeTool connects out to, billed by the minute | `nodetool worker` on Runpod or Vast |
|
|
16
|
+
|
|
17
|
+
`nodetool deploy` supports one target type, `docker`. There is no SSH-service,
|
|
18
|
+
Runpod-endpoint or Cloud Run deploy type. Runpod and Vast are `nodetool worker`.
|
|
19
|
+
|
|
20
|
+
# Server authentication
|
|
21
|
+
|
|
22
|
+
The server picks its auth mode from `SUPABASE_URL` and `SUPABASE_KEY` alone. It
|
|
23
|
+
reads neither `AUTH_PROVIDER` nor `NODETOOL_SERVER_MODE`.
|
|
24
|
+
|
|
25
|
+
| Mode | Enabled by | Accepted credentials |
|
|
26
|
+
|------|------------|----------------------|
|
|
27
|
+
| Local | Default, with the Supabase variables unset | Loopback connections run as user `"1"` with no token. `NODETOOL_TRUST_LOCALHOST=false` turns that off. Sources in `NODETOOL_TRUST_LOCAL_NETWORKS` (comma-separated CIDRs) also run as user `"1"` with no token. A bearer equal to `SERVER_AUTH_TOKEN` runs as user `"1"`. A token minted by `nodetool deploy users-add` runs as that API user. Anything else gets `401`. |
|
|
28
|
+
| Supabase | `SUPABASE_URL` and `SUPABASE_KEY` both set | Every non-public request needs a valid Supabase JWT. `SERVER_AUTH_TOKEN`, `NODETOOL_TRUST_LOCAL_NETWORKS` and API-user tokens are ignored. Loopback is trusted only with `NODETOOL_TRUST_LOCALHOST=true`. |
|
|
29
|
+
|
|
30
|
+
In both modes, an access token (`ntk_…`) created in **Settings → MCP → Connect
|
|
31
|
+
an agent remotely** authenticates as the person who created it. Send any token
|
|
32
|
+
as `Authorization: Bearer <token>`. A WebSocket client that cannot set headers
|
|
33
|
+
passes `?api_key=<token>` on the `/ws` URL.
|
|
34
|
+
|
|
35
|
+
Docker NATs a published-port connection, so it arrives from the bridge gateway,
|
|
36
|
+
not loopback. In Local mode that means `401` until the source range is in
|
|
37
|
+
`NODETOOL_TRUST_LOCAL_NETWORKS` or the client sends a token. Every listed range
|
|
38
|
+
runs as user `"1"` with no password, so list only networks you control. A
|
|
39
|
+
server that other people reach over the internet needs Supabase mode, with
|
|
40
|
+
`SUPABASE_ANON_KEY` set too so the web login screen works.
|
|
41
|
+
|
|
42
|
+
# Docker Compose (single host)
|
|
43
|
+
|
|
44
|
+
The repository's `docker-compose.yml` is the reference self-host setup.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
cp .env.example .env # fill in the provider keys you use
|
|
48
|
+
docker compose up -d # serves http://localhost:17777
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- It runs in Local mode and trusts Docker's bridge ranges
|
|
52
|
+
(`172.16.0.0/12,192.168.65.0/24`) by default. Safe on a laptop, LAN or VPN.
|
|
53
|
+
Before exposing the port publicly, set `SUPABASE_URL`, `SUPABASE_KEY` and
|
|
54
|
+
`SUPABASE_ANON_KEY` in `.env`.
|
|
55
|
+
- All state (database, assets, vector store, model cache, generated secret
|
|
56
|
+
key) lives on the `nodetool-data` volume at `/workspace`.
|
|
57
|
+
- `NODETOOL_PORT` changes the host port. `NODETOOL_VERSION` pins an image tag.
|
|
58
|
+
|
|
59
|
+
# `nodetool deploy` (remote Docker host)
|
|
60
|
+
|
|
61
|
+
`deployment.yaml` lives in the user config directory, not the working directory.
|
|
62
|
+
Open it with `nodetool deploy edit` rather than guessing the path.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
nodetool deploy init # create deployment.yaml
|
|
66
|
+
nodetool deploy add my-server --type docker # describe one target
|
|
67
|
+
nodetool deploy plan my-server # review before touching the host
|
|
68
|
+
nodetool deploy apply my-server # --dry-run to print the steps only
|
|
69
|
+
nodetool deploy status my-server
|
|
70
|
+
nodetool deploy logs my-server --follow --tail 200
|
|
71
|
+
nodetool deploy list # live state of every target
|
|
72
|
+
nodetool deploy show my-server # config only, no remote call
|
|
73
|
+
nodetool deploy destroy my-server # --force skips the confirmation
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
deployments:
|
|
78
|
+
my-server:
|
|
79
|
+
type: docker
|
|
80
|
+
host: 192.168.1.10 # localhost skips SSH and uses the local Docker
|
|
81
|
+
ssh:
|
|
82
|
+
user: ubuntu
|
|
83
|
+
key_path: ~/.ssh/id_ed25519
|
|
84
|
+
image:
|
|
85
|
+
name: ghcr.io/nodetool-ai/nodetool
|
|
86
|
+
tag: latest
|
|
87
|
+
container:
|
|
88
|
+
name: nodetool-server
|
|
89
|
+
port: 8000 # host port
|
|
90
|
+
gpu: "0" # GPU device ids, or omit
|
|
91
|
+
environment: # extra env, e.g. SUPABASE_URL, provider keys
|
|
92
|
+
OPENAI_API_KEY: "..."
|
|
93
|
+
paths:
|
|
94
|
+
workspace: ~/nodetool_data/workspace
|
|
95
|
+
hf_cache: ~/nodetool_data/hf-cache
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The CLI writes a generated `server_auth_token` and a
|
|
99
|
+
`container.environment.SECRETS_MASTER_KEY` into `deployment.yaml` for any
|
|
100
|
+
target that lacks them. `apply` starts the container with the token as
|
|
101
|
+
`SERVER_AUTH_TOKEN`, so in Local mode a client authenticates with
|
|
102
|
+
`Authorization: Bearer <server_auth_token>`. The container keeps its database,
|
|
103
|
+
assets, vector store and API users under `/workspace`.
|
|
104
|
+
|
|
105
|
+
## Remote workflows and API users
|
|
106
|
+
|
|
107
|
+
These verbs send an admin bearer token: `--token <token>`, else
|
|
108
|
+
`NODETOOL_ADMIN_TOKEN`, else an interactive prompt.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
nodetool deploy workflows sync my-server <workflow_id> # assets and models must already be there
|
|
112
|
+
nodetool deploy workflows list my-server
|
|
113
|
+
nodetool deploy workflows run my-server <workflow_id> -p prompt="a red fox"
|
|
114
|
+
nodetool deploy workflows delete my-server <workflow_id>
|
|
115
|
+
|
|
116
|
+
nodetool deploy users-add my-server alice --role admin # prints alice's token
|
|
117
|
+
nodetool deploy users-list my-server
|
|
118
|
+
nodetool deploy users-reset-token my-server alice
|
|
119
|
+
nodetool deploy users-remove my-server alice
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
API-user tokens work in Local mode only (§ Server authentication).
|
|
123
|
+
|
|
124
|
+
# `nodetool worker` (rented GPU)
|
|
125
|
+
|
|
126
|
+
A worker is a Runpod or Vast instance that the user's NodeTool attaches to for
|
|
127
|
+
GPU nodes. It bills by the minute until stopped. Confirm the GPU type and the
|
|
128
|
+
expected hours with the user before creating one, and always set an auto-stop
|
|
129
|
+
guard.
|
|
130
|
+
|
|
131
|
+
Store the provider key first: `nodetool secrets store RUNPOD_API_KEY` (or
|
|
132
|
+
`VAST_API_KEY`).
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
nodetool worker profile add hf-a40 --target runpod \
|
|
136
|
+
--image ghcr.io/nodetool-ai/nodetool-worker:latest \
|
|
137
|
+
--gpu "NVIDIA A40" --idle-timeout 15 --max-lifetime 240
|
|
138
|
+
nodetool worker create --profile hf-a40 --attach
|
|
139
|
+
|
|
140
|
+
nodetool worker list
|
|
141
|
+
nodetool worker status <instance-id>
|
|
142
|
+
nodetool worker stop <instance-id> # or --all
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- `--idle-timeout <minutes>` stops an idle worker. `--max-lifetime <minutes>` is
|
|
146
|
+
a hard limit. Set both, since a forgotten worker keeps billing.
|
|
147
|
+
- `--disk <gb>` sizes the persistent volume that holds the model cache
|
|
148
|
+
(default 100). `--ssh-key <path.pub>` opens SSH for debugging.
|
|
149
|
+
- `--attach` prints `NODETOOL_WORKER_URL`. For another shell, export
|
|
150
|
+
`NODETOOL_WORKER_TOKEN=$(nodetool worker token <instance-id>)`.
|
|
151
|
+
- `nodetool worker models download --repo-id <owner/name>` pulls weights onto
|
|
152
|
+
the attached worker before a run, so the first run does not pay for the
|
|
153
|
+
download on GPU time. `models list` and `models delete` manage the cache.
|
|
154
|
+
|
|
155
|
+
# Environment
|
|
156
|
+
|
|
157
|
+
| Variable | Purpose |
|
|
158
|
+
|----------|---------|
|
|
159
|
+
| `SUPABASE_URL`, `SUPABASE_KEY` | Turn on Supabase mode. `SUPABASE_KEY` is the service-role key |
|
|
160
|
+
| `SUPABASE_ANON_KEY` | Public key the login screen uses. Without it no user can sign in |
|
|
161
|
+
| `SECRETS_MASTER_KEY` | Encrypts stored secrets. Set it explicitly (`openssl rand -base64 32`). A lost key makes every stored secret unreadable |
|
|
162
|
+
| `NODETOOL_TRUST_LOCAL_NETWORKS` | Local mode only: CIDRs that run as user `"1"` with no token |
|
|
163
|
+
| `NODETOOL_TRUST_LOCALHOST` | Overrides whether loopback bypasses auth |
|
|
164
|
+
| `NODETOOL_ENABLE_MCP=1` | Serves `/mcp` on a production server |
|
|
165
|
+
| `NODETOOL_STORAGE_BACKEND` | `file` (default), `s3` or `supabase`, with `ASSET_BUCKET` and `TEMP_BUCKET` |
|
|
166
|
+
| `DB_PATH`, `ASSET_FOLDER`, `VECTORSTORE_DB_PATH`, `HF_HOME` | Keep these on a persistent volume |
|
|
167
|
+
| `HOST`, `PORT` | Bind address. The image binds `0.0.0.0` |
|
|
168
|
+
| `NODETOOL_LOG_LEVEL` | Log level, default `info` |
|
|
169
|
+
| Provider keys | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `HF_TOKEN`, `FAL_API_KEY` as needed |
|
|
170
|
+
|
|
171
|
+
The image already sets `NODETOOL_ENV=production`. Production mode turns off the
|
|
172
|
+
Python bridge, the file browser and `/mcp` (unless enabled), and loads only node
|
|
173
|
+
packs on the allowlist. Do not set it on a desktop or development install.
|
|
174
|
+
|
|
175
|
+
# Running the server directly
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
nodetool serve # 127.0.0.1:7777
|
|
179
|
+
nodetool serve --host 0.0.0.0 --port 8080
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Health checks: `GET /health` (liveness, no auth) and `GET /ready`.
|
|
183
|
+
|
|
184
|
+
# Common pitfalls
|
|
185
|
+
|
|
186
|
+
- **UI loads but every API call returns `401` in Docker**: the published port
|
|
187
|
+
arrives from the bridge gateway. Add the bridge range to
|
|
188
|
+
`NODETOOL_TRUST_LOCAL_NETWORKS` or send a token.
|
|
189
|
+
- **`SERVER_AUTH_TOKEN` or `NODETOOL_TRUST_LOCAL_NETWORKS` stops working after
|
|
190
|
+
adding Supabase**: Supabase mode ignores both. Clients must sign in or use an
|
|
191
|
+
access token.
|
|
192
|
+
- **Anon key in `SUPABASE_KEY`**: the server needs the service-role key there.
|
|
193
|
+
- **No persistent volume**: the database, assets and secrets key vanish when the
|
|
194
|
+
container is recreated.
|
|
195
|
+
- **Worker left running**: `nodetool worker list`, then `nodetool worker stop --all`.
|