@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.
Files changed (158) hide show
  1. package/README.md +34 -0
  2. package/dist/.nodetool-build-stamp +1 -0
  3. package/dist/base-node.d.ts +285 -14
  4. package/dist/base-node.d.ts.map +1 -1
  5. package/dist/base-node.js +359 -49
  6. package/dist/base-node.js.map +1 -1
  7. package/dist/class-name-to-title.d.ts +26 -0
  8. package/dist/class-name-to-title.d.ts.map +1 -0
  9. package/dist/class-name-to-title.js +59 -0
  10. package/dist/class-name-to-title.js.map +1 -0
  11. package/dist/code-analysis.d.ts +234 -0
  12. package/dist/code-analysis.d.ts.map +1 -0
  13. package/dist/code-analysis.js +919 -0
  14. package/dist/code-analysis.js.map +1 -0
  15. package/dist/code-body.d.ts +61 -0
  16. package/dist/code-body.d.ts.map +1 -0
  17. package/dist/code-body.js +288 -0
  18. package/dist/code-body.js.map +1 -0
  19. package/dist/code-node-validation.d.ts +67 -0
  20. package/dist/code-node-validation.d.ts.map +1 -0
  21. package/dist/code-node-validation.js +540 -0
  22. package/dist/code-node-validation.js.map +1 -0
  23. package/dist/content-card.d.ts +30 -0
  24. package/dist/content-card.d.ts.map +1 -0
  25. package/dist/content-card.js +68 -0
  26. package/dist/content-card.js.map +1 -0
  27. package/dist/correlation-validation.d.ts +19 -0
  28. package/dist/correlation-validation.d.ts.map +1 -0
  29. package/dist/correlation-validation.js +101 -0
  30. package/dist/correlation-validation.js.map +1 -0
  31. package/dist/cost-estimate.d.ts +244 -0
  32. package/dist/cost-estimate.d.ts.map +1 -0
  33. package/dist/cost-estimate.js +637 -0
  34. package/dist/cost-estimate.js.map +1 -0
  35. package/dist/decorators.d.ts.map +1 -1
  36. package/dist/decorators.js +4 -1
  37. package/dist/decorators.js.map +1 -1
  38. package/dist/docs/nodes.d.ts.map +1 -1
  39. package/dist/docs/nodes.js +3 -2
  40. package/dist/docs/nodes.js.map +1 -1
  41. package/dist/docs/overview.d.ts.map +1 -1
  42. package/dist/docs/overview.js +4 -3
  43. package/dist/docs/overview.js.map +1 -1
  44. package/dist/docs/workflows.d.ts.map +1 -1
  45. package/dist/docs/workflows.js +10 -9
  46. package/dist/docs/workflows.js.map +1 -1
  47. package/dist/field-classification.d.ts +31 -0
  48. package/dist/field-classification.d.ts.map +1 -0
  49. package/dist/field-classification.js +58 -0
  50. package/dist/field-classification.js.map +1 -0
  51. package/dist/graph-validation.d.ts +239 -0
  52. package/dist/graph-validation.d.ts.map +1 -0
  53. package/dist/graph-validation.js +1174 -0
  54. package/dist/graph-validation.js.map +1 -0
  55. package/dist/index.d.ts +27 -2
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +29 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/js-script-link.d.ts +53 -0
  60. package/dist/js-script-link.d.ts.map +1 -0
  61. package/dist/js-script-link.js +58 -0
  62. package/dist/js-script-link.js.map +1 -0
  63. package/dist/js-script-materialize.d.ts +52 -0
  64. package/dist/js-script-materialize.d.ts.map +1 -0
  65. package/dist/js-script-materialize.js +24 -0
  66. package/dist/js-script-materialize.js.map +1 -0
  67. package/dist/manifest-node-values.d.ts +106 -0
  68. package/dist/manifest-node-values.d.ts.map +1 -0
  69. package/dist/manifest-node-values.js +195 -0
  70. package/dist/manifest-node-values.js.map +1 -0
  71. package/dist/metadata.d.ts +75 -3
  72. package/dist/metadata.d.ts.map +1 -1
  73. package/dist/metadata.js +132 -49
  74. package/dist/metadata.js.map +1 -1
  75. package/dist/node-metadata.d.ts.map +1 -1
  76. package/dist/node-metadata.js +61 -56
  77. package/dist/node-metadata.js.map +1 -1
  78. package/dist/node-type-inventory.d.ts +51 -0
  79. package/dist/node-type-inventory.d.ts.map +1 -0
  80. package/dist/node-type-inventory.js +121 -0
  81. package/dist/node-type-inventory.js.map +1 -0
  82. package/dist/nodes/test-nodes.d.ts +77 -30
  83. package/dist/nodes/test-nodes.d.ts.map +1 -1
  84. package/dist/nodes/test-nodes.js +2 -34
  85. package/dist/nodes/test-nodes.js.map +1 -1
  86. package/dist/pack-loader.d.ts +202 -0
  87. package/dist/pack-loader.d.ts.map +1 -0
  88. package/dist/pack-loader.js +529 -0
  89. package/dist/pack-loader.js.map +1 -0
  90. package/dist/port-types.d.ts +15 -0
  91. package/dist/port-types.d.ts.map +1 -0
  92. package/dist/port-types.js +72 -0
  93. package/dist/port-types.js.map +1 -0
  94. package/dist/pricing-bundle.d.ts +83 -0
  95. package/dist/pricing-bundle.d.ts.map +1 -0
  96. package/dist/pricing-bundle.js +82 -0
  97. package/dist/pricing-bundle.js.map +1 -0
  98. package/dist/pricing-params.d.ts +59 -0
  99. package/dist/pricing-params.d.ts.map +1 -0
  100. package/dist/pricing-params.js +282 -0
  101. package/dist/pricing-params.js.map +1 -0
  102. package/dist/python-package-scan.d.ts +8 -0
  103. package/dist/python-package-scan.d.ts.map +1 -1
  104. package/dist/python-package-scan.js +21 -8
  105. package/dist/python-package-scan.js.map +1 -1
  106. package/dist/registry.d.ts +105 -2
  107. package/dist/registry.d.ts.map +1 -1
  108. package/dist/registry.js +403 -30
  109. package/dist/registry.js.map +1 -1
  110. package/dist/sandbox-bridge-packs.d.ts +39 -0
  111. package/dist/sandbox-bridge-packs.d.ts.map +1 -0
  112. package/dist/sandbox-bridge-packs.js +249 -0
  113. package/dist/sandbox-bridge-packs.js.map +1 -0
  114. package/dist/sandbox-catalog-host.d.ts +31 -0
  115. package/dist/sandbox-catalog-host.d.ts.map +1 -0
  116. package/dist/sandbox-catalog-host.js +78 -0
  117. package/dist/sandbox-catalog-host.js.map +1 -0
  118. package/dist/sandbox-module-catalog.d.ts +14 -0
  119. package/dist/sandbox-module-catalog.d.ts.map +1 -0
  120. package/dist/sandbox-module-catalog.js +402 -0
  121. package/dist/sandbox-module-catalog.js.map +1 -0
  122. package/dist/sandbox-npm-artifacts.d.ts +56 -0
  123. package/dist/sandbox-npm-artifacts.d.ts.map +1 -0
  124. package/dist/sandbox-npm-artifacts.js +17 -0
  125. package/dist/sandbox-npm-artifacts.js.map +1 -0
  126. package/dist/sandbox-pack-discovery.d.ts +93 -0
  127. package/dist/sandbox-pack-discovery.d.ts.map +1 -0
  128. package/dist/sandbox-pack-discovery.js +807 -0
  129. package/dist/sandbox-pack-discovery.js.map +1 -0
  130. package/dist/search.d.ts +29 -1
  131. package/dist/search.d.ts.map +1 -1
  132. package/dist/search.js +136 -9
  133. package/dist/search.js.map +1 -1
  134. package/dist/type-compat.d.ts +36 -0
  135. package/dist/type-compat.d.ts.map +1 -0
  136. package/dist/type-compat.js +98 -0
  137. package/dist/type-compat.js.map +1 -0
  138. package/dist/type-predicates.d.ts +25 -0
  139. package/dist/type-predicates.d.ts.map +1 -0
  140. package/dist/type-predicates.js +31 -0
  141. package/dist/type-predicates.js.map +1 -0
  142. package/dist/validation.d.ts +18 -0
  143. package/dist/validation.d.ts.map +1 -1
  144. package/dist/validation.js +70 -11
  145. package/dist/validation.js.map +1 -1
  146. package/dist/workflow-document-tools.d.ts +35 -0
  147. package/dist/workflow-document-tools.d.ts.map +1 -0
  148. package/dist/workflow-document-tools.js +402 -0
  149. package/dist/workflow-document-tools.js.map +1 -0
  150. package/dist/workflow-interface.d.ts +62 -0
  151. package/dist/workflow-interface.d.ts.map +1 -0
  152. package/dist/workflow-interface.js +351 -0
  153. package/dist/workflow-interface.js.map +1 -0
  154. package/package.json +67 -10
  155. package/dist/package-registry-client.d.ts +0 -12
  156. package/dist/package-registry-client.d.ts.map +0 -1
  157. package/dist/package-registry-client.js +0 -40
  158. 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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"}
@@ -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
- basic_fields?: string[];
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
- is_dynamic?: boolean;
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
- expose_as_tool?: boolean;
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
@@ -1 +1 @@
1
- {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"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"}
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 fs from "node:fs";
2
- import path from "node:path";
3
- import crypto from "node:crypto";
4
- import os from "node:os";
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
- if (depth > maxDepth)
43
+ const tooDeep = depth > maxDepth;
44
+ if (tooDeep)
17
45
  return;
18
- const normalizedRoot = root.split(path.sep).join("/");
19
- if (normalizedRoot.endsWith("/src/nodetool/package_metadata") ||
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
- const normalized = fullPath.split(path.sep).join("/");
50
- if (normalized.endsWith("/src/nodetool/package_metadata") ||
51
- normalized.endsWith("/nodetool/package_metadata")) {
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
- const CACHE_DIR = path.join(os.tmpdir(), "nodetool-metadata-cache");
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
- const key = crypto
91
- .createHash("sha256")
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
- // Python's json module allows NaN/Infinity which are not valid JSON; replace them with null.
144
- const raw = fs
145
- .readFileSync(file, "utf8")
146
- .replace(/\bNaN\b|-?Infinity\b/g, "null");
147
- parsed = JSON.parse(raw);
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: typeof parsed.name === "string"
239
+ name: isString(parsed.name)
163
240
  ? parsed.name
164
241
  : path.basename(file, ".json"),
165
- description: typeof parsed.description === "string" ? parsed.description : undefined,
166
- version: typeof parsed.version === "string" ? parsed.version : undefined,
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: typeof parsed.repo_id === "string" ? parsed.repo_id : undefined,
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
- typeof node !== "object" ||
182
- typeof node.node_type !== "string") {
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 = (options.roots && options.roots.length > 0 ? options.roots : [process.cwd()]).map((p) => path.resolve(p));
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
  }