node-red-contrib-vectorprime 0.1.41 → 0.1.43

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 CHANGED
@@ -1,70 +1,29 @@
1
1
  # node-red-contrib-vectorprime
2
2
 
3
- **VectorPrime Rank** rank tasks, alerts, or decisions inside Node-RED using the VectorPrime Decision Kernel (**Contract v1**).
3
+ **1-minute setup:** install the node → drag **Rank Decision (VectorPrime)** into a flow open **VectorPrime Config** get your API key paste → Deploy.
4
4
 
5
- This node helps your flow choose the **best next action** when multiple options compete — **without faking certainty**.
5
+ 👉 **Get your API key:** https://vectorprime.tech/?utm_source=node-red&utm_medium=readme&utm_campaign=node_red_contrib&utm_content=top_cta
6
6
 
7
- Prioritize tasks
8
- ✅ Rank alerts by urgency
9
- ✅ Choose the best option from multiple choices
10
- ✅ Turn “too many choices” into a safe automation decision state
11
- ✅ Works inside Node-RED flows with a simple node
7
+ > Pricing note: This node is free to use. VectorPrime API usage may require a paid plan after free-tier limits.
12
8
 
13
9
  ---
14
10
 
15
- ## What it does (in 1 sentence)
11
+ ## What this node does
16
12
 
17
- **Send a list of options get them ranked best-to-worst with probabilities + metadata you can branch on.**
13
+ VectorPrime helps Node-RED flows choose the **best next action** when multiple options compete without hard-coded rules.
18
14
 
19
- ---
20
-
21
- ## Why developers install this
22
-
23
- Most automation flows break down when you have **multiple possible actions** and don’t know which one to run.
24
-
25
- VectorPrime solves that by turning this:
15
+ Use cases:
26
16
 
27
- - “Which alert should fire first?”
28
- - “Which ticket should I do next?”
29
- - “Which customer should get priority?”
30
- - “Which action gives the best outcome with least risk?”
31
- - “Which fix should I deploy first?”
32
-
33
- Into:
34
-
35
- ✅ A **ranked result** when the input supports a winner
36
- ✅ Or a **tie / no-dominant outcome** (so your flow can branch safely)
37
-
38
- ---
39
-
40
- ## Nodes Included
41
-
42
- - **VectorPrime Config**
43
- Stores your VectorPrime Base URL + API Key.
44
-
45
- - **VectorPrime Rank**
46
- Sends options to VectorPrime (`/v1/kernel/rank`) and returns a ranked result.
17
+ - Prioritize tasks
18
+ - Rank alerts by urgency/impact
19
+ - Choose the safest next step in automation flows
20
+ - Route tickets/jobs based on structured decision input
47
21
 
48
22
  ---
49
23
 
50
24
  ## Install
51
25
 
52
- ### Option A: Install from the Node-RED palette (recommended)
53
- 1) Open Node-RED in your browser
54
- 2) Click the menu (top-right) ☰
55
- 3) Click **Manage palette**
56
- 4) Click the **Install** tab
57
- 5) Search: `node-red-contrib-vectorprime`
58
- 6) Click **Install**
59
-
60
- ### Option B: Install from terminal (Node-RED user directory)
61
-
62
- Open your Node-RED user directory:
63
-
64
- - **Windows:** `C:\Users\<you>\.node-red`
65
- - **macOS/Linux:** `~/.node-red`
66
-
67
- Then run:
26
+ In your Node-RED user directory (usually `C:\Users\<you>\.node-red`):
68
27
 
69
28
  ```bash
70
- npm install node-red-contrib-vectorprime
29
+ npm install node-red-contrib-vectorprime
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "node-red-contrib-vectorprime",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
4
4
  "description": "Rank tasks, alerts, or actions inside Node-RED when multiple options compete. Choose the safest next step automatically instead of hard-coded rules.",
5
5
  "license": "MIT",
6
6
  "author": "Faisal Khan",
7
7
  "homepage": "https://gitlab.com/faisalkhan3000/vectorprime-node-red-node",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://gitlab.com/faisalkhan3000/vectorprime-node-red-node.git"
10
+ "url": "git+https://gitlab.com/faisalkhan3000/vectorprime-node-red-node.git"
11
11
  },
12
12
  "bugs": {
13
13
  "url": "https://gitlab.com/faisalkhan3000/vectorprime-node-red-node/-/issues"
package/vectorprime.html CHANGED
@@ -1,5 +1,15 @@
1
+ ---
2
+
3
+ # 2) REPLACE: `vectorprime.html` (FULL FILE)
4
+
5
+ ```html
1
6
  <!-- =========================
2
- VectorPrime Config Template
7
+ VectorPrime Nodes (Node-RED)
8
+ WEBSITE-ONLY KEYS (no minting inside Node-RED)
9
+ ========================= -->
10
+
11
+ <!-- =========================
12
+ Config Node UI
3
13
  ========================= -->
4
14
  <script type="text/x-red" data-template-name="vectorprime-config">
5
15
  <div class="form-row">
@@ -9,26 +19,28 @@
9
19
  <input type="text" id="node-config-input-baseUrl" placeholder="https://vectorprime-kernel-backend.onrender.com">
10
20
  </div>
11
21
 
12
- <!-- ✅ REQUIRED: email for /v1/billing/signup -->
13
22
  <div class="form-row">
14
- <label for="node-config-input-email">
15
- <i class="fa fa-envelope"></i> Email
23
+ <label for="node-config-input-apiKey">
24
+ <i class="fa fa-key"></i> API Key
16
25
  </label>
17
- <input type="text" id="node-config-input-email" placeholder="you@example.com">
26
+ <input type="password" id="node-config-input-apiKey" placeholder="vp_live_...">
18
27
  </div>
19
28
 
20
29
  <div class="form-row">
21
- <label for="node-config-input-apiKey">
22
- <i class="fa fa-key"></i> API Key
23
- </label>
24
- <input type="password" id="node-config-input-apiKey" placeholder="vp_free_...">
30
+ <a
31
+ class="vp-cta-btn"
32
+ href="https://vectorprime.tech/?utm_source=node-red&utm_medium=config&utm_campaign=node_red_contrib&utm_content=get_key_button"
33
+ target="_blank"
34
+ rel="noopener noreferrer"
35
+ >
36
+ Get your API key on vectorprime.tech
37
+ </a>
25
38
  </div>
26
39
 
27
40
  <div class="form-row">
28
- <button id="vp-get-free-key" class="red-ui-button">
29
- Get Free Key
30
- </button>
31
- <span id="vp-key-status" style="margin-left:10px; font-size:12px;"></span>
41
+ <div style="font-size:12px; color:#666; line-height:1.4;">
42
+ Then paste it into <b>API Key</b> above.
43
+ </div>
32
44
  </div>
33
45
 
34
46
  <div class="form-row">
@@ -36,6 +48,21 @@
36
48
  ✅ Key is stored securely in Node-RED credentials (not exported with flows).
37
49
  </div>
38
50
  </div>
51
+
52
+ <style>
53
+ .vp-cta-btn{
54
+ display:block;
55
+ width:100%;
56
+ padding:10px 12px;
57
+ text-align:center;
58
+ background:#2563EB;
59
+ color:#fff !important;
60
+ border-radius:8px;
61
+ text-decoration:none;
62
+ font-weight:600;
63
+ }
64
+ .vp-cta-btn:hover{ filter:brightness(0.92); }
65
+ </style>
39
66
  </script>
40
67
 
41
68
  <script type="text/javascript">
@@ -50,8 +77,7 @@
50
77
  RED.nodes.registerType("vectorprime-config", {
51
78
  category: "config",
52
79
  defaults: {
53
- baseUrl: { value: "https://vectorprime-kernel-backend.onrender.com", required: true },
54
- email: { value: "", required: true }
80
+ baseUrl: { value: "https://vectorprime-kernel-backend.onrender.com", required: true }
55
81
  },
56
82
  credentials: {
57
83
  apiKey: { type: "password" }
@@ -60,65 +86,16 @@
60
86
  return "VectorPrime Config";
61
87
  },
62
88
  oneditprepare: function () {
63
- const statusEl = document.getElementById("vp-key-status");
64
- const btn = document.getElementById("vp-get-free-key");
65
-
66
- btn.onclick = async function (e) {
67
- e.preventDefault();
68
- statusEl.textContent = "Requesting free key...";
69
- statusEl.style.color = "#444";
70
-
71
- try {
72
- const baseUrlInput = document.getElementById("node-config-input-baseUrl");
73
- const emailInput = document.getElementById("node-config-input-email");
74
- const apiKeyInput = document.getElementById("node-config-input-apiKey");
75
-
76
- const baseUrl = normalizeBaseUrl(baseUrlInput.value);
77
- const email = ((emailInput && emailInput.value) ? emailInput.value : "").trim();
78
-
79
- if (!baseUrl.startsWith("http")) {
80
- statusEl.textContent = "❌ Base URL missing/invalid";
81
- statusEl.style.color = "red";
82
- return;
83
- }
84
-
85
- if (!email || !email.includes("@")) {
86
- statusEl.textContent = "❌ Enter a valid email";
87
- statusEl.style.color = "red";
88
- return;
89
- }
90
-
91
- // ✅ Call REAL backend endpoint that exists
92
- const resp = await fetch(`${baseUrl}/v1/billing/signup`, {
93
- method: "POST",
94
- headers: { "Content-Type": "application/json" },
95
- body: JSON.stringify({ email: email })
96
- });
97
-
98
- const text = await resp.text();
99
- let data;
100
- try { data = JSON.parse(text); } catch { data = { raw: text }; }
101
-
102
- if (!resp.ok) {
103
- statusEl.textContent = `❌ Failed (${resp.status}): ${data.message || data.error || data.detail || "unknown"}`;
104
- statusEl.style.color = "red";
105
- return;
106
- }
107
-
108
- if (!data.api_key) {
109
- statusEl.textContent = "❌ Backend did not return api_key";
110
- statusEl.style.color = "red";
111
- return;
112
- }
113
-
114
- apiKeyInput.value = data.api_key;
115
- statusEl.textContent = "✅ Free key generated + saved";
116
- statusEl.style.color = "green";
117
- } catch (err) {
118
- statusEl.textContent = `❌ Error: ${err.message}`;
119
- statusEl.style.color = "red";
120
- }
121
- };
89
+ const baseUrlInput = document.getElementById("node-config-input-baseUrl");
90
+ if (baseUrlInput && baseUrlInput.value) {
91
+ baseUrlInput.value = normalizeBaseUrl(baseUrlInput.value);
92
+ }
93
+ },
94
+ oneditsave: function () {
95
+ const baseUrlInput = document.getElementById("node-config-input-baseUrl");
96
+ if (baseUrlInput && baseUrlInput.value) {
97
+ baseUrlInput.value = normalizeBaseUrl(baseUrlInput.value);
98
+ }
122
99
  }
123
100
  });
124
101
  })();
@@ -130,9 +107,9 @@
130
107
 
131
108
  <h3>Quick Setup</h3>
132
109
  <ol>
133
- <li>Set <b>Base URL</b> (default is already correct)</li>
134
- <li>Enter your <b>Email</b></li>
135
- <li>Click <b>Get Free Key</b></li>
110
+ <li>Confirm <b>Base URL</b> (default is correct)</li>
111
+ <li>Click <b>Get your API key</b> (opens vectorprime.tech)</li>
112
+ <li>Paste the key into <b>API Key</b></li>
136
113
  <li>Click <b>Done</b></li>
137
114
  </ol>
138
115
 
@@ -143,16 +120,13 @@
143
120
  <h3>Recommended Base URL</h3>
144
121
  <pre>https://vectorprime-kernel-backend.onrender.com</pre>
145
122
 
146
- <h3>Tip</h3>
147
- <p>
148
- You can also paste your own paid key here if you want higher limits.
149
- </p>
123
+ <p><b>Website-only keys:</b> this node does not mint keys inside Node-RED.</p>
150
124
  </script>
151
125
 
152
126
  <hr />
153
127
 
154
128
  <!-- =========================
155
- VectorPrime Rank Template
129
+ Rank Node UI
156
130
  ========================= -->
157
131
  <script type="text/x-red" data-template-name="vectorprime-rank">
158
132
  <div class="form-row">
@@ -189,10 +163,7 @@
189
163
  inputs: 1,
190
164
  outputs: 1,
191
165
  icon: "font-awesome/fa-bolt",
192
-
193
- // ✅ This is the name users see in the palette
194
166
  paletteLabel: "Rank Decision (VectorPrime)",
195
-
196
167
  label: function () {
197
168
  return this.name || "Rank Decision (VectorPrime)";
198
169
  }
@@ -201,39 +172,37 @@
201
172
 
202
173
  <!-- ✅ HELP TAB: vectorprime-rank -->
203
174
  <script type="text/x-red" data-help-name="vectorprime-rank">
204
- <p>
205
- <b>Rank Decision (VectorPrime)</b> picks the best next action automatically.
206
- Use it anywhere your flow needs <b>priority scoring</b> or <b>decision ranking</b>.
207
- </p>
175
+ <p><b>Rank Decision (VectorPrime)</b> ranks options (tasks/actions/alerts) using the VectorPrime kernel.</p>
208
176
 
209
177
  <h3>What it does</h3>
210
178
  <ul>
211
179
  <li>Reads options from <code>msg.payload</code></li>
212
- <li>Calls the VectorPrime Decision Kernel Rank API</li>
180
+ <li>Calls the VectorPrime kernel rank API</li>
213
181
  <li>Returns the ranked decision inside <code>msg.payload</code></li>
214
182
  </ul>
215
183
 
216
- <h3>Where developers use this most</h3>
217
- <ul>
218
- <li><b>Incident response:</b> rank alerts → fix the most critical first</li>
219
- <li><b>Automation flows:</b> choose the best next action from multiple tasks</li>
220
- <li><b>Ops / IoT:</b> prioritize device events + maintenance decisions</li>
221
- <li><b>Ticketing & work queues:</b> rank jobs, leads, or support tickets</li>
222
- </ul>
223
-
224
184
  <h3>Input format (simple)</h3>
225
185
  <p>Send a list to rank inside <code>msg.payload.items</code>:</p>
226
186
 
227
187
  <pre><code>{
228
188
  "items": [
229
- { "id": "fix-prod-bug", "label": "Fix production bug", "urgency": 10, "impact": 10 },
230
- { "id": "ship-feature", "label": "Ship new feature", "urgency": 7, "impact": 8 },
231
- { "id": "refactor", "label": "Refactor module", "urgency": 3, "impact": 5 }
189
+ { "id": "a", "label": "Fix production bug", "urgency": 9, "impact": 10, "effort": 3 },
190
+ { "id": "b", "label": "Ship new feature", "urgency": 6, "impact": 7, "effort": 6 },
191
+ { "id": "c", "label": "Refactor module", "urgency": 3, "impact": 4, "effort": 8 }
232
192
  ]
233
193
  }</code></pre>
234
194
 
235
195
  <p>
236
- ✅ The node automatically converts <code>items</code> into the backend format
237
- (<code>decision_id</code>, <code>prompt</code>, <code>options</code>) so flows stay easy.
196
+ ✅ The node automatically converts <code>items</code> into:
197
+ <code>decision_id</code>, <code>prompt</code>, <code>options</code>.
238
198
  </p>
199
+
200
+ <h3>Output</h3>
201
+ <p>On success, the node sets <code>msg.payload</code> to the kernel response, typically including:</p>
202
+ <ul>
203
+ <li><code>ranking</code> (best → worst)</li>
204
+ <li><code>probabilities</code> (per option id)</li>
205
+ <li><code>meta</code> (engine info / tie-break flags)</li>
206
+ <li><code>vp_input</code> (what was sent)</li>
207
+ </ul>
239
208
  </script>
package/vectorprime.js CHANGED
@@ -1,7 +1,7 @@
1
1
  module.exports = function (RED) {
2
2
  "use strict";
3
3
 
4
- // Node 22 has global fetch, but keep fallback for safety
4
+ // Node 18+ has global fetch. Keep fallback for older Node.
5
5
  const fetchFn =
6
6
  typeof fetch === "function"
7
7
  ? fetch
@@ -25,10 +25,6 @@ module.exports = function (RED) {
25
25
  return Number.isFinite(n) ? n : null;
26
26
  }
27
27
 
28
- function almostEqual(a, b, eps = 1e-9) {
29
- return Math.abs(a - b) <= eps;
30
- }
31
-
32
28
  function isFlatProbabilities(probObj) {
33
29
  if (!probObj || typeof probObj !== "object") return false;
34
30
  const keys = Object.keys(probObj);
@@ -62,10 +58,9 @@ module.exports = function (RED) {
62
58
  });
63
59
 
64
60
  // Convert to pseudo-probabilities (normalized non-negative)
65
- // Make everything >= 0 by shifting if needed
66
61
  const scores = scored.map(s => s.score);
67
62
  const minScore = Math.min(...scores);
68
- const shifted = scores.map(s => s - minScore); // now min is 0
63
+ const shifted = scores.map(s => s - minScore); // min is 0
69
64
  const sum = shifted.reduce((acc, v) => acc + v, 0);
70
65
 
71
66
  const probabilities = {};
@@ -74,7 +69,6 @@ module.exports = function (RED) {
74
69
  probabilities[s.id] = shifted[j] / sum;
75
70
  });
76
71
  } else {
77
- // all zero (fully tied) -> uniform
78
72
  const uniform = 1 / scored.length;
79
73
  scored.forEach(s => { probabilities[s.id] = uniform; });
80
74
  }
@@ -87,6 +81,7 @@ module.exports = function (RED) {
87
81
 
88
82
  // -----------------------------
89
83
  // Config Node (stores Base URL + API Key)
84
+ // WEBSITE-ONLY KEYS: no minting inside Node-RED
90
85
  // -----------------------------
91
86
  function VectorPrimeConfigNode(n) {
92
87
  RED.nodes.createNode(this, n);
@@ -94,10 +89,7 @@ module.exports = function (RED) {
94
89
  this.baseUrl =
95
90
  (n.baseUrl && String(n.baseUrl).trim()) ||
96
91
  "https://vectorprime-kernel-backend.onrender.com";
97
-
98
- // NOTE: email is stored as a normal config property (not credentials).
99
- // Runtime node doesn't need it; editor uses it to request keys.
100
- this.email = (n.email && String(n.email).trim()) || "";
92
+ // apiKey is stored in credentials only.
101
93
  }
102
94
 
103
95
  RED.nodes.registerType("vectorprime-config", VectorPrimeConfigNode, {
@@ -124,9 +116,7 @@ module.exports = function (RED) {
124
116
  try {
125
117
  if (!cfg) {
126
118
  node.status({ fill: "red", shape: "ring", text: "missing config" });
127
- throw new Error(
128
- "VectorPrime config missing. Open node settings and select/create a Config."
129
- );
119
+ throw new Error("VectorPrime config missing. Open node settings and select/create a Config.");
130
120
  }
131
121
 
132
122
  const baseUrl = normalizeBaseUrl(cfg.baseUrl);
@@ -145,15 +135,14 @@ module.exports = function (RED) {
145
135
  if (!storedKey) {
146
136
  node.status({ fill: "red", shape: "ring", text: "missing api key" });
147
137
  throw new Error(
148
- "No API key. Open VectorPrime Config and click 'Get Free Key' or paste your paid key."
138
+ "No API key. Get one at https://vectorprime.tech/?utm_source=node-red&utm_medium=runtime_error&utm_campaign=node_red_contrib&utm_content=missing_key then paste it into the VectorPrime Config."
149
139
  );
150
140
  }
151
141
 
152
- // -----------------------------
153
- // AUTO-CONVERT ITEMS OPTIONS
142
+ // ------------------------------------
143
+ // AUTO-CONVERT items -> options (friendly input)
154
144
  // Backend expects: decision_id, prompt, options
155
- // Users send: {items:[...]}
156
- // -----------------------------
145
+ // ------------------------------------
157
146
  const originalPayload = msg.payload;
158
147
  let payload = msg.payload || {};
159
148
  let inputOptionsForFallback = null;
@@ -185,12 +174,14 @@ module.exports = function (RED) {
185
174
  inputOptionsForFallback = payload.options;
186
175
  }
187
176
 
188
- // Validate final expected shape
189
- if (!payload.decision_id || !payload.prompt || !payload.options) {
177
+ if (!payload || typeof payload !== "object") {
190
178
  node.status({ fill: "red", shape: "ring", text: "bad payload" });
191
- throw new Error(
192
- "Invalid payload. Provide { items:[...] } or full { decision_id, prompt, options }."
193
- );
179
+ throw new Error("Invalid payload. Provide { items:[...] } or full { decision_id, prompt, options }.");
180
+ }
181
+
182
+ if (!payload.decision_id || !payload.prompt || !payload.options || !Array.isArray(payload.options)) {
183
+ node.status({ fill: "red", shape: "ring", text: "bad payload" });
184
+ throw new Error("Invalid payload. Provide { items:[...] } or full { decision_id, prompt, options }.");
194
185
  }
195
186
 
196
187
  node.status({ fill: "blue", shape: "dot", text: "ranking..." });
@@ -213,60 +204,42 @@ module.exports = function (RED) {
213
204
  throw new Error(`VectorPrime API ${resp.status}: ${JSON.stringify(data)}`);
214
205
  }
215
206
 
216
- // -----------------------------
217
- // ✅ FIX: If backend returns a tie/flat result, do deterministic local ranking
218
- // This prevents users from seeing "random" / "input-order" ranking.
219
- // -----------------------------
207
+ // If backend returns a tie/flat result, do deterministic local ranking
220
208
  const tieBreak =
221
209
  data && data.meta && typeof data.meta === "object"
222
210
  ? data.meta.tie_break
223
211
  : null;
224
212
 
225
- const hasRanking = Array.isArray(data && data.ranking);
226
- const hasProb = data && data.probabilities;
227
-
228
- const looksTied =
229
- (tieBreak === "preserve_input_order" && isFlatProbabilities(hasProb)) ||
230
- (isFlatProbabilities(hasProb));
231
-
232
- if (looksTied && Array.isArray(inputOptionsForFallback) && inputOptionsForFallback.length > 0) {
233
- const fallback = buildFallbackRankingFromOptions(inputOptionsForFallback);
234
-
235
- // keep raw backend response for debugging
236
- msg.vp_raw = data;
237
-
238
- // replace with deterministic result
239
- data = {
240
- ...data,
241
- ranking: fallback.ranking,
242
- probabilities: fallback.probabilities,
243
- meta: {
244
- ...(data.meta || {}),
245
- notes: (data.meta && data.meta.notes ? String(data.meta.notes) + " | " : "") +
246
- "Node-RED fallback ranking applied (urgency/impact/effort) because backend result was tied/flat.",
247
- numeric_source: "node_red_fallback",
248
- tie_break: "fallback_score_then_preserve_input_order",
249
- },
250
- };
213
+ const hasProb = data && data.probabilities && typeof data.probabilities === "object";
214
+ const isFlat = hasProb ? isFlatProbabilities(data.probabilities) : false;
215
+
216
+ if ((tieBreak || isFlat) && Array.isArray(inputOptionsForFallback) && inputOptionsForFallback.length >= 2) {
217
+ const local = buildFallbackRankingFromOptions(inputOptionsForFallback);
218
+ data = data && typeof data === "object" ? data : {};
219
+ data.ranking = local.ranking;
220
+ data.probabilities = local.probabilities;
221
+ data.meta = data.meta && typeof data.meta === "object" ? data.meta : {};
222
+ data.meta.local_fallback = true;
223
+ data.meta.local_fallback_reason = tieBreak ? "backend_tie_break" : "flat_probabilities";
251
224
  }
252
225
 
253
- node.status({ fill: "green", shape: "dot", text: "ok" });
226
+ // Attach what we sent (useful for debugging + audits)
227
+ if (data && typeof data === "object") {
228
+ data.vp_input = payload;
229
+ data.vp_original_payload = originalPayload;
230
+ }
254
231
 
255
- // ✅ Replace payload with ranked result
256
232
  msg.payload = data;
257
233
 
258
- // preserve original input for debugging (no breaking changes)
259
- msg._vp_input = originalPayload;
260
-
234
+ node.status({ fill: "green", shape: "dot", text: "ok" });
261
235
  send(msg);
262
236
  done();
263
237
  } catch (err) {
264
- node.status({ fill: "red", shape: "ring", text: "failed" });
265
- node.error(err, msg);
238
+ node.status({ fill: "red", shape: "ring", text: "error" });
266
239
  done(err);
267
240
  }
268
241
  });
269
242
  }
270
243
 
271
244
  RED.nodes.registerType("vectorprime-rank", VectorPrimeRankNode);
272
- };
245
+ };