@nodetool-ai/node-sdk 0.7.0-rc.23 → 0.8.2
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 +34 -0
- package/dist/.nodetool-build-stamp +1 -0
- package/dist/base-node.d.ts +285 -14
- package/dist/base-node.d.ts.map +1 -1
- package/dist/base-node.js +359 -49
- package/dist/base-node.js.map +1 -1
- package/dist/class-name-to-title.d.ts +26 -0
- package/dist/class-name-to-title.d.ts.map +1 -0
- package/dist/class-name-to-title.js +59 -0
- package/dist/class-name-to-title.js.map +1 -0
- package/dist/code-analysis.d.ts +234 -0
- package/dist/code-analysis.d.ts.map +1 -0
- package/dist/code-analysis.js +919 -0
- package/dist/code-analysis.js.map +1 -0
- package/dist/code-body.d.ts +61 -0
- package/dist/code-body.d.ts.map +1 -0
- package/dist/code-body.js +288 -0
- package/dist/code-body.js.map +1 -0
- package/dist/code-node-validation.d.ts +67 -0
- package/dist/code-node-validation.d.ts.map +1 -0
- package/dist/code-node-validation.js +540 -0
- package/dist/code-node-validation.js.map +1 -0
- package/dist/content-card.d.ts +30 -0
- package/dist/content-card.d.ts.map +1 -0
- package/dist/content-card.js +68 -0
- package/dist/content-card.js.map +1 -0
- package/dist/correlation-validation.d.ts +19 -0
- package/dist/correlation-validation.d.ts.map +1 -0
- package/dist/correlation-validation.js +101 -0
- package/dist/correlation-validation.js.map +1 -0
- package/dist/cost-estimate.d.ts +244 -0
- package/dist/cost-estimate.d.ts.map +1 -0
- package/dist/cost-estimate.js +637 -0
- package/dist/cost-estimate.js.map +1 -0
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +4 -1
- package/dist/decorators.js.map +1 -1
- package/dist/docs/nodes.d.ts.map +1 -1
- package/dist/docs/nodes.js +3 -2
- package/dist/docs/nodes.js.map +1 -1
- package/dist/docs/overview.d.ts.map +1 -1
- package/dist/docs/overview.js +4 -3
- package/dist/docs/overview.js.map +1 -1
- package/dist/docs/workflows.d.ts.map +1 -1
- package/dist/docs/workflows.js +10 -9
- package/dist/docs/workflows.js.map +1 -1
- package/dist/field-classification.d.ts +31 -0
- package/dist/field-classification.d.ts.map +1 -0
- package/dist/field-classification.js +58 -0
- package/dist/field-classification.js.map +1 -0
- package/dist/graph-validation.d.ts +239 -0
- package/dist/graph-validation.d.ts.map +1 -0
- package/dist/graph-validation.js +1174 -0
- package/dist/graph-validation.js.map +1 -0
- package/dist/index.d.ts +27 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -1
- package/dist/index.js.map +1 -1
- package/dist/js-script-link.d.ts +53 -0
- package/dist/js-script-link.d.ts.map +1 -0
- package/dist/js-script-link.js +58 -0
- package/dist/js-script-link.js.map +1 -0
- package/dist/js-script-materialize.d.ts +52 -0
- package/dist/js-script-materialize.d.ts.map +1 -0
- package/dist/js-script-materialize.js +24 -0
- package/dist/js-script-materialize.js.map +1 -0
- package/dist/manifest-node-values.d.ts +106 -0
- package/dist/manifest-node-values.d.ts.map +1 -0
- package/dist/manifest-node-values.js +195 -0
- package/dist/manifest-node-values.js.map +1 -0
- package/dist/metadata.d.ts +75 -3
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +132 -49
- package/dist/metadata.js.map +1 -1
- package/dist/node-metadata.d.ts.map +1 -1
- package/dist/node-metadata.js +61 -56
- package/dist/node-metadata.js.map +1 -1
- package/dist/node-type-inventory.d.ts +51 -0
- package/dist/node-type-inventory.d.ts.map +1 -0
- package/dist/node-type-inventory.js +121 -0
- package/dist/node-type-inventory.js.map +1 -0
- package/dist/nodes/test-nodes.d.ts +77 -30
- package/dist/nodes/test-nodes.d.ts.map +1 -1
- package/dist/nodes/test-nodes.js +2 -34
- package/dist/nodes/test-nodes.js.map +1 -1
- package/dist/pack-loader.d.ts +202 -0
- package/dist/pack-loader.d.ts.map +1 -0
- package/dist/pack-loader.js +529 -0
- package/dist/pack-loader.js.map +1 -0
- package/dist/port-types.d.ts +15 -0
- package/dist/port-types.d.ts.map +1 -0
- package/dist/port-types.js +72 -0
- package/dist/port-types.js.map +1 -0
- package/dist/pricing-bundle.d.ts +83 -0
- package/dist/pricing-bundle.d.ts.map +1 -0
- package/dist/pricing-bundle.js +82 -0
- package/dist/pricing-bundle.js.map +1 -0
- package/dist/pricing-params.d.ts +59 -0
- package/dist/pricing-params.d.ts.map +1 -0
- package/dist/pricing-params.js +282 -0
- package/dist/pricing-params.js.map +1 -0
- package/dist/python-package-scan.d.ts +8 -0
- package/dist/python-package-scan.d.ts.map +1 -1
- package/dist/python-package-scan.js +21 -8
- package/dist/python-package-scan.js.map +1 -1
- package/dist/registry.d.ts +105 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +403 -30
- package/dist/registry.js.map +1 -1
- package/dist/sandbox-bridge-packs.d.ts +39 -0
- package/dist/sandbox-bridge-packs.d.ts.map +1 -0
- package/dist/sandbox-bridge-packs.js +249 -0
- package/dist/sandbox-bridge-packs.js.map +1 -0
- package/dist/sandbox-catalog-host.d.ts +31 -0
- package/dist/sandbox-catalog-host.d.ts.map +1 -0
- package/dist/sandbox-catalog-host.js +78 -0
- package/dist/sandbox-catalog-host.js.map +1 -0
- package/dist/sandbox-module-catalog.d.ts +14 -0
- package/dist/sandbox-module-catalog.d.ts.map +1 -0
- package/dist/sandbox-module-catalog.js +402 -0
- package/dist/sandbox-module-catalog.js.map +1 -0
- package/dist/sandbox-npm-artifacts.d.ts +56 -0
- package/dist/sandbox-npm-artifacts.d.ts.map +1 -0
- package/dist/sandbox-npm-artifacts.js +17 -0
- package/dist/sandbox-npm-artifacts.js.map +1 -0
- package/dist/sandbox-pack-discovery.d.ts +93 -0
- package/dist/sandbox-pack-discovery.d.ts.map +1 -0
- package/dist/sandbox-pack-discovery.js +807 -0
- package/dist/sandbox-pack-discovery.js.map +1 -0
- package/dist/search.d.ts +29 -1
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +136 -9
- package/dist/search.js.map +1 -1
- package/dist/type-compat.d.ts +36 -0
- package/dist/type-compat.d.ts.map +1 -0
- package/dist/type-compat.js +98 -0
- package/dist/type-compat.js.map +1 -0
- package/dist/type-predicates.d.ts +25 -0
- package/dist/type-predicates.d.ts.map +1 -0
- package/dist/type-predicates.js +31 -0
- package/dist/type-predicates.js.map +1 -0
- package/dist/validation.d.ts +18 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +70 -11
- package/dist/validation.js.map +1 -1
- package/dist/workflow-document-tools.d.ts +35 -0
- package/dist/workflow-document-tools.d.ts.map +1 -0
- package/dist/workflow-document-tools.js +402 -0
- package/dist/workflow-document-tools.js.map +1 -0
- package/dist/workflow-interface.d.ts +62 -0
- package/dist/workflow-interface.d.ts.map +1 -0
- package/dist/workflow-interface.js +351 -0
- package/dist/workflow-interface.js.map +1 -0
- package/package.json +67 -10
- package/dist/package-registry-client.d.ts +0 -12
- package/dist/package-registry-client.d.ts.map +0 -1
- package/dist/package-registry-client.js +0 -40
- package/dist/package-registry-client.js.map +0 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property values for manifest-driven node factories: atlascloud, fal, kie,
|
|
3
|
+
* replicate, together, topaz. Sibling of `classifyFields` in
|
|
4
|
+
* `field-classification.ts`, which the same factories share.
|
|
5
|
+
*/
|
|
6
|
+
import { mapPromptAssetsToInputs } from "@nodetool-ai/runtime";
|
|
7
|
+
/**
|
|
8
|
+
* The default for a declared property whose manifest field carries none.
|
|
9
|
+
*
|
|
10
|
+
* Container types are matched by prefix: the manifests between them name
|
|
11
|
+
* hundreds of element types (`list[Image]`, `list[list[TrackPoint]]`), and the
|
|
12
|
+
* element type never changes what the container starts at.
|
|
13
|
+
*/
|
|
14
|
+
export function defaultForPropType(propType) {
|
|
15
|
+
switch (propType) {
|
|
16
|
+
case "bool":
|
|
17
|
+
return false;
|
|
18
|
+
case "int":
|
|
19
|
+
case "float":
|
|
20
|
+
return 0;
|
|
21
|
+
case "image":
|
|
22
|
+
return {
|
|
23
|
+
type: "image",
|
|
24
|
+
uri: "",
|
|
25
|
+
asset_id: null,
|
|
26
|
+
data: null,
|
|
27
|
+
metadata: null
|
|
28
|
+
};
|
|
29
|
+
case "audio":
|
|
30
|
+
return {
|
|
31
|
+
type: "audio",
|
|
32
|
+
uri: "",
|
|
33
|
+
asset_id: null,
|
|
34
|
+
data: null,
|
|
35
|
+
metadata: null
|
|
36
|
+
};
|
|
37
|
+
case "video":
|
|
38
|
+
return {
|
|
39
|
+
type: "video",
|
|
40
|
+
uri: "",
|
|
41
|
+
asset_id: null,
|
|
42
|
+
data: null,
|
|
43
|
+
metadata: null,
|
|
44
|
+
duration: null,
|
|
45
|
+
format: null
|
|
46
|
+
};
|
|
47
|
+
default:
|
|
48
|
+
if (propType.startsWith("list["))
|
|
49
|
+
return [];
|
|
50
|
+
if (propType.startsWith("dict["))
|
|
51
|
+
return null;
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function propertyOf(instance, name) {
|
|
56
|
+
// SAFETY: every declared property is registered from a manifest field, whose
|
|
57
|
+
// declared types are exactly the scalars, media refs, and lists or dicts of
|
|
58
|
+
// those that `NodeValue` names.
|
|
59
|
+
const properties = instance;
|
|
60
|
+
return properties[name];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The media kind a prop type names, or `null` when it names none.
|
|
64
|
+
* `list[image]` answers `"image"` — {@link isListAssetPropType} tells the two apart.
|
|
65
|
+
*/
|
|
66
|
+
export function assetKindOf(propType) {
|
|
67
|
+
switch (propType.toLowerCase()) {
|
|
68
|
+
case "image":
|
|
69
|
+
case "list[image]":
|
|
70
|
+
return "image";
|
|
71
|
+
case "video":
|
|
72
|
+
case "list[video]":
|
|
73
|
+
return "video";
|
|
74
|
+
case "audio":
|
|
75
|
+
case "list[audio]":
|
|
76
|
+
return "audio";
|
|
77
|
+
default:
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** True for `list[image]` / `list[video]` / `list[audio]`. */
|
|
82
|
+
export function isListAssetPropType(propType) {
|
|
83
|
+
const lower = propType.toLowerCase();
|
|
84
|
+
return lower.startsWith("list[") && assetKindOf(lower) !== null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether an enum's API argument must be sent as a number under `policy`.
|
|
88
|
+
* Exported because replicate also needs it at property-registration time, to
|
|
89
|
+
* register numeric options and a numeric default.
|
|
90
|
+
*/
|
|
91
|
+
export function manifestEnumIsNumeric(values, policy = "declared") {
|
|
92
|
+
if (!Array.isArray(values) || values.length === 0)
|
|
93
|
+
return false;
|
|
94
|
+
if (policy === "declared") {
|
|
95
|
+
// A numeric option equals its own numeric value; the string "1024" does
|
|
96
|
+
// not, and NaN never equals itself.
|
|
97
|
+
return values.every((v) => v === Number(v));
|
|
98
|
+
}
|
|
99
|
+
return values.every((v) => v !== "" && Number.isFinite(Number(v)));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Coerce a stored property value to the type a manifest-driven API expects.
|
|
103
|
+
*
|
|
104
|
+
* A non-numeric value in an `int`/`float` field answers `null` rather than
|
|
105
|
+
* `NaN`: the factories all strip `null` args before sending, and `NaN` survives
|
|
106
|
+
* that strip and reaches the provider as an invalid number.
|
|
107
|
+
*
|
|
108
|
+
* `list[...]` / `dict[...]` values pass through untouched — they are the user's
|
|
109
|
+
* own structure, not a scalar to reshape.
|
|
110
|
+
*/
|
|
111
|
+
export function coerceManifestScalar(value, propType, enumValues, enumPolicy = "declared") {
|
|
112
|
+
if (value === null || value === undefined)
|
|
113
|
+
return value;
|
|
114
|
+
const type = propType.toLowerCase();
|
|
115
|
+
if (type.startsWith("list[") || type.startsWith("dict["))
|
|
116
|
+
return value;
|
|
117
|
+
switch (type) {
|
|
118
|
+
case "int": {
|
|
119
|
+
const n = Number(value);
|
|
120
|
+
return Number.isFinite(n) ? Math.trunc(n) : null;
|
|
121
|
+
}
|
|
122
|
+
case "float": {
|
|
123
|
+
const n = Number(value);
|
|
124
|
+
return Number.isFinite(n) ? n : null;
|
|
125
|
+
}
|
|
126
|
+
case "bool": {
|
|
127
|
+
// The UI serializes a checkbox as the string "false" as readily as the
|
|
128
|
+
// boolean; `Boolean("false")` is true, which is how fal/kie/topaz sent
|
|
129
|
+
// an unchecked box as checked.
|
|
130
|
+
const text = String(value).trim().toLowerCase();
|
|
131
|
+
if (text === "false" || text === "" || text === "0")
|
|
132
|
+
return false;
|
|
133
|
+
return Boolean(value);
|
|
134
|
+
}
|
|
135
|
+
default:
|
|
136
|
+
return manifestEnumIsNumeric(enumValues, enumPolicy)
|
|
137
|
+
? Number(value)
|
|
138
|
+
: String(value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Answer `null` for a numeric `0` that the field's `min` forbids, so arg
|
|
143
|
+
* cleanup drops it.
|
|
144
|
+
*
|
|
145
|
+
* The provider codegens fill an optional number that has no schema default
|
|
146
|
+
* with `0`. Where the field's `min` is above zero (Flux 2 `width`/`height`,
|
|
147
|
+
* min 256, used only with `aspect_ratio=custom`), that placeholder can never
|
|
148
|
+
* be valid, and sending it makes the provider reject the whole request. A `0`
|
|
149
|
+
* inside the declared range is a real value and passes through.
|
|
150
|
+
*/
|
|
151
|
+
export function unsetZeroBelowMin(value, min, defaultValue) {
|
|
152
|
+
return value === 0 && defaultValue === 0 && min !== undefined && min > 0
|
|
153
|
+
? null
|
|
154
|
+
: value;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Route `asset://` media mentioned inline in a node's text inputs onto its empty
|
|
158
|
+
* image/audio/video inputs, and strip the mentions from the text.
|
|
159
|
+
*
|
|
160
|
+
* `hasSource` is the caller's own "this ref already points at bytes" test —
|
|
161
|
+
* the providers disagree about whether an `asset_id`-only ref counts, so the
|
|
162
|
+
* decision stays with them.
|
|
163
|
+
*/
|
|
164
|
+
export async function promptAssetOverridesFor(instance, fields, hasSource, context) {
|
|
165
|
+
const textFields = [];
|
|
166
|
+
const assetFields = [];
|
|
167
|
+
for (const field of fields) {
|
|
168
|
+
if (field.parentField)
|
|
169
|
+
continue;
|
|
170
|
+
const kind = assetKindOf(field.propType);
|
|
171
|
+
if (kind) {
|
|
172
|
+
const list = isListAssetPropType(field.propType);
|
|
173
|
+
const value = propertyOf(instance, field.name);
|
|
174
|
+
assetFields.push({
|
|
175
|
+
name: field.name,
|
|
176
|
+
label: field.apiParamName ?? field.name,
|
|
177
|
+
kind,
|
|
178
|
+
list,
|
|
179
|
+
hasSource: list
|
|
180
|
+
? Array.isArray(value) && value.some(hasSource)
|
|
181
|
+
: hasSource(value)
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
else if (field.propType.toLowerCase() === "str") {
|
|
185
|
+
textFields.push({
|
|
186
|
+
name: field.name,
|
|
187
|
+
value: String(propertyOf(instance, field.name) ?? "")
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// SAFETY: the overrides are asset refs this call injected from `asset://`
|
|
192
|
+
// mentions, so they are node property values like the ones they replace.
|
|
193
|
+
return mapPromptAssetsToInputs(textFields, assetFields, context);
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=manifest-node-values.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest-node-values.js","sourceRoot":"","sources":["../src/manifest-node-values.ts"],"names":[],"mappings":"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"}
|
package/dist/metadata.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { InputMode, NodeEffect, OutputCorrelation, Platform } from "@nodetool-ai/protocol";
|
|
1
2
|
export interface TypeMetadata {
|
|
2
3
|
type: string;
|
|
3
4
|
optional?: boolean;
|
|
@@ -30,24 +31,88 @@ export interface NodeMetadata {
|
|
|
30
31
|
namespace: string;
|
|
31
32
|
node_type: string;
|
|
32
33
|
layout?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Node body renderer key. "content_card" makes the frontend render a
|
|
36
|
+
* media/text-forward content card instead of the generic input/output body.
|
|
37
|
+
* Node authors opt in via the class's `body` static (TS) or `_body`/`body()`
|
|
38
|
+
* (Python). Absent or "default" → generic body.
|
|
39
|
+
*/
|
|
40
|
+
body?: string;
|
|
33
41
|
properties: PropertyMetadata[];
|
|
34
42
|
outputs: OutputSlotMetadata[];
|
|
35
43
|
recommended_models?: unknown[];
|
|
36
|
-
|
|
44
|
+
inline_fields?: string[];
|
|
45
|
+
input_fields?: string[];
|
|
37
46
|
required_settings?: string[];
|
|
38
47
|
/**
|
|
39
48
|
* Runtime packages this node depends on (e.g. "ffmpeg", "python", "ollama").
|
|
40
49
|
* The frontend uses this to show install prompts before execution.
|
|
41
50
|
*/
|
|
42
51
|
required_runtimes?: string[];
|
|
43
|
-
|
|
52
|
+
supports_dynamic_inputs?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Types a user may pick for a dynamic input slot on this node. Unset means
|
|
55
|
+
* the full type palette. See `BaseNode.allowedDynamicSlotTypes`.
|
|
56
|
+
*/
|
|
57
|
+
allowed_dynamic_slot_types?: TypeMetadata[];
|
|
44
58
|
is_streaming_input?: boolean;
|
|
45
59
|
is_streaming_output?: boolean;
|
|
60
|
+
input_mode?: InputMode;
|
|
61
|
+
output_correlation?: Record<string, OutputCorrelation>;
|
|
46
62
|
is_controlled?: boolean;
|
|
47
|
-
|
|
63
|
+
/** §7 — Zip/Cross nodes opt out of the incomparable-scope check. */
|
|
64
|
+
is_join_node?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Trigger node: compiles to a `trigger_registrations` row on activation
|
|
67
|
+
* and enters via `emitTriggerEvent` when a run carries a trigger event.
|
|
68
|
+
*/
|
|
69
|
+
is_trigger?: boolean;
|
|
70
|
+
/** Emit output_update even for connected handles (UI-monitor nodes). */
|
|
71
|
+
always_emit_output_updates?: boolean;
|
|
48
72
|
supports_dynamic_outputs?: boolean;
|
|
49
73
|
auto_save_asset?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Opt-in: re-running the node with identical inputs is safe. Absent means
|
|
76
|
+
* unsafe, so the supervisor withholds `retry`. See `BaseNode.retrySafe`.
|
|
77
|
+
*/
|
|
78
|
+
retry_safe?: boolean;
|
|
79
|
+
/** See `BaseNode.appendStaticInputs`. */
|
|
80
|
+
append_static_inputs?: string[];
|
|
81
|
+
/**
|
|
82
|
+
* Per-type cache lifetime for partial runs (seconds, or the `"forever"`
|
|
83
|
+
* sentinel — never `Infinity`, which is not JSON-safe). Only consulted for
|
|
84
|
+
* Computed nodes. Unset / `0` = never reuse. See `BaseNode.cacheTtl` and
|
|
85
|
+
* docs/superpowers/specs/2026-06-27-run-subgraph-caching.md §4.
|
|
86
|
+
*/
|
|
87
|
+
cache_ttl?: number | "forever";
|
|
88
|
+
/**
|
|
89
|
+
* What running the node does to the world. Reactive runs (mini-app sliders,
|
|
90
|
+
* live previews) traverse "pure" and "read" only. Absent is treated as
|
|
91
|
+
* "external". See `BaseNode.effect`.
|
|
92
|
+
*/
|
|
93
|
+
effect?: NodeEffect;
|
|
94
|
+
/**
|
|
95
|
+
* Names the output slot carrying the node's primary generation (persisted
|
|
96
|
+
* value + content-card preview). Absent falls back to the first output.
|
|
97
|
+
*/
|
|
98
|
+
primary_output?: string;
|
|
50
99
|
model_packs?: unknown[];
|
|
100
|
+
/**
|
|
101
|
+
* Deployment platforms this node supports. Absent or empty values are
|
|
102
|
+
* treated as ["node"] (most restrictive). See `@nodetool-ai/protocol`'s
|
|
103
|
+
* Platform type for the closed set.
|
|
104
|
+
*/
|
|
105
|
+
platforms?: readonly Platform[];
|
|
106
|
+
/** When true, the node remains runnable but should be deprioritized in search. */
|
|
107
|
+
deprecated?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* When true, the node is registered and runnable but hidden from discovery
|
|
110
|
+
* UIs (palette, search) entirely — for internal nodes a user never adds by
|
|
111
|
+
* hand (e.g. agent-runner-inserted steps).
|
|
112
|
+
*/
|
|
113
|
+
hidden?: boolean;
|
|
114
|
+
/** Preferred replacement node_type when deprecated is true. */
|
|
115
|
+
replaced_by?: string;
|
|
51
116
|
}
|
|
52
117
|
export interface PackageMetadata {
|
|
53
118
|
name: string;
|
|
@@ -72,5 +137,12 @@ export interface PythonMetadataLoadResult {
|
|
|
72
137
|
duplicates: string[];
|
|
73
138
|
warnings: string[];
|
|
74
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Python's json module emits bare `NaN`/`Infinity`/`-Infinity` tokens, which
|
|
142
|
+
* are not valid JSON. Replace them with `null` — but only outside string
|
|
143
|
+
* literals, so descriptions or defaults containing the words "NaN" or
|
|
144
|
+
* "Infinity" survive intact.
|
|
145
|
+
*/
|
|
146
|
+
export declare function sanitizePythonJson(raw: string): string;
|
|
75
147
|
export declare function loadPythonPackageMetadata(options?: PythonMetadataLoadOptions): PythonMetadataLoadResult;
|
|
76
148
|
//# sourceMappingURL=metadata.d.ts.map
|
package/dist/metadata.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"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"}
|
package/dist/metadata.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { IS_NODE, getNodeBuiltinSync } from "@nodetool-ai/config";
|
|
2
|
+
import { isObjectLike, isString } from "./type-predicates.js";
|
|
3
|
+
function asDefault(mod) {
|
|
4
|
+
if (!mod)
|
|
5
|
+
return undefined;
|
|
6
|
+
return mod.default ?? mod;
|
|
7
|
+
}
|
|
8
|
+
const fs = asDefault(getNodeBuiltinSync("node:fs"));
|
|
9
|
+
const path = asDefault(getNodeBuiltinSync("node:path"));
|
|
10
|
+
const crypto = asDefault(getNodeBuiltinSync("node:crypto"));
|
|
11
|
+
const os = asDefault(getNodeBuiltinSync("node:os"));
|
|
5
12
|
/** Ensure a raw type object from JSON always has type_args as an array. */
|
|
6
13
|
export function normalizeTypeMetadata(t) {
|
|
7
14
|
return {
|
|
@@ -12,22 +19,32 @@ export function normalizeTypeMetadata(t) {
|
|
|
12
19
|
function isRecord(value) {
|
|
13
20
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
14
21
|
}
|
|
22
|
+
/** True if `dir` is a nodetool `package_metadata` directory. */
|
|
23
|
+
function isMetadataDir(dir) {
|
|
24
|
+
const normalized = dir.split(path.sep).join("/");
|
|
25
|
+
// Stryker disable next-line LogicalOperator,MethodExpression: a "/src/nodetool/package_metadata" path also ends with "/nodetool/package_metadata", so the first check is redundant with the second — mutating it leaves detection unchanged (equivalent).
|
|
26
|
+
return normalized.endsWith("/src/nodetool/package_metadata") || normalized.endsWith("/nodetool/package_metadata");
|
|
27
|
+
}
|
|
28
|
+
/** Read the `.json` files in a `package_metadata` directory into `files`. */
|
|
29
|
+
function scanMetadataDir(dir, files, warnings) {
|
|
30
|
+
try {
|
|
31
|
+
const metadataFiles = fs
|
|
32
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
33
|
+
.filter((f) => f.isFile() && f.name.endsWith(".json"))
|
|
34
|
+
.map((f) => path.join(dir, f.name));
|
|
35
|
+
files.push(...metadataFiles);
|
|
36
|
+
// Stryker disable next-line BlockStatement,StringLiteral: a readdir failure on a directory we just matched is unreachable in tests; the warning text is diagnostic.
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
warnings.push(`Failed to scan metadata dir ${dir}: ${String(error)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
15
42
|
function walkForMetadataFiles(root, maxDepth, files, warnings, depth = 0) {
|
|
16
|
-
|
|
43
|
+
const tooDeep = depth > maxDepth;
|
|
44
|
+
if (tooDeep)
|
|
17
45
|
return;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
normalizedRoot.endsWith("/nodetool/package_metadata")) {
|
|
21
|
-
try {
|
|
22
|
-
const metadataFiles = fs
|
|
23
|
-
.readdirSync(root, { withFileTypes: true })
|
|
24
|
-
.filter((f) => f.isFile() && f.name.endsWith(".json"))
|
|
25
|
-
.map((f) => path.join(root, f.name));
|
|
26
|
-
files.push(...metadataFiles);
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
warnings.push(`Failed to scan metadata dir ${root}: ${String(error)}`);
|
|
30
|
-
}
|
|
46
|
+
if (isMetadataDir(root)) {
|
|
47
|
+
scanMetadataDir(root, files, warnings);
|
|
31
48
|
return;
|
|
32
49
|
}
|
|
33
50
|
let entries;
|
|
@@ -46,19 +63,9 @@ function walkForMetadataFiles(root, maxDepth, files, warnings, depth = 0) {
|
|
|
46
63
|
entry.name === "dist") {
|
|
47
64
|
continue;
|
|
48
65
|
}
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
const metadataFiles = fs
|
|
54
|
-
.readdirSync(fullPath, { withFileTypes: true })
|
|
55
|
-
.filter((f) => f.isFile() && f.name.endsWith(".json"))
|
|
56
|
-
.map((f) => path.join(fullPath, f.name));
|
|
57
|
-
files.push(...metadataFiles);
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
warnings.push(`Failed to scan metadata dir ${fullPath}: ${String(error)}`);
|
|
61
|
-
}
|
|
66
|
+
// Stryker disable next-line ConditionalExpression,BlockStatement: redundant with the recursion below — a metadata subdir not matched (or not scanned) here is found when walkForMetadataFiles recurses into it and matches the top-level isMetadataDir check (equivalent).
|
|
67
|
+
if (isMetadataDir(fullPath)) {
|
|
68
|
+
scanMetadataDir(fullPath, files, warnings);
|
|
62
69
|
continue;
|
|
63
70
|
}
|
|
64
71
|
walkForMetadataFiles(fullPath, maxDepth, files, warnings, depth + 1);
|
|
@@ -69,37 +76,53 @@ function walkForMetadataFiles(root, maxDepth, files, warnings, depth = 0) {
|
|
|
69
76
|
// Metadata cache — avoids re-reading and re-parsing JSON files on every startup
|
|
70
77
|
// when the underlying metadata files haven't changed.
|
|
71
78
|
// ---------------------------------------------------------------------------
|
|
72
|
-
|
|
79
|
+
// Per-user cache dir — the shared system tmpdir is world-writable, so a
|
|
80
|
+
// predictable path there would let another local user pre-seed poisoned
|
|
81
|
+
// metadata or plant symlinks.
|
|
82
|
+
// `IS_NODE` alone is not enough: Cloudflare Workers (`nodejs_compat`) set
|
|
83
|
+
// `process.versions.node`, so IS_NODE is true, yet `importNodeBuiltin` returns
|
|
84
|
+
// null there (workerd blocks the `new Function` dynamic import). Gate on the
|
|
85
|
+
// builtins actually loading so this module-load const doesn't deref a null
|
|
86
|
+
// `path`/`os` on the Workers isolate.
|
|
87
|
+
const CACHE_DIR = IS_NODE && path && os
|
|
88
|
+
? path.join(
|
|
89
|
+
// Stryker disable next-line ConditionalExpression,LogicalOperator: XDG_CACHE_HOME is unset in the test environment, so the homedir fallback always applies (equivalent).
|
|
90
|
+
process.env["XDG_CACHE_HOME"] || path.join(os.homedir(), ".cache"), "nodetool", "metadata-cache")
|
|
91
|
+
: // Stryker disable next-line StringLiteral: the non-Node branch is unreachable in the (Node) test environment, where IS_NODE is always true (dead branch).
|
|
92
|
+
"/tmp/nodetool-metadata-cache";
|
|
73
93
|
const CACHE_VERSION = 1;
|
|
74
94
|
/** Build a fingerprint from sorted file paths + their mtime/size. */
|
|
75
95
|
function buildFingerprint(files) {
|
|
76
96
|
const hash = crypto.createHash("sha256");
|
|
77
97
|
for (const file of files) {
|
|
78
98
|
hash.update(file);
|
|
99
|
+
// Stryker disable BlockStatement: the catch is unreachable for freshly-walked files (they exist); emptying it changes only an opaque cache-key input (equivalent).
|
|
79
100
|
try {
|
|
80
101
|
const stat = fs.statSync(file);
|
|
81
102
|
hash.update(`|${stat.mtimeMs}|${stat.size}`);
|
|
82
103
|
}
|
|
83
104
|
catch {
|
|
105
|
+
// Stryker disable next-line StringLiteral: opaque, consistent cache-key input (equivalent).
|
|
84
106
|
hash.update("|missing");
|
|
85
107
|
}
|
|
108
|
+
// Stryker restore BlockStatement
|
|
86
109
|
}
|
|
87
110
|
return hash.digest("hex");
|
|
88
111
|
}
|
|
89
112
|
function getCachePath(roots) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.update(roots.join(":"))
|
|
93
|
-
.digest("hex")
|
|
94
|
-
.slice(0, 16);
|
|
113
|
+
// Stryker disable next-line MethodExpression,StringLiteral: the cache filename derives from an opaque hash of the roots; any consistent key maps a roots-set to a stable file, so these hashing details are equivalent.
|
|
114
|
+
const key = crypto.createHash("sha256").update(roots.join(":")).digest("hex").slice(0, 16);
|
|
95
115
|
return path.join(CACHE_DIR, `metadata-${key}.json`);
|
|
96
116
|
}
|
|
97
117
|
function tryReadCache(cachePath, fingerprint) {
|
|
98
118
|
try {
|
|
119
|
+
// Stryker disable next-line ConditionalExpression: a missing cache file makes the readFileSync below throw into the catch, which also returns null (equivalent).
|
|
99
120
|
if (!fs.existsSync(cachePath))
|
|
100
121
|
return null;
|
|
122
|
+
// Stryker disable next-line StringLiteral: Node decodes "" as utf-8, so the encoding argument is equivalent.
|
|
101
123
|
const raw = fs.readFileSync(cachePath, "utf8");
|
|
102
124
|
const entry = JSON.parse(raw);
|
|
125
|
+
// Stryker disable next-line ConditionalExpression: the cache-staleness guard is covered behaviourally by the cache round-trip test (a content change invalidates via fingerprint); the version vs fingerprint operands are not independently asserted.
|
|
103
126
|
if (entry.version !== CACHE_VERSION || entry.fingerprint !== fingerprint)
|
|
104
127
|
return null;
|
|
105
128
|
return {
|
|
@@ -109,10 +132,12 @@ function tryReadCache(cachePath, fingerprint) {
|
|
|
109
132
|
duplicates: entry.duplicates,
|
|
110
133
|
warnings: entry.warnings
|
|
111
134
|
};
|
|
135
|
+
// Stryker disable BlockStatement: emptying this catch makes tryReadCache fall through to `return undefined`, which the caller treats identically to null (equivalent).
|
|
112
136
|
}
|
|
113
137
|
catch {
|
|
114
138
|
return null;
|
|
115
139
|
}
|
|
140
|
+
// Stryker restore BlockStatement
|
|
116
141
|
}
|
|
117
142
|
function writeCache(cachePath, fingerprint, result) {
|
|
118
143
|
try {
|
|
@@ -132,6 +157,54 @@ function writeCache(cachePath, fingerprint, result) {
|
|
|
132
157
|
// Cache write failure is non-fatal
|
|
133
158
|
}
|
|
134
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Python's json module emits bare `NaN`/`Infinity`/`-Infinity` tokens, which
|
|
162
|
+
* are not valid JSON. Replace them with `null` — but only outside string
|
|
163
|
+
* literals, so descriptions or defaults containing the words "NaN" or
|
|
164
|
+
* "Infinity" survive intact.
|
|
165
|
+
*/
|
|
166
|
+
export function sanitizePythonJson(raw) {
|
|
167
|
+
let out = "";
|
|
168
|
+
let inString = false;
|
|
169
|
+
for (let i = 0; i < raw.length; i++) {
|
|
170
|
+
const ch = raw[i];
|
|
171
|
+
if (inString) {
|
|
172
|
+
out += ch;
|
|
173
|
+
if (ch === "\\") {
|
|
174
|
+
// Copy the escaped character verbatim so an escaped quote (\")
|
|
175
|
+
// doesn't terminate the string early.
|
|
176
|
+
out += raw[i + 1] ?? "";
|
|
177
|
+
i++;
|
|
178
|
+
}
|
|
179
|
+
else if (ch === '"') {
|
|
180
|
+
inString = false;
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (ch === '"') {
|
|
185
|
+
inString = true;
|
|
186
|
+
out += ch;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (raw.startsWith("NaN", i)) {
|
|
190
|
+
out += "null";
|
|
191
|
+
i += 2;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (raw.startsWith("-Infinity", i)) {
|
|
195
|
+
out += "null";
|
|
196
|
+
i += 8;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (raw.startsWith("Infinity", i)) {
|
|
200
|
+
out += "null";
|
|
201
|
+
i += 7;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
out += ch;
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
135
208
|
function parseMetadataFiles(files) {
|
|
136
209
|
const packages = [];
|
|
137
210
|
const nodesByType = new Map();
|
|
@@ -140,11 +213,15 @@ function parseMetadataFiles(files) {
|
|
|
140
213
|
for (const file of files) {
|
|
141
214
|
let parsed;
|
|
142
215
|
try {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
.
|
|
146
|
-
|
|
147
|
-
|
|
216
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
217
|
+
try {
|
|
218
|
+
parsed = JSON.parse(raw);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Python's json module allows NaN/Infinity which are not valid JSON;
|
|
222
|
+
// retry with those tokens (outside string literals) nulled out.
|
|
223
|
+
parsed = JSON.parse(sanitizePythonJson(raw));
|
|
224
|
+
}
|
|
148
225
|
}
|
|
149
226
|
catch (error) {
|
|
150
227
|
warnings.push(`Failed to parse JSON ${file}: ${String(error)}`);
|
|
@@ -159,15 +236,15 @@ function parseMetadataFiles(files) {
|
|
|
159
236
|
const metaDir = path.dirname(file); // .../nodetool/package_metadata
|
|
160
237
|
const sourceFolder = path.dirname(path.dirname(metaDir)); // strip /nodetool/package_metadata
|
|
161
238
|
const pkg = {
|
|
162
|
-
name:
|
|
239
|
+
name: isString(parsed.name)
|
|
163
240
|
? parsed.name
|
|
164
241
|
: path.basename(file, ".json"),
|
|
165
|
-
description:
|
|
166
|
-
version:
|
|
242
|
+
description: isString(parsed.description) ? parsed.description : undefined,
|
|
243
|
+
version: isString(parsed.version) ? parsed.version : undefined,
|
|
167
244
|
authors: Array.isArray(parsed.authors)
|
|
168
245
|
? parsed.authors
|
|
169
246
|
: undefined,
|
|
170
|
-
repo_id:
|
|
247
|
+
repo_id: isString(parsed.repo_id) ? parsed.repo_id : undefined,
|
|
171
248
|
nodes: Array.isArray(parsed.nodes)
|
|
172
249
|
? parsed.nodes
|
|
173
250
|
: undefined,
|
|
@@ -176,10 +253,12 @@ function parseMetadataFiles(files) {
|
|
|
176
253
|
sourceFolder
|
|
177
254
|
};
|
|
178
255
|
packages.push(pkg);
|
|
256
|
+
// Stryker disable next-line ArrayDeclaration: a bogus seed node is a string, which the guard below skips (typeof "..." !== "object"), so the result is unchanged (equivalent).
|
|
179
257
|
for (const node of pkg.nodes ?? []) {
|
|
180
258
|
if (!node ||
|
|
181
|
-
|
|
182
|
-
|
|
259
|
+
// Stryker disable next-line ConditionalExpression: a non-object truthy node has no string node_type, so the next check rejects it either way (equivalent).
|
|
260
|
+
!isObjectLike(node) ||
|
|
261
|
+
!isString(node.node_type)) {
|
|
183
262
|
continue;
|
|
184
263
|
}
|
|
185
264
|
if (nodesByType.has(node.node_type)) {
|
|
@@ -209,7 +288,9 @@ function parseMetadataFiles(files) {
|
|
|
209
288
|
};
|
|
210
289
|
}
|
|
211
290
|
export function loadPythonPackageMetadata(options = {}) {
|
|
212
|
-
const roots = (
|
|
291
|
+
const roots = (
|
|
292
|
+
// Stryker disable next-line ConditionalExpression,LogicalOperator,EqualityOperator,ArrayDeclaration: the cwd fallback for an empty roots list is environment-dependent — a clean working tree yields no metadata, so [] and [process.cwd()] are indistinguishable in tests (equivalent).
|
|
293
|
+
options.roots && options.roots.length > 0 ? options.roots : [process.cwd()]).map((p) => path.resolve(p));
|
|
213
294
|
const maxDepth = options.maxDepth ?? 8;
|
|
214
295
|
const warnings = [];
|
|
215
296
|
const fileSet = new Set();
|
|
@@ -231,12 +312,14 @@ export function loadPythonPackageMetadata(options = {}) {
|
|
|
231
312
|
const cached = tryReadCache(cachePath, fingerprint);
|
|
232
313
|
if (cached) {
|
|
233
314
|
// Merge any walk warnings into the cached result
|
|
315
|
+
// Stryker disable next-line ConditionalExpression,EqualityOperator,BlockStatement: merging an empty warnings list is a no-op, so the > 0 guard is equivalent for the empty case; the populated case requires a cache hit *and* walk warnings simultaneously, which the cache round-trip test does not combine.
|
|
234
316
|
if (warnings.length > 0) {
|
|
235
317
|
cached.warnings = [...warnings, ...cached.warnings];
|
|
236
318
|
}
|
|
237
319
|
return cached;
|
|
238
320
|
}
|
|
239
321
|
const result = parseMetadataFiles(files);
|
|
322
|
+
// Stryker disable next-line ConditionalExpression,EqualityOperator: the populated path is covered by the walk-warnings test (a non-existent root is prepended to the parse warnings); merging an empty list is a no-op, so the >= 0 / always-true variants are equivalent.
|
|
240
323
|
if (warnings.length > 0) {
|
|
241
324
|
result.warnings = [...warnings, ...result.warnings];
|
|
242
325
|
}
|