@manny-est/node-red-flowpilot 0.6.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  All notable changes to FlowPilot are documented here.
4
4
 
5
+ ## [0.6.1] - 2026-09-02
6
+
7
+ ### Fixed
8
+ - **Deterministic run summaries actually work now.** 0.6.0 shipped this
9
+ feature — the per-item ✓/✗ outcome from a Modify or Generate run,
10
+ built from what the WRITE tools actually reported instead of the
11
+ model's own retelling — but a control-flow bug in the message-
12
+ rendering path made it unreachable for every real run: the code path
13
+ that shows the deterministic summary as primary (with the model's
14
+ own explanation demoted underneath) never executed. Every response
15
+ fell through to showing the model's own prose at full weight
16
+ instead, exactly the narrative-accuracy gap this feature exists to
17
+ close. Found and fixed during this release's own go-live testing,
18
+ now live-verified against a real agentic write.
19
+ - **Installed-package awareness was silently non-functional — FlowPilot
20
+ now actually receives your palette.** The feature that keeps
21
+ Generate/Modify from proposing node types you don't have, and lets
22
+ FlowPilot correctly answer "is X installed?", called a Node-RED API
23
+ that doesn't exist on this version's plugin interface. The call was
24
+ a silent no-op on every single request: FlowPilot never had real
25
+ palette information to work with, so the model either said as much
26
+ or, in some cases, guessed. This wasn't a timing bug or a stale
27
+ cache — the underlying data source itself never worked. Now reads
28
+ the live node registry directly and is correct immediately, from
29
+ the very first request after a restart.
30
+
5
31
  ## [0.6.0] - 2026-09-01
6
32
 
7
33
  Promoted from `0.6.0-beta.1` to the stable `latest` npm tag.
package/flowpilot.js CHANGED
@@ -1,6 +1,6 @@
1
- const http = require("http");
2
1
  const https = require("https");
3
2
  const path = require("path");
3
+ const nodeRedRegistry = require("@node-red/registry");
4
4
  const PACKAGE_VERSION = require("./package.json").version;
5
5
  const createStorage = require("./lib/storage");
6
6
  const openaiProvider = require("./lib/provider-openai-compatible");
@@ -189,7 +189,47 @@ async function performUpdateCheck() {
189
189
  }
190
190
  }
191
191
 
192
+ // Found live (0.6.1), while verifying the palette-cache fix below: Node-RED
193
+ // loads flowpilot.js TWICE per process start, and always has — package.json
194
+ // declares BOTH "nodes": {"flowpilot": "flowpilot-node-entry.js"} (itself a
195
+ // shim added earlier this same release for a related but distinct bug — see
196
+ // that shim's own comment) AND "plugins": {"flowpilot": "flowpilot.html"}.
197
+ // @node-red/registry/lib/loader.js's loadPluginConfig derives a companion
198
+ // runtime file for every "plugins" entry via the SAME basename-replace
199
+ // pattern (file.replace(/\.[^.]+$/,".js")) that caused the earlier
200
+ // flowpilot.html double-load — "flowpilot.html" -> "flowpilot.js", which
201
+ // genuinely exists, so Node-RED loads and calls it a second time completely
202
+ // independently of the "nodes" entry's shim. Confirmed live: a temporary
203
+ // diagnostic showed flowPilotRuntime executing twice per restart, and a
204
+ // registered RED.events.on("nodes-started", ...) listener firing twice.
205
+ // Unlike the client-side html/script duplication, this one can't be closed
206
+ // by renaming what "plugins" points to without duplicating flowpilot.html's
207
+ // content under a second filename — not worth the maintenance burden for
208
+ // what's fundamentally the same fix in spirit. Guard here instead.
209
+ //
210
+ // A first attempt at this guard used a plain module-level `let` flag,
211
+ // reasoning that Node's require() cache makes a module's own top-level code
212
+ // run exactly once. That reasoning doesn't hold here: confirmed by reading
213
+ // @node-red/registry/lib/loader.js directly, the "nodes" entry is loaded via
214
+ // loadNodeSet, which uses a dynamic import() of a file:// URL (see its
215
+ // `pathToFileURL(node.file)` call), while the "plugins" entry's companion
216
+ // file is loaded via loadPlugin, which uses a plain require(). Node's CJS
217
+ // require() cache and its ESM import() module cache are not the same
218
+ // cache — a module reached through both paths can genuinely execute its
219
+ // top-level code twice, each with its OWN independent closure (own
220
+ // `let flowPilotRuntimeInitialized`, own everything). Confirmed live: a
221
+ // module-level flag did NOT stop the second invocation's independent
222
+ // httpAdmin route registrations from being the ones actually serving
223
+ // requests, while the diagnostic-carrying instance sat silent.
224
+ //
225
+ // Fix: mark completion on `global` instead. Node's global object is a
226
+ // true process-wide singleton — unaffected by which loader/module-cache
227
+ // reached this file — so it's a reliable gate regardless of how many
228
+ // separate module realms this file's code ends up evaluated in.
192
229
  module.exports = function flowPilotRuntime(RED) {
230
+ if (global.__flowPilotRuntimeInitialized) { return; }
231
+ global.__flowPilotRuntimeInitialized = true;
232
+
193
233
  const storage = createStorage(RED.settings.userDir);
194
234
 
195
235
  // ---------------------------------------------------------------------
@@ -703,17 +743,42 @@ module.exports = function flowPilotRuntime(RED) {
703
743
  // their node types when relevant and otherwise stick to core nodes
704
744
  // rather than proposing types that aren't installed.
705
745
  //
706
- // The node-level RED API passed to this module has no direct registry
707
- // lookup (no RED.nodes.getNodeList), so the node list is fetched via a
708
- // loopback call to Node-RED's own admin API (the same data the palette
709
- // sidebar uses) and cached briefly — the palette rarely changes, and
710
- // every chat/generate/document/modify request goes through
711
- // buildMessages, so this must stay cheap and synchronous.
746
+ // Node-RED's admin GET /nodes route ultimately reads the runtime registry's
747
+ // getNodeList(). A previous fix tried to reach that through RED.nodes, but
748
+ // Node-RED 5's @node-red/registry/lib/util.js createNodeApi only copies a
749
+ // small allow-list onto the node-level RED API (createNode/getNode/
750
+ // eachNode/addCredentials/getCredentials/deleteCredentials)
751
+ // getNodeList is NOT one of them. Confirmed live: RED.nodes.getNodeList is
752
+ // undefined in this runtime, so describeInstalledNodes() quietly returned
753
+ // null from its catch on every request even after flows:started had fired.
754
+ //
755
+ // Use the registry module directly instead. FlowPilot already runs inside
756
+ // the same Node-RED process, and @node-red/registry is the exact source
757
+ // runtime.nodes.getNodeList() delegates to.
758
+ //
759
+ // Found live (0.6.1): this WAS a cached snapshot, through three
760
+ // successive designs (a naive restart-seeded TTL timer; a
761
+ // "nodes-started"-gated refresh; that plus a bounded settle re-check on
762
+ // top), because the original data source was an HTTP loopback to this
763
+ // same instance's own /nodes admin route — expensive enough to be worth
764
+ // caching, and, on any adminAuth-enabled instance, unable to
765
+ // authenticate itself at all (a real bug, fixed by switching to the
766
+ // in-process RED.nodes.getNodeList() call below). Once the data source
767
+ // is a synchronous, already-in-memory function call, caching it stops
768
+ // paying for itself — it only adds staleness/timing bugs, three of
769
+ // which got live-found in a row this same afternoon (the gate never
770
+ // set; set but the fetch 401ing; set and fetched but still an early
771
+ // snapshot for a slow-registering package like uibuilder). The one
772
+ // thing still worth gating is genuinely knowing nothing yet: before
773
+ // Node-RED's own "flows:started" event (the non-deprecated alias of
774
+ // "nodes-started", confirmed emitted from
775
+ // @node-red/runtime/lib/flows/index.js right after every active flow's
776
+ // own start() resolves) has fired at least once,
777
+ // describeInstalledNodes() returns null — "don't know yet" instead of a
778
+ // false "confirmed absent."
712
779
  // ---------------------------------------------------------------------
713
- let installedNodesCache = null;
714
- let installedNodesCacheAt = 0;
715
- let installedNodesRefreshInFlight = false;
716
- const INSTALLED_NODES_CACHE_TTL_MS = 5 * 60 * 1000;
780
+ let nodesRegistryReady = false;
781
+ RED.events.on("flows:started", function () { nodesRegistryReady = true; });
717
782
 
718
783
  function buildInstalledNodesContent(list) {
719
784
  if (!Array.isArray(list)) { return null; }
@@ -752,39 +817,13 @@ module.exports = function flowPilotRuntime(RED) {
752
817
  return content;
753
818
  }
754
819
 
755
- function refreshInstalledNodesCache() {
756
- if (installedNodesRefreshInFlight) { return; }
757
- installedNodesRefreshInFlight = true;
758
-
759
- const root = String(RED.settings.httpAdminRoot || "/").replace(/\/+$/, "");
760
- const req = http.get({
761
- host: "127.0.0.1",
762
- port: RED.settings.uiPort,
763
- path: root + "/nodes",
764
- headers: { Accept: "application/json" },
765
- timeout: 5000
766
- }, function (res) {
767
- const chunks = [];
768
- res.on("data", function (chunk) { chunks.push(chunk); });
769
- res.on("end", function () {
770
- installedNodesRefreshInFlight = false;
771
- if (res.statusCode !== 200) { return; }
772
- try {
773
- const list = JSON.parse(Buffer.concat(chunks).toString("utf8"));
774
- installedNodesCache = buildInstalledNodesContent(list);
775
- installedNodesCacheAt = Date.now();
776
- } catch (err) { /* leave previous cache value in place */ }
777
- });
778
- });
779
- req.on("error", function () { installedNodesRefreshInFlight = false; });
780
- req.on("timeout", function () { req.destroy(); installedNodesRefreshInFlight = false; });
781
- }
782
-
783
820
  function describeInstalledNodes() {
784
- if (Date.now() - installedNodesCacheAt > INSTALLED_NODES_CACHE_TTL_MS) {
785
- refreshInstalledNodesCache();
821
+ if (!nodesRegistryReady) { return null; }
822
+ try {
823
+ return buildInstalledNodesContent(nodeRedRegistry.getNodeList());
824
+ } catch (err) {
825
+ return null;
786
826
  }
787
- return installedNodesCache;
788
827
  }
789
828
 
790
829
  // Chat-only: the user's base system prompt plus a freshly-generated
package/lib/core/modes.js CHANGED
@@ -1259,7 +1259,19 @@
1259
1259
  updateSelectionStatus();
1260
1260
  return true;
1261
1261
  }
1262
- if (data.prose) {
1262
+ // finalizeSimpleGeneration/finalizeModifyResult set prose:true on
1263
+ // EVERY agent-strategy final response, whether or not real
1264
+ // WRITE-tool work happened this run — it's the server's generic
1265
+ // "no classic flow/changes to review" signal, not a claim that
1266
+ // nothing happened. When real write results are attached
1267
+ // (data._agentWriteResults, added client-side in handleStep before
1268
+ // onDone fires), the caller's own deterministic-summary rendering
1269
+ // needs to run instead of this generic prose bubble — bug found
1270
+ // live during the 0.6.0 go-live smoke test: this unconditional
1271
+ // early-return made C1's entire deterministic-summary/prose-
1272
+ // demotion feature unreachable for every real agent-strategy run.
1273
+ var hasRealWriteResults = Array.isArray(data._agentWriteResults) && data._agentWriteResults.length > 0;
1274
+ if (data.prose && !hasRealWriteResults) {
1263
1275
  if (looksLikeToolEnvelope(data.explanation)) {
1264
1276
  handleExecuteError("FlowPilot's reply didn't come through as expected.", data.explanation);
1265
1277
  updateSelectionStatus();
@@ -1320,7 +1332,7 @@
1320
1332
  // executable flows (not documentation-only comment nodes) and when no
1321
1333
  // loop is already active. The secondary "Just add to canvas" button is
1322
1334
  // always shown alongside it as an escape hatch.
1323
- var _hasDeployable = flow.some(function (n) { return n && n.type !== "comment" && n.type !== "group"; });
1335
+ var _hasDeployable = (flow || []).some(function (n) { return n && n.type !== "comment" && n.type !== "group"; });
1324
1336
  var _buildOnImported = (goalPrompt && !activeBuildLoop && _hasDeployable)
1325
1337
  ? function (importResult) { startBuildLoop(goalPrompt, flow, importResult); }
1326
1338
  : null;
@@ -2019,7 +2031,7 @@
2019
2031
  // also runs verifyImportedNodes so the todo record still gets checked
2020
2032
  // off. Without a build loop, fall back to a plain "Add to canvas"
2021
2033
  // button that still fires the verify callback.
2022
- var _hasDeployable = flow.some(function (n) { return n && n.type !== "comment" && n.type !== "group"; });
2034
+ var _hasDeployable = (flow || []).some(function (n) { return n && n.type !== "comment" && n.type !== "group"; });
2023
2035
  var _wantLoop = goalPrompt && !activeBuildLoop && _hasDeployable;
2024
2036
  var _onImported = _wantLoop
2025
2037
  ? function (importResult) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manny-est/node-red-flowpilot",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "FlowPilot for Node-RED - an AI-powered development assistant sidebar",
5
5
  "main": "flowpilot.js",
6
6
  "keywords": [