@manny-est/node-red-flowpilot 0.5.1 → 0.6.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/storage.js CHANGED
@@ -1,10 +1,83 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
 
4
+ // Sentinel the client sends back for an apiKey field it never actually saw
5
+ // (see maskProviderSecrets in flowpilot.js) to mean "leave the stored key
6
+ // alone". Shared between the GET/POST /flowpilot/settings routes (masking)
7
+ // and reconcileProviderSecrets below (unmasking on save) — both must agree
8
+ // on the exact string or a real key could get silently overwritten.
9
+ const API_KEY_UNCHANGED = "__FP_KEY_UNCHANGED__";
10
+
4
11
  function ensureDir(dir) {
5
12
  fs.mkdirSync(dir, { recursive: true });
6
13
  }
7
14
 
15
+ // Strips control characters (CR/LF and friends) from a freshly-typed API
16
+ // key before it's persisted — cheap insurance against a pasted value
17
+ // corrupting a future header or a log line.
18
+ function sanitizeApiKey(raw) {
19
+ return String(raw).replace(/[\x00-\x1F\x7F]/g, "");
20
+ }
21
+
22
+ // Reconciles an incoming providers list (as submitted by POST /flowpilot/
23
+ // settings, where the client only ever sees the masked sentinel/"" for
24
+ // apiKey — never a real key) against the previously-stored list, matched by
25
+ // provider id. A sentinel or missing apiKey keeps the stored key; an empty
26
+ // string clears it; anything else is a real retyped key. Also enforces the
27
+ // server-writes-only discipline for confirmedBaseUrl/confirmedAt (the
28
+ // provider-confirmation gate, B1): a client can never set or forge these,
29
+ // and confirmation is dropped whenever baseUrl or apiKey actually changed
30
+ // from what was last confirmed.
31
+ // trustedConfirmation: true ONLY when called from saveSettings' own internal
32
+ // callers (/flowpilot/test, /flowpilot/probe, right after a real passing
33
+ // check) — never from the public POST /settings path. Those two routes
34
+ // compute confirmedBaseUrl themselves (== the exact URL they just verified),
35
+ // so it's server-computed data at that point, not client input; every OTHER
36
+ // provider in the same save (anything not freshly (re)confirmed this call)
37
+ // still only keeps its OWN prior confirmation, and only while baseUrl/apiKey
38
+ // still match what was actually confirmed.
39
+ function reconcileProviderSecrets(incoming, existing, trustedConfirmation) {
40
+ const existingById = {};
41
+ (Array.isArray(existing) ? existing : []).forEach(function (p) {
42
+ if (p && p.id) { existingById[p.id] = p; }
43
+ });
44
+
45
+ return (Array.isArray(incoming) ? incoming : []).map(function (p) {
46
+ const prior = existingById[p.id];
47
+ const next = Object.assign({}, p);
48
+ delete next.hasApiKey;
49
+
50
+ if (next.apiKey === API_KEY_UNCHANGED || next.apiKey === undefined) {
51
+ next.apiKey = prior ? (prior.apiKey || "") : "";
52
+ } else if (typeof next.apiKey === "string" && next.apiKey !== "") {
53
+ next.apiKey = sanitizeApiKey(next.apiKey);
54
+ }
55
+
56
+ // typeof, not truthiness: baseUrl "" is a real, documented, supported
57
+ // value (Anthropic's "leave blank for api.anthropic.com" convention),
58
+ // so confirmedBaseUrl can legitimately BE "" too — `"" && ...` would
59
+ // silently evaluate false and lock that configuration out of
60
+ // confirmation forever. Only an actually-absent field should fail.
61
+ if (trustedConfirmation && typeof p.confirmedBaseUrl === "string") {
62
+ next.confirmedBaseUrl = p.confirmedBaseUrl;
63
+ next.confirmedAt = p.confirmedAt;
64
+ return next;
65
+ }
66
+
67
+ // Untrusted path (public POST /settings): never trust what the client
68
+ // sent, start from the prior stored state, then drop it if baseUrl or
69
+ // the (now-reconciled) apiKey no longer matches what was confirmed.
70
+ if (prior && typeof prior.confirmedBaseUrl === "string" && next.baseUrl === prior.baseUrl && next.apiKey === prior.apiKey) {
71
+ next.confirmedBaseUrl = prior.confirmedBaseUrl;
72
+ next.confirmedAt = prior.confirmedAt;
73
+ } else {
74
+ delete next.confirmedBaseUrl;
75
+ delete next.confirmedAt;
76
+ }
77
+ return next;
78
+ });
79
+ }
80
+
8
81
  function createStorage(userDir) {
9
82
  const baseDir = path.join(userDir, "flowpilot");
10
83
  const chatsDir = path.join(baseDir, "chats");
@@ -18,10 +91,15 @@ function createStorage(userDir) {
18
91
  return {
19
92
  id: "default",
20
93
  providerName: "LocalAI",
94
+ // "openai-compatible" (default) or "anthropic"
95
+ type: "openai-compatible",
21
96
  baseUrl: "http://localhost:8080",
22
97
  apiKey: "",
23
98
  model: "",
24
- temperature: 0.2
99
+ temperature: 0.2,
100
+ // Configured context window size for this provider in tokens (0 = unknown).
101
+ // When set, FlowPilot warns when the assembled prompt approaches the limit.
102
+ numCtx: 0
25
103
  };
26
104
  }
27
105
 
@@ -33,7 +111,11 @@ function createStorage(userDir) {
33
111
  maxContextChars: 12000,
34
112
  defaultContextMode: "selected",
35
113
  allowConfigContext: false,
36
- logFullContext: false,
114
+ // When true, provider turns (post-redaction messages, replies, and tool calls)
115
+ // are appended to debug.log
116
+ // (0600 perms). Auth headers/keys are never logged — only the content bytes
117
+ // that the provider actually received. Off by default (diagnostic tool).
118
+ debugLogging: false,
37
119
  streamingEnabled: true,
38
120
  // First-run welcome/warning shows until the user saves settings once.
39
121
  firstRunAcknowledged: false,
@@ -50,6 +132,9 @@ function createStorage(userDir) {
50
132
  // than cloud providers; users on that hardware raise this in Behavior
51
133
  // settings rather than living with a hardcoded ceiling.
52
134
  requestTimeoutMs: 180000,
135
+ // Hard output bound for each tool-capable agent turn. Classic completions
136
+ // remain uncapped so ordinary flow envelopes are never silently clipped.
137
+ agentTurnMaxTokens: 4096,
53
138
  // Max build->deploy->test->fix cycles the /build agentic loop will run
54
139
  // before stopping with an honest "couldn't fully verify" instead of
55
140
  // proposing another fix. Bounds against a non-converging loop burning
@@ -62,6 +147,14 @@ function createStorage(userDir) {
62
147
  // and shows a checkpoint question ("Continue with AI review, or stop?")
63
148
  // instead of auto-advancing. Default false = original auto-advance behavior.
64
149
  loopHoldStep: false,
150
+ // Routes Generate through the Phase 10 step-queue engine (graph read-back
151
+ // verification after import). The legacy path remains available when a
152
+ // user explicitly disables this setting.
153
+ enableStepQueue: true,
154
+ // W7 WRITE-tool loop. Default-off until the server/client Round 1
155
+ // plumbing has passed its integration and mandatory human live-test
156
+ // gates. This is separate from enableStepQueue (Generate checklist UI).
157
+ enableAgentWrite: false,
65
158
  // Lets the user silence the recurring secrets/size reminder bar after
66
159
  // typing an explicit acknowledgement in settings.
67
160
  suppressContextWarnings: false,
@@ -71,12 +164,15 @@ function createStorage(userDir) {
71
164
  // dedicated Node-RED credentials field is dropped by the frontend
72
165
  // regardless of this setting, via a different, always-on mechanism.
73
166
  redactionEnabled: true,
74
- // Chat-only persona slider, 1-10: 1 is a plain Node-RED engineer, 10 is
75
- // a comically over-the-top airline captain who happens to be a Node-RED
76
- // expert. 3 matches the original "subtle co-pilot" voice this replaced.
167
+ // Chat-only persona slider, 1-5 (CLAUDE-032: was 1-10, collapsed to 5
168
+ // discrete levels after live testing found the old scale's
169
+ // interpolated-between-anchors design produced no discernible voice
170
+ // difference across most of its range): 1 is a plain Node-RED engineer,
171
+ // 5 is a comically over-the-top airline captain who happens to be a
172
+ // Node-RED expert. 2 ("subtle co-pilot") is the default.
77
173
  // See lib/persona-prompt.js — generated fresh per request, never baked
78
174
  // into the persisted systemPrompt below.
79
- personaIntensity: 3,
175
+ personaIntensity: 2,
80
176
  // User-defined intent buttons: array of { label, text }.
81
177
  customIntents: [],
82
178
  systemPrompt: require("./default-system-prompt")
@@ -163,6 +259,7 @@ function createStorage(userDir) {
163
259
 
164
260
  if (!fs.existsSync(settingsFile)) {
165
261
  fs.writeFileSync(settingsFile, JSON.stringify(defaultSettings, null, 2), "utf8");
262
+ try { fs.chmodSync(settingsFile, 0o600); } catch (e) { /* best-effort */ }
166
263
  }
167
264
 
168
265
  if (!fs.existsSync(auditFile)) {
@@ -191,7 +288,13 @@ function createStorage(userDir) {
191
288
  }
192
289
  }
193
290
 
194
- function saveSettings(settings) {
291
+ // options.trustConfirmation: pass true ONLY from /flowpilot/test or
292
+ // /flowpilot/probe's own internal saveSettings call, right after a real
293
+ // passing provider check — see reconcileProviderSecrets's own comment.
294
+ // Every other caller (in particular the public POST /settings route)
295
+ // omits this, so confirmedBaseUrl/confirmedAt stay strictly
296
+ // server-computed and can never be set via a settings save.
297
+ function saveSettings(settings, options) {
195
298
  init();
196
299
 
197
300
  let current = {};
@@ -205,16 +308,19 @@ function createStorage(userDir) {
205
308
  const merged = Object.assign({}, defaultSettings, current, settings || {});
206
309
  merged.systemPrompt = fixStaleSystemPrompt(merged.systemPrompt);
207
310
  delete merged._error;
208
- // If the caller sent a providers list, it wins outright (Object.assign
209
- // already did this, but be explicit for clarity/safety).
311
+ // If the caller sent a providers list, reconcile it against what's
312
+ // actually stored (real apiKey/confirmedBaseUrl never come from the
313
+ // client — see reconcileProviderSecrets above) rather than trusting it
314
+ // outright the way Object.assign would.
210
315
  if (settings && Array.isArray(settings.providers)) {
211
- merged.providers = settings.providers;
316
+ merged.providers = reconcileProviderSecrets(settings.providers, current.providers, !!(options && options.trustConfirmation));
212
317
  }
213
318
  // Saving settings is an explicit user action; mark first-run complete so
214
319
  // the welcome/warning stops showing.
215
320
  merged.firstRunAcknowledged = true;
216
321
 
217
322
  fs.writeFileSync(settingsFile, JSON.stringify(merged, null, 2), "utf8");
323
+ try { fs.chmodSync(settingsFile, 0o600); } catch (e) { /* best-effort */ }
218
324
 
219
325
  return merged;
220
326
  }
@@ -240,7 +346,16 @@ function createStorage(userDir) {
240
346
 
241
347
  function appendTranscript(conversationId, entry) {
242
348
  init();
243
- fs.appendFileSync(transcriptFile(conversationId), JSON.stringify(entry) + "\n", "utf8");
349
+ const file = transcriptFile(conversationId);
350
+ const isNewFile = !fs.existsSync(file);
351
+ // Same 0600-on-append pattern as appendDebugLog below — transcripts hold
352
+ // full conversation content, never world-readable even on first write.
353
+ const fd = fs.openSync(file, "a", 0o600);
354
+ fs.writeSync(fd, JSON.stringify(entry) + "\n");
355
+ fs.closeSync(fd);
356
+ if (isNewFile) {
357
+ try { fs.chmodSync(file, 0o600); } catch (e) { /* best-effort */ }
358
+ }
244
359
  }
245
360
 
246
361
  // Removes a conversation's transcript file (e.g. user deletes it from the
@@ -290,6 +405,32 @@ function createStorage(userDir) {
290
405
  return defaultSettings.systemPrompt;
291
406
  }
292
407
 
408
+ const debugLogFile = path.join(baseDir, "debug.log");
409
+
410
+ // Append one JSON-lines entry to debug.log.
411
+ // Written 0600 — diagnostic data, never world-readable.
412
+ // Auth keys are NOT included (only baseUrl + model from the provider
413
+ // profile, never apiKey). The bytes logged are post-redaction: the same
414
+ // content the provider actually received.
415
+ function appendDebugLog(entry) {
416
+ init();
417
+ const line = JSON.stringify({
418
+ timestamp: new Date().toISOString(),
419
+ ...entry
420
+ });
421
+ try {
422
+ // Open with O_APPEND | O_CREAT, mode 0600 so secrets never land
423
+ // in a world-readable file even on first write.
424
+ const fd = fs.openSync(debugLogFile, "a", 0o600);
425
+ fs.writeSync(fd, line + "\n");
426
+ fs.closeSync(fd);
427
+ // Ensure 0600 regardless of umask on subsequent opens.
428
+ fs.chmodSync(debugLogFile, 0o600);
429
+ } catch (err) {
430
+ console.error("[FlowPilot] debug log write failed:", err.message);
431
+ }
432
+ }
433
+
293
434
  init();
294
435
 
295
436
  return {
@@ -298,11 +439,13 @@ function createStorage(userDir) {
298
439
  backupsDir,
299
440
  settingsFile,
300
441
  auditFile,
442
+ debugLogFile,
301
443
  getSettings,
302
444
  saveSettings,
303
445
  getActiveProvider,
304
446
  getDefaultSystemPrompt,
305
447
  appendAudit,
448
+ appendDebugLog,
306
449
  appendTranscript,
307
450
  readTranscript,
308
451
  deleteTranscript,
@@ -310,4 +453,9 @@ function createStorage(userDir) {
310
453
  };
311
454
  }
312
455
 
313
- module.exports = createStorage;
456
+ // Static, instance-independent — flowpilot.js's route handlers need the
457
+ // exact same sentinel string that reconcileProviderSecrets checks against
458
+ // above, without needing a storage instance to get it.
459
+ createStorage.API_KEY_UNCHANGED = API_KEY_UNCHANGED;
460
+
461
+ module.exports = createStorage;
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+
3
+ // W2 — Class A validator / repair layer.
4
+ //
5
+ // Rules that are mechanically checkable or repairable belong here, not in
6
+ // the model's attention budget. For each repair implemented, the
7
+ // corresponding prompt rule is deleted or shortened in the same commit —
8
+ // the win is measured in prompt shrinkage + corpus improvement, not in
9
+ // code added.
10
+ //
11
+ // Server-side mirror of the client-side DIFF_SKIP in apply-review.js.
12
+ // When a field appears here, it is stripped from changes[].set before
13
+ // the diff reaches the client (rule A6). Keep this list in sync with
14
+ // DIFF_SKIP in apply-review.js.
15
+ const DIFF_SKIP_SERVER = {
16
+ // Appearance-tab metadata (all node types)
17
+ info: 1, inputLabels: 1, outputLabels: 1, icon: 1,
18
+ // debug node display flags
19
+ console: 1, tostatus: 1, targetType: 1, statusVal: 1, statusType: 1,
20
+ // function node internal flags
21
+ noerr: 1, initialize: 1, finalize: 1,
22
+ // mqtt in/out retain handling
23
+ rh: 1
24
+ };
25
+
26
+ // A1: missing wires on a non-comment node → inject [].
27
+ // A2: comment node with non-empty wires → force empty.
28
+ // A3: x/y/z present on a node → strip (editor handles placement).
29
+ // A4: tab/subflow type → remove from array entirely.
30
+ function repairFlowNodes(nodes, repairs) {
31
+ if (!Array.isArray(nodes)) { return nodes; }
32
+ const out = [];
33
+ nodes.forEach(function (n) {
34
+ if (!n || typeof n !== "object") { return; }
35
+
36
+ // A4: reject tab/subflow — editor types, not importable nodes
37
+ if (n.type === "tab" || n.type === "subflow") {
38
+ repairs.push({ rule: "A4", detail: "removed " + n.type + " node id=" + (n.id || "?") });
39
+ return;
40
+ }
41
+
42
+ const fixed = Object.assign({}, n);
43
+
44
+ // A3: strip x/y/z (editor assigns positions on import; if present
45
+ // they cause nodes to pile up at exact coordinates instead of
46
+ // being auto-arranged)
47
+ const stripped = [];
48
+ ["x", "y", "z"].forEach(function (k) {
49
+ if (k in fixed) { delete fixed[k]; stripped.push(k); }
50
+ });
51
+ if (stripped.length) {
52
+ repairs.push({ rule: "A3", detail: "stripped " + stripped.join(",") + " from id=" + (n.id || "?") });
53
+ }
54
+
55
+ if (n.type === "comment") {
56
+ // A2: comment nodes are passive annotations — wires must be empty
57
+ if (Array.isArray(n.wires) && n.wires.some(function (p) { return Array.isArray(p) && p.length > 0; })) {
58
+ fixed.wires = [];
59
+ repairs.push({ rule: "A2", detail: "forced wires:[] on comment id=" + (n.id || "?") });
60
+ } else if (!Array.isArray(n.wires)) {
61
+ fixed.wires = [];
62
+ }
63
+ } else {
64
+ // A1: every non-comment node must have a wires array
65
+ if (!Array.isArray(n.wires)) {
66
+ fixed.wires = [];
67
+ repairs.push({ rule: "A1", detail: "injected wires:[] on id=" + (n.id || "?") + " type=" + (n.type || "?") });
68
+ }
69
+ }
70
+
71
+ out.push(fixed);
72
+ });
73
+ return out;
74
+ }
75
+
76
+ // A5: http-request node headers in {key, value} shape → transform to
77
+ // {keyType, keyValue, valueType, valueValue}. The flat shape is silently
78
+ // ignored by Node-RED; the editor uses the keyed shape exclusively.
79
+ function repairHttpHeaders(headers) {
80
+ if (!Array.isArray(headers)) { return headers; }
81
+ return headers.map(function (h) {
82
+ if (h && typeof h === "object" &&
83
+ "key" in h && "value" in h &&
84
+ !("keyType" in h)) {
85
+ return {
86
+ keyType: "other", keyValue: String(h.key || ""),
87
+ valueType: "other", valueValue: String(h.value || "")
88
+ };
89
+ }
90
+ return h;
91
+ });
92
+ }
93
+
94
+ // A6: forbidden fields in changes[].set → strip (server-side DIFF_SKIP).
95
+ // A7: group inside set → strip (group membership changes via newGroups only).
96
+ // A8: http-request headers {key,value} shape → transform.
97
+ // A9: same id in changes AND removeNodes → drop from changes.
98
+ //
99
+ // Note: redaction-placeholder stripping (formerly A8 in planning notes)
100
+ // was implemented as W0.2 (stripRedactionPlaceholders in flowpilot.js)
101
+ // and runs after this function — no duplication needed here.
102
+ function repairChanges(changes, removeNodes, repairs) {
103
+ if (!Array.isArray(changes)) { return changes; }
104
+ const removeSet = new Set(Array.isArray(removeNodes) ? removeNodes : []);
105
+
106
+ return changes.filter(function (entry) {
107
+ if (!entry || typeof entry !== "object") { return false; }
108
+ // A9: id appears in both changes and removeNodes — removeNodes wins
109
+ if (entry.id && removeSet.has(entry.id)) {
110
+ repairs.push({ rule: "A9", detail: "dropped id=" + entry.id + " from changes (also in removeNodes)" });
111
+ return false;
112
+ }
113
+ return true;
114
+ }).map(function (entry) {
115
+ if (!entry.set || typeof entry.set !== "object") { return entry; }
116
+ const cleanSet = {};
117
+ const droppedA6 = [];
118
+ let droppedGroup = false;
119
+
120
+ Object.keys(entry.set).forEach(function (k) {
121
+ // A6: forbidden internal fields
122
+ if (DIFF_SKIP_SERVER[k]) { droppedA6.push(k); return; }
123
+ // A7: group is informational context, not settable via changes
124
+ if (k === "group") { droppedGroup = true; return; }
125
+
126
+ let v = entry.set[k];
127
+ // A5: http-request node headers on the modify path
128
+ if (k === "headers") { v = repairHttpHeaders(v); }
129
+
130
+ cleanSet[k] = v;
131
+ });
132
+
133
+ if (droppedA6.length) {
134
+ repairs.push({ rule: "A6", detail: "stripped " + droppedA6.join(",") + " from set on id=" + entry.id });
135
+ }
136
+ if (droppedGroup) {
137
+ repairs.push({ rule: "A7", detail: "stripped group from set on id=" + entry.id });
138
+ }
139
+
140
+ return Object.assign({}, entry, { set: cleanSet });
141
+ });
142
+ }
143
+
144
+ // A10: newWires entries using fromId/toId → rename to from/to. The Modify
145
+ // finalizer consumes only from/to, so leaving the common model-produced
146
+ // aliases in place would make both endpoint references appear missing.
147
+ function repairNewWires(newWires, repairs) {
148
+ if (!Array.isArray(newWires)) { return newWires; }
149
+ return newWires.map(function (wire, index) {
150
+ if (!wire || typeof wire !== "object") { return wire; }
151
+ const fixed = Object.assign({}, wire);
152
+ const renamed = [];
153
+ if (!("from" in fixed) && "fromId" in fixed) {
154
+ fixed.from = fixed.fromId;
155
+ delete fixed.fromId;
156
+ renamed.push("fromId->from");
157
+ }
158
+ if (!("to" in fixed) && "toId" in fixed) {
159
+ fixed.to = fixed.toId;
160
+ delete fixed.toId;
161
+ renamed.push("toId->to");
162
+ }
163
+ if (renamed.length) {
164
+ repairs.push({ rule: "A10", detail: "renamed " + renamed.join(",") + " on newWires[" + index + "]" });
165
+ }
166
+ return fixed;
167
+ });
168
+ }
169
+
170
+ // Switch rules/wires mismatch — detect, do not repair. The rule
171
+ // alignment is a semantic contract (rules[i] corresponds to output port
172
+ // i); auto-repairing the wrong choice would silently misroute messages.
173
+ // Instead, surface as a targeted retry so the model sees exactly what's
174
+ // wrong and can fix it in one shot.
175
+ // Returns an array of { id, rulesLen, wiresLen } — empty when all clean.
176
+ function detectSwitchMismatches(changes) {
177
+ const mismatches = [];
178
+ (Array.isArray(changes) ? changes : []).forEach(function (entry) {
179
+ if (!entry || !entry.set) { return; }
180
+ const rules = entry.set.rules;
181
+ const wires = entry.set.wires;
182
+ if (Array.isArray(rules) && Array.isArray(wires) && rules.length !== wires.length) {
183
+ mismatches.push({ id: entry.id, rulesLen: rules.length, wiresLen: wires.length });
184
+ }
185
+ });
186
+ return mismatches;
187
+ }
188
+
189
+ // Top-level entry point. Takes a parsed envelope object and returns:
190
+ // { envelope, repairs, switchMismatches }
191
+ //
192
+ // - envelope: repaired copy (original not mutated)
193
+ // - repairs: array of { rule, detail } for each repair applied
194
+ // - switchMismatches: array of { id, rulesLen, wiresLen } for switch
195
+ // nodes whose rules/wires arrays have different lengths (targeted retry
196
+ // candidates — callers surface these as skippedNotes or 422 bounces)
197
+ function repairEnvelope(parsed) {
198
+ if (!parsed || typeof parsed !== "object") {
199
+ return { envelope: parsed, repairs: [], switchMismatches: [] };
200
+ }
201
+
202
+ const repairs = [];
203
+ const out = Object.assign({}, parsed);
204
+
205
+ // Flow array (Generate/Build): A1, A2, A3, A4
206
+ if (Array.isArray(out.flow)) {
207
+ out.flow = repairFlowNodes(out.flow, repairs);
208
+ }
209
+
210
+ // newNodes (Modify): A1, A2, A3, A4
211
+ if (Array.isArray(out.newNodes)) {
212
+ out.newNodes = repairFlowNodes(out.newNodes, repairs);
213
+ }
214
+
215
+ // changes (Modify): A5, A6, A7, A9
216
+ if (Array.isArray(out.changes)) {
217
+ out.changes = repairChanges(out.changes, out.removeNodes, repairs);
218
+ }
219
+
220
+ // newWires (Modify): A10
221
+ if (Array.isArray(out.newWires)) {
222
+ out.newWires = repairNewWires(out.newWires, repairs);
223
+ }
224
+
225
+ // Switch alignment check (detect only, not repair)
226
+ const switchMismatches = detectSwitchMismatches(out.changes);
227
+
228
+ return { envelope: out, repairs: repairs, switchMismatches: switchMismatches };
229
+ }
230
+
231
+ module.exports = {
232
+ repairEnvelope: repairEnvelope,
233
+ repairFlowNodes: repairFlowNodes,
234
+ repairChanges: repairChanges,
235
+ repairNewWires: repairNewWires,
236
+ detectSwitchMismatches: detectSwitchMismatches,
237
+ DIFF_SKIP_SERVER: DIFF_SKIP_SERVER
238
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manny-est/node-red-flowpilot",
3
- "version": "0.5.1",
3
+ "version": "0.6.0-beta.1",
4
4
  "description": "FlowPilot for Node-RED - an AI-powered development assistant sidebar",
5
5
  "main": "flowpilot.js",
6
6
  "keywords": [