@manny-est/node-red-flowpilot 0.4.1 → 0.5.0
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 +27 -0
- package/PROJECT-OVERVIEW.md +7 -7
- package/README.md +14 -8
- package/USER-GUIDE.md +27 -10
- package/flowpilot-core.css +44 -0
- package/flowpilot.js +38 -135
- package/lib/build-core-script.js +44 -0
- package/lib/build-system-prompt.js +6 -0
- package/lib/core/apply-review.js +1677 -0
- package/lib/core/history.js +93 -0
- package/lib/core/init.js +1510 -0
- package/lib/core/main.js +1710 -0
- package/lib/core/markdown.js +165 -0
- package/lib/core/modes.js +1567 -0
- package/lib/core/redaction.js +165 -0
- package/lib/core/selection-context.js +211 -0
- package/lib/envelope.js +136 -0
- package/lib/modify-system-prompt.js +3 -1
- package/lib/storage.js +4 -0
- package/package.json +1 -2
- package/flowpilot-core.js +0 -6427
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to FlowPilot are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.5.0] - 2026-07-06
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Slash autocomplete**: type `/` in the compose box and a panel shows all available commands with descriptions. Arrow keys, Tab, and Enter navigate; Escape dismisses.
|
|
9
|
+
- **Mode-suggestion chips**: FlowPilot detects when you describe a Generate, Modify, or Build task while in a different mode and offers a one-click chip to switch — no need to type the slash command manually. A chip also surfaces after a Generate response if the flow looks like it needs a follow-up Build loop.
|
|
10
|
+
- **Build loop — hold-at-next-step pacing**: a new Settings → Behavior toggle pauses the loop at each waypoint for review instead of auto-advancing. Useful for carefully inspecting each iteration.
|
|
11
|
+
- **Build loop — checkpoint questions**: the loop can now ask a focused clarifying question at any waypoint (e.g. "the debug output shows X — did you mean to test Y?") with prefilled quick-reply options, the same mechanism Modify uses. The loop only continues once you answer.
|
|
12
|
+
- **Build loop — context-aware start**: `/build` now reads your current selection as the starting context (same as `/modify`), letting you target an existing sub-flow to extend or fix rather than always building from scratch.
|
|
13
|
+
- **Build loop — done confirmation**: a "Done" confirmation step with an explicit success/fail verdict now closes the loop instead of silently stopping.
|
|
14
|
+
- **Pop-out — debug log**: the debug log panel is now fully mirrored in the pop-out window, including the "Clear debug" button and the "Attach N to context" indicator.
|
|
15
|
+
- **Pop-out — Recall and flight log**: the Recall panel and flight-log history are now accessible from the pop-out; opening a past conversation or recalling a message works the same as in the main window.
|
|
16
|
+
- **Pop-out — prompt resize handle**: the compose box in the pop-out can now be resized vertically, matching the main window.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **Partial id-validation** (`finalizeModifyResult`): a Modify response containing a mix of valid patches and one bad node id no longer rejects the whole response — valid patches are applied and the bad ones are silently dropped. Previously a single unrecognized id caused every change in the batch to be discarded.
|
|
20
|
+
- **Redaction round-trip poisoning** (issue #7): when a Modify request targets a field that FlowPilot redacted (e.g. an HTTP Authorization header), the model's response — echoing back the `[redacted: ...]` placeholder — previously produced a silent empty diff. Now the diff step recognises placeholder values, skips those fields, and if ALL proposed changes were redacted-field-only, shows an explicit warning explaining the limitation and instructing the user to edit that field directly in the node editor.
|
|
21
|
+
- **Token credential redaction**: `Authorization: Token <value>` headers (used by Django REST Framework and similar APIs) were not caught by the existing bearer-token pattern and could be sent to the model in plain text. The `Token <credential>` form is now recognised as a separate pattern.
|
|
22
|
+
- **Recursive sentinel check**: `isSanitizeSentinel` now walks into nested arrays and objects, so a placeholder buried inside a list property (e.g. `rules[0].v`) is correctly detected and skipped instead of producing a spurious empty diff.
|
|
23
|
+
- **Group data corruption**: a plain `changes` patch could reach the `nodes` membership array of a group container (since context exposes it as a plain field), overwriting the live group membership with a stringified copy. The `nodes` array is now in `DIFF_SKIP` so Modify patches never touch it directly.
|
|
24
|
+
- **Group create/extend on mixed-membership selections**: creating or extending a group when the selection contained nodes from more than one existing group (or some grouped and some ungrouped) could silently fail or corrupt membership. The membership reconciliation step now handles the mixed case correctly.
|
|
25
|
+
- **Invalid port wiring**: FlowPilot now guards against wiring to or from a port index that doesn't exist on a node (e.g. connecting output 3 of a node that only has 2 outputs) — the bad wire is skipped with a warning instead of being applied and then causing a Node-RED canvas error.
|
|
26
|
+
- **Pop-out — settings auth failure**: opening Settings from the pop-out could fail with a 404 or 401 in certain auth configurations because the settings request was issued from the pop-out's nested route rather than through the main window. All settings reads and writes are now relayed through the parent window.
|
|
27
|
+
- **Pop-out — recall and flight-log relay**: recall and flight-log API calls are now relayed through the parent window so they resolve correctly from the pop-out.
|
|
28
|
+
|
|
29
|
+
### Internal
|
|
30
|
+
- Phase 9 refactor complete: `flowpilot-core.js` is now assembled at runtime from focused fragments under `lib/core/` (redaction, history, markdown, selection-context, apply-review, modes, main, init). The assembled script is functionally identical to the old single file; the split is purely for maintainability.
|
|
31
|
+
|
|
5
32
|
## [0.4.1] - 2026-06-29
|
|
6
33
|
|
|
7
34
|
### Fixed
|
package/PROJECT-OVERVIEW.md
CHANGED
|
@@ -115,13 +115,13 @@ including every step of the `/build` loop and every group mutation.
|
|
|
115
115
|
|
|
116
116
|
- **`flowpilot.html`** — the editor-side plugin entry point: registers the
|
|
117
117
|
sidebar panel and loads the shared client module below.
|
|
118
|
-
- **`
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
page (`lib/popout/view.html`)
|
|
124
|
-
state to/from the main editor window over `postMessage`.
|
|
118
|
+
- **`lib/core/*.js`** — the shared client logic, split into focused fragments
|
|
119
|
+
(redaction, history, markdown, selection-context, apply-review, modes, main,
|
|
120
|
+
init) and assembled at request time by `lib/build-core-script.js` into one
|
|
121
|
+
complete IIFE served at `/flowpilot/core.js`. Used by both the sidebar and
|
|
122
|
+
the pop-out window so they stay in sync by construction. The pop-out's
|
|
123
|
+
minimal page (`lib/popout/view.html`) loads this same assembled module and
|
|
124
|
+
relays state to/from the main editor window over `postMessage`.
|
|
125
125
|
- **`flowpilot.js`** — the Node-RED runtime plugin: HTTP routes
|
|
126
126
|
(`/flowpilot/*`), provider calls, response parsing/validation, and audit
|
|
127
127
|
logging.
|
package/README.md
CHANGED
|
@@ -93,26 +93,32 @@ See the [User Guide](USER-GUIDE.md#privacy-and-safety) for the full details.
|
|
|
93
93
|
|
|
94
94
|
Node-RED 4.x and 5.x, tested. Node.js 16+.
|
|
95
95
|
|
|
96
|
-
## Install
|
|
96
|
+
## Install
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
In the Node-RED editor: **Menu → Manage palette → Install**, search for
|
|
99
|
+
`@manny-est/node-red-flowpilot`, and click **Install**.
|
|
100
|
+
|
|
101
|
+
Or, from your Node-RED user directory:
|
|
99
102
|
|
|
100
103
|
```bash
|
|
101
104
|
cd ~/.node-red
|
|
102
|
-
npm install /
|
|
105
|
+
npm install @manny-est/node-red-flowpilot
|
|
103
106
|
node-red
|
|
104
107
|
```
|
|
105
108
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
Restart Node-RED (or the container) after installing or updating — FlowPilot's
|
|
110
|
+
editor UI is cached server-side, so a browser refresh alone is not enough.
|
|
111
|
+
|
|
112
|
+
For a Docker/container setup, install into the mounted Node-RED user directory.
|
|
113
|
+
If your user directory is `/data` (or `/workspaces/nodered`, etc.), this folder
|
|
114
|
+
should exist:
|
|
109
115
|
|
|
110
116
|
```text
|
|
111
117
|
<node-red-userDir>/node_modules/@manny-est/node-red-flowpilot
|
|
112
118
|
```
|
|
113
119
|
|
|
114
|
-
Restart the Node-RED container/process after
|
|
115
|
-
|
|
120
|
+
Restart the Node-RED container/process after updating the package files
|
|
121
|
+
directly — the plugin server caches assembled scripts in memory.
|
|
116
122
|
|
|
117
123
|
FlowPilot stores its own settings and logs separately from the plugin code,
|
|
118
124
|
under `<node-red-userDir>/flowpilot/`:
|
package/USER-GUIDE.md
CHANGED
|
@@ -212,14 +212,23 @@ and apply like any other change.
|
|
|
212
212
|
### Build (the agentic loop)
|
|
213
213
|
|
|
214
214
|
Describe a goal (e.g. "fetch the weather every hour and log it to a file")
|
|
215
|
-
and arm **Build**, or type `/build <goal>` directly.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
215
|
+
and arm **Build**, or type `/build <goal>` directly. If you have nodes
|
|
216
|
+
selected, FlowPilot uses them as the starting context (like Modify) — useful
|
|
217
|
+
for targeting an existing sub-flow to extend or fix.
|
|
218
|
+
|
|
219
|
+
FlowPilot plans the work, proposes a first flow, and once you apply and
|
|
220
|
+
deploy it, walks an interactive loop: attach the resulting Debug output,
|
|
221
|
+
FlowPilot reviews it against the goal, and either confirms it's working
|
|
222
|
+
(with a Done confirmation showing the debug output that proved success) or
|
|
223
|
+
proposes a fix — repeating until it works or a configurable attempt limit is
|
|
224
|
+
reached (**Settings → Behavior → Build loop iteration cap**). At any waypoint
|
|
225
|
+
FlowPilot may ask a focused clarifying question with quick-reply buttons
|
|
226
|
+
before continuing. Every proposal and fix still goes through the same
|
|
227
|
+
review-then-apply flow as Modify — FlowPilot never deploys for you.
|
|
228
|
+
|
|
229
|
+
**Hold at next step** (**Settings → Behavior**): when enabled, the loop pauses
|
|
230
|
+
at each waypoint for you to review before auto-advancing — useful for
|
|
231
|
+
careful, step-by-step inspection of each iteration.
|
|
223
232
|
|
|
224
233
|
### Document
|
|
225
234
|
|
|
@@ -263,13 +272,18 @@ them from there or by clicking **Clear chat**.
|
|
|
263
272
|
|
|
264
273
|

|
|
265
274
|
|
|
266
|
-
### Action chips
|
|
275
|
+
### Action chips and mode suggestions
|
|
267
276
|
|
|
268
277
|
When FlowPilot's reply describes a change you could make, it may offer an
|
|
269
278
|
action chip — a one-click button that switches to the suggested mode
|
|
270
279
|
(Generate/Modify/Document/Chat) with the request pre-filled. Nothing is sent
|
|
271
280
|
until you review and hit Send yourself.
|
|
272
281
|
|
|
282
|
+
FlowPilot also watches what you type: if your prompt reads like a Generate,
|
|
283
|
+
Modify, or Build task but a different mode is armed, it surfaces a suggestion
|
|
284
|
+
chip below the reply so you can switch with one click instead of typing the
|
|
285
|
+
slash command manually.
|
|
286
|
+
|
|
273
287
|

|
|
274
288
|
|
|
275
289
|

|
|
@@ -303,7 +317,9 @@ Behavior → Stream chat replies**.
|
|
|
303
317
|
|
|
304
318
|
### Slash commands
|
|
305
319
|
|
|
306
|
-
Type
|
|
320
|
+
Type `/` in the prompt box to see a floating panel listing all available
|
|
321
|
+
commands with descriptions. Arrow keys or Tab to highlight, Enter or Tab to
|
|
322
|
+
complete, Escape to dismiss.
|
|
307
323
|
|
|
308
324
|
- `/help` — show the full command/feature briefing
|
|
309
325
|
- `/generate`, `/document`, `/modify`, `/build` — arm that Execute mode
|
|
@@ -312,6 +328,7 @@ Type these directly into the prompt box:
|
|
|
312
328
|
- `/history` — open the Flight log
|
|
313
329
|
- `/settings` — open Settings
|
|
314
330
|
- `/demo` — load a sample Generate request into the prompt box
|
|
331
|
+
- `/feedback` — links to the repo and issue tracker
|
|
315
332
|
- `/compact` / `/expand` — hide/restore labels on the selected node(s),
|
|
316
333
|
instant and deterministic, no AI round-trip
|
|
317
334
|
- `/disable` / `/enable` — disable/re-enable the selected node(s) (skipped on
|
package/flowpilot-core.css
CHANGED
|
@@ -1056,6 +1056,50 @@
|
|
|
1056
1056
|
background: rgba(127, 127, 127, 0.15);
|
|
1057
1057
|
}
|
|
1058
1058
|
|
|
1059
|
+
/* Slash-command autocomplete panel */
|
|
1060
|
+
#fp-slash-suggest {
|
|
1061
|
+
position: absolute;
|
|
1062
|
+
bottom: 100%;
|
|
1063
|
+
left: 0;
|
|
1064
|
+
right: 0;
|
|
1065
|
+
margin-bottom: 4px;
|
|
1066
|
+
background: var(--red-ui-primary-background, #fff);
|
|
1067
|
+
border: 1px solid var(--red-ui-form-input-border-color, #ccc);
|
|
1068
|
+
border-radius: 6px;
|
|
1069
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
|
1070
|
+
overflow: hidden;
|
|
1071
|
+
z-index: 100;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
.fp-slash-row {
|
|
1075
|
+
display: flex;
|
|
1076
|
+
align-items: baseline;
|
|
1077
|
+
gap: 10px;
|
|
1078
|
+
padding: 6px 10px;
|
|
1079
|
+
cursor: pointer;
|
|
1080
|
+
user-select: none;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
.fp-slash-row:hover,
|
|
1084
|
+
.fp-slash-row.fp-slash-active {
|
|
1085
|
+
background: var(--red-ui-secondary-background, #f5f5f5);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
.fp-slash-cmd {
|
|
1089
|
+
font-weight: 600;
|
|
1090
|
+
font-size: 13px;
|
|
1091
|
+
min-width: 90px;
|
|
1092
|
+
color: var(--red-ui-primary-text-color, #333);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
.fp-slash-desc {
|
|
1096
|
+
font-size: 12px;
|
|
1097
|
+
color: var(--red-ui-secondary-text-color, #777);
|
|
1098
|
+
white-space: nowrap;
|
|
1099
|
+
overflow: hidden;
|
|
1100
|
+
text-overflow: ellipsis;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1059
1103
|
#fp-provider-status {
|
|
1060
1104
|
flex: 0 1 auto;
|
|
1061
1105
|
min-width: 0;
|
package/flowpilot.js
CHANGED
|
@@ -7,6 +7,8 @@ const documentSystemPrompt = require("./lib/document-system-prompt");
|
|
|
7
7
|
const modifySystemPrompt = require("./lib/modify-system-prompt");
|
|
8
8
|
const buildSystemPrompt = require("./lib/build-system-prompt");
|
|
9
9
|
const personaPrompt = require("./lib/persona-prompt");
|
|
10
|
+
const { buildCoreScript } = require("./lib/build-core-script");
|
|
11
|
+
const { extractJsonObject } = require("./lib/envelope");
|
|
10
12
|
|
|
11
13
|
module.exports = function flowPilotRuntime(RED) {
|
|
12
14
|
const storage = createStorage(RED.settings.userDir);
|
|
@@ -625,6 +627,11 @@ module.exports = function flowPilotRuntime(RED) {
|
|
|
625
627
|
// static lib/debug/view.html that loads the SAME debug-utils.js the
|
|
626
628
|
// sidebar uses).
|
|
627
629
|
//
|
|
630
|
+
// Phase 9 refactor: the SOURCE is now split into lib/core/*.js fragments
|
|
631
|
+
// (see lib/build-core-script.js for why and how), but this route's
|
|
632
|
+
// behavior is unchanged — it still serves one complete script at this
|
|
633
|
+
// same URL, just assembled instead of read off disk verbatim.
|
|
634
|
+
//
|
|
628
635
|
// INTENTIONALLY UNGATED (fixed in 0.4.1 — was needsPermission("settings.
|
|
629
636
|
// read") in 0.4.0, which broke the editor on every adminAuth-enabled
|
|
630
637
|
// instance): these are static client assets, fetched via plain <script
|
|
@@ -639,7 +646,7 @@ module.exports = function flowPilotRuntime(RED) {
|
|
|
639
646
|
// routes (settings, chat, generate, modify, etc. below) stay gated.
|
|
640
647
|
|
|
641
648
|
RED.httpAdmin.get("/flowpilot/core.js", function (req, res) {
|
|
642
|
-
res.
|
|
649
|
+
res.type("application/javascript").send(buildCoreScript());
|
|
643
650
|
});
|
|
644
651
|
|
|
645
652
|
RED.httpAdmin.get("/flowpilot/core.css", function (req, res) {
|
|
@@ -963,125 +970,6 @@ module.exports = function flowPilotRuntime(RED) {
|
|
|
963
970
|
// types or wire integrity yet (that's the next chunk) — it returns the parsed
|
|
964
971
|
// envelope so the frontend can display it for review.
|
|
965
972
|
|
|
966
|
-
// Given s[startIdx] === "{", scans forward with brace-depth counting that
|
|
967
|
-
// ignores braces inside string literals (so a value like "{{payload}}"
|
|
968
|
-
// can't be mistaken for structure) to find the index of the MATCHING
|
|
969
|
-
// closing "}". Returns -1 if the braces never balance before the string
|
|
970
|
-
// ends (truncated/malformed input).
|
|
971
|
-
function findMatchingBrace(s, startIdx) {
|
|
972
|
-
let depth = 0;
|
|
973
|
-
let inString = false;
|
|
974
|
-
let escaped = false;
|
|
975
|
-
for (let i = startIdx; i < s.length; i++) {
|
|
976
|
-
const ch = s[i];
|
|
977
|
-
if (inString) {
|
|
978
|
-
if (escaped) { escaped = false; }
|
|
979
|
-
else if (ch === "\\") { escaped = true; }
|
|
980
|
-
else if (ch === "\"") { inString = false; }
|
|
981
|
-
continue;
|
|
982
|
-
}
|
|
983
|
-
if (ch === "\"") { inString = true; }
|
|
984
|
-
else if (ch === "{") { depth++; }
|
|
985
|
-
else if (ch === "}") {
|
|
986
|
-
depth--;
|
|
987
|
-
if (depth === 0) { return i; }
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
return -1;
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
function extractJsonObject(text) {
|
|
994
|
-
if (!text) { throw new Error("Empty response from provider."); }
|
|
995
|
-
let s = String(text).trim();
|
|
996
|
-
// Strip markdown code fences if the model wrapped the JSON.
|
|
997
|
-
s = s.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
998
|
-
|
|
999
|
-
const firstBrace = s.indexOf("{");
|
|
1000
|
-
const firstBracket = s.indexOf("[");
|
|
1001
|
-
|
|
1002
|
-
// The model occasionally returns a bare top-level array (e.g.
|
|
1003
|
-
// `[ {...node...} ]`) instead of the {explanation, flow} envelope. If we
|
|
1004
|
-
// fell through to the {...} extraction below, indexOf("{")/lastIndexOf("}")
|
|
1005
|
-
// would grab just the first node object — which has no "flow" key and
|
|
1006
|
-
// fails validation. Detect this case up front and wrap it as a minimal
|
|
1007
|
-
// envelope instead.
|
|
1008
|
-
if (firstBracket !== -1 && (firstBrace === -1 || firstBracket < firstBrace)) {
|
|
1009
|
-
const lastBracket = s.lastIndexOf("]");
|
|
1010
|
-
if (lastBracket !== -1 && lastBracket > firstBracket) {
|
|
1011
|
-
try {
|
|
1012
|
-
const arr = JSON.parse(s.slice(firstBracket, lastBracket + 1));
|
|
1013
|
-
if (Array.isArray(arr)) {
|
|
1014
|
-
return { explanation: "", flow: arr };
|
|
1015
|
-
}
|
|
1016
|
-
} catch (e) {
|
|
1017
|
-
// Not a parseable array — fall through to the {...} extraction.
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
const firstObjIdx = s.indexOf("{");
|
|
1023
|
-
if (firstObjIdx === -1) {
|
|
1024
|
-
// No JSON object found at all — flagged separately from a found-
|
|
1025
|
-
// but-unparseable ({...} present, JSON.parse failed) "garbled" error,
|
|
1026
|
-
// so callers can distinguish "model just answered in prose" (tolerate)
|
|
1027
|
-
// from "model's JSON envelope is broken" (still an error).
|
|
1028
|
-
const err = new Error("Provider did not return a JSON object.");
|
|
1029
|
-
err.noJsonFound = true;
|
|
1030
|
-
throw err;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
// There may be more than one "{" before the real envelope — e.g. prose
|
|
1034
|
-
// explaining a fix that mentions inline code like "{{payload}}" before
|
|
1035
|
-
// the actual JSON (seen live: a review response started with "The
|
|
1036
|
-
// template node is using `{{payload}}` with...", and slicing from THAT
|
|
1037
|
-
// brace to the envelope's real closing "}" produced unparseable
|
|
1038
|
-
// garbage). Try each candidate "{" in order with string-aware brace
|
|
1039
|
-
// matching (findMatchingBrace, which ignores braces inside quoted
|
|
1040
|
-
// strings) rather than just slicing from the first "{" to the last
|
|
1041
|
-
// "}".
|
|
1042
|
-
//
|
|
1043
|
-
// A candidate must not just PARSE, it must also look like one of the
|
|
1044
|
-
// known envelope shapes (have at least one recognized top-level key) —
|
|
1045
|
-
// seen live: a pure-prose advice response that mentioned structured
|
|
1046
|
-
// logging included the illustrative example
|
|
1047
|
-
// `{"level":"info","event":"trivia_answer","user":"alex","correct":true}`,
|
|
1048
|
-
// which IS valid standalone JSON, so the old "first candidate that
|
|
1049
|
-
// parses wins" rule accepted it as "the envelope" and the caller threw
|
|
1050
|
-
// "no recognizable modify fields" — when the right answer was to treat
|
|
1051
|
-
// the whole reply as prose, since there was no real envelope at all.
|
|
1052
|
-
const ENVELOPE_KEYS = ["explanation", "flow", "question", "changes", "newNodes", "newWires", "removeNodes", "newGroups", "prose"];
|
|
1053
|
-
function looksLikeEnvelope(obj) {
|
|
1054
|
-
if (!obj || typeof obj !== "object" || Array.isArray(obj)) { return false; }
|
|
1055
|
-
return ENVELOPE_KEYS.some(function (k) { return k in obj; });
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
let lastError = null;
|
|
1059
|
-
let searchFrom = firstObjIdx;
|
|
1060
|
-
while (searchFrom !== -1 && searchFrom < s.length) {
|
|
1061
|
-
const end = findMatchingBrace(s, searchFrom);
|
|
1062
|
-
if (end !== -1) {
|
|
1063
|
-
try {
|
|
1064
|
-
const candidate = JSON.parse(s.slice(searchFrom, end + 1));
|
|
1065
|
-
if (looksLikeEnvelope(candidate)) { return candidate; }
|
|
1066
|
-
// Valid JSON, but not envelope-shaped (e.g. an illustrative
|
|
1067
|
-
// example embedded in prose) — keep searching rather than
|
|
1068
|
-
// accepting it.
|
|
1069
|
-
} catch (e) {
|
|
1070
|
-
lastError = e;
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
searchFrom = s.indexOf("{", searchFrom + 1);
|
|
1074
|
-
}
|
|
1075
|
-
// No candidate both parsed AND looked like a real envelope — equivalent
|
|
1076
|
-
// to "the model just answered in prose," not "the envelope is broken."
|
|
1077
|
-
// Let callers fall back to rendering this as a normal message instead
|
|
1078
|
-
// of surfacing a parse error (same noJsonFound flag the "no { at all"
|
|
1079
|
-
// branch above uses).
|
|
1080
|
-
const err = lastError || new Error("Provider's JSON object could not be parsed.");
|
|
1081
|
-
err.noJsonFound = true;
|
|
1082
|
-
throw err;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
973
|
// ---------------------------------------------------------------------
|
|
1086
974
|
// Shared helper: resolve the active provider and assemble the messages
|
|
1087
975
|
// array for a generation-style request (generate/document/modify). Split
|
|
@@ -1538,29 +1426,42 @@ module.exports = function flowPilotRuntime(RED) {
|
|
|
1538
1426
|
// Validate that changes contains no hallucinated ids, and that no id is
|
|
1539
1427
|
// both patched and marked for removal. A group id from contextGroupIds
|
|
1540
1428
|
// is allowed here too (see above) even though it's not in originalIds.
|
|
1429
|
+
// Instead of rejecting the whole response when some ids are bad, drop
|
|
1430
|
+
// only the offending patches and apply the rest — same philosophy as the
|
|
1431
|
+
// applyInsertions partial-failure fix (Phase 8.5 #11). A skippedNote in
|
|
1432
|
+
// the response body tells the user what was dropped and why.
|
|
1541
1433
|
const extraIds = changeIds.filter(function (id) { return !originalIds.has(String(id)) && !contextGroupIds.has(String(id)); });
|
|
1542
1434
|
const wronglyRemovedIds = changeIds.filter(function (id) { return removeSet.has(String(id)); });
|
|
1543
1435
|
|
|
1544
|
-
const
|
|
1545
|
-
if (extraIds.length) {
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1436
|
+
const skippedDescriptions = [];
|
|
1437
|
+
if (extraIds.length) {
|
|
1438
|
+
skippedDescriptions.push(
|
|
1439
|
+
extraIds.length === 1
|
|
1440
|
+
? "Skipped 1 change — that node wasn't in your current selection; reselect it to include it"
|
|
1441
|
+
: "Skipped " + extraIds.length + " changes — those nodes weren't in your current selection; reselect them to include them"
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
if (wronglyRemovedIds.length) {
|
|
1445
|
+
skippedDescriptions.push(
|
|
1446
|
+
wronglyRemovedIds.length === 1
|
|
1447
|
+
? "Skipped 1 change — that node was also marked for removal"
|
|
1448
|
+
: "Skipped " + wronglyRemovedIds.length + " changes — those nodes were also marked for removal"
|
|
1449
|
+
);
|
|
1557
1450
|
}
|
|
1451
|
+
if (skippedDescriptions.length > 0) {
|
|
1452
|
+
storage.appendAudit({ action: "modify_id_mismatch_partial", skipped_extra: extraIds, skipped_conflict: wronglyRemovedIds });
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
const badIdSet = new Set(extraIds.map(String).concat(wronglyRemovedIds.map(String)));
|
|
1456
|
+
const validChanges = changes.filter(function (c) {
|
|
1457
|
+
return c && c.id !== undefined && c.id !== null && !badIdSet.has(String(c.id));
|
|
1458
|
+
});
|
|
1558
1459
|
|
|
1559
1460
|
// Each patch's "set" is shallow-merged onto a copy of the original node.
|
|
1560
1461
|
// "id", "x", "y", "z" can never move via a patch — strip them
|
|
1561
1462
|
// defensively even though the prompt already forbids them.
|
|
1562
1463
|
const patchById = {};
|
|
1563
|
-
|
|
1464
|
+
validChanges.forEach(function (c) {
|
|
1564
1465
|
const set = (c.set && typeof c.set === "object") ? c.set : {};
|
|
1565
1466
|
const clean = Object.assign({}, set);
|
|
1566
1467
|
delete clean.id;
|
|
@@ -1587,7 +1488,8 @@ module.exports = function flowPilotRuntime(RED) {
|
|
|
1587
1488
|
// is, and findLiveNode() already resolves a group id to the live
|
|
1588
1489
|
// group object (Phase 8.5 C2 slice 1). This is how a group gets
|
|
1589
1490
|
// renamed/restyled — pure property edit, no new apply-side code.
|
|
1590
|
-
const
|
|
1491
|
+
const validChangeIds = validChanges.map(function (c) { return c.id; });
|
|
1492
|
+
const groupPatchIds = validChangeIds.filter(function (id) {
|
|
1591
1493
|
return contextGroupIds.has(String(id)) && !originalIds.has(String(id));
|
|
1592
1494
|
});
|
|
1593
1495
|
groupPatchIds.forEach(function (id) {
|
|
@@ -1698,6 +1600,7 @@ module.exports = function flowPilotRuntime(RED) {
|
|
|
1698
1600
|
removeNodes: finalRemoveNodes,
|
|
1699
1601
|
newGroups: newGroups
|
|
1700
1602
|
};
|
|
1603
|
+
if (skippedDescriptions.length > 0) { body.skippedNote = skippedDescriptions.join(". ") + "."; }
|
|
1701
1604
|
if (result.suggestedAction) { body.suggestedAction = result.suggestedAction; }
|
|
1702
1605
|
|
|
1703
1606
|
return { status: 200, body: body };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
// flowpilot-core.js is loaded directly in the BROWSER via a single
|
|
7
|
+
// <script src="flowpilot/core.js"> tag - one big IIFE, no module system,
|
|
8
|
+
// every function/var sharing one closure by lexical scope (not via
|
|
9
|
+
// require/import). Phase 9's refactor splits the SOURCE into focused files
|
|
10
|
+
// under lib/core/ for maintainability, but the served SCRIPT must stay
|
|
11
|
+
// exactly what it was: one concatenated text, so cross-fragment references
|
|
12
|
+
// keep working unmodified and script-loading/timing behavior never changes.
|
|
13
|
+
//
|
|
14
|
+
// Order matters only for top-level code that runs immediately at load time
|
|
15
|
+
// (e.g. a `var x = (function(){...})();` initializer) - plain function
|
|
16
|
+
// declarations are hoisted within the shared closure and safe in any
|
|
17
|
+
// order. Keep new fragments appended in the same relative order they held
|
|
18
|
+
// in the original single file unless a specific dependency says otherwise.
|
|
19
|
+
const FRAGMENT_ORDER = [
|
|
20
|
+
"redaction.js",
|
|
21
|
+
"history.js",
|
|
22
|
+
"markdown.js",
|
|
23
|
+
"selection-context.js",
|
|
24
|
+
"apply-review.js",
|
|
25
|
+
"modes.js",
|
|
26
|
+
"main.js",
|
|
27
|
+
"init.js"
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const HEADER = "(function () {\n \"use strict\";\n";
|
|
31
|
+
const FOOTER = "\n})();\n";
|
|
32
|
+
|
|
33
|
+
let cached = null;
|
|
34
|
+
|
|
35
|
+
function buildCoreScript() {
|
|
36
|
+
if (cached) { return cached; }
|
|
37
|
+
const body = FRAGMENT_ORDER.map(function (name) {
|
|
38
|
+
return fs.readFileSync(path.join(__dirname, "core", name), "utf8");
|
|
39
|
+
}).join("\n");
|
|
40
|
+
cached = HEADER + body + FOOTER;
|
|
41
|
+
return cached;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { buildCoreScript: buildCoreScript };
|
|
@@ -25,6 +25,12 @@ Example "explanation" for a trivial goal:
|
|
|
25
25
|
|
|
26
26
|
Deploy and trigger it; let me know what the debug output shows."
|
|
27
27
|
|
|
28
|
+
ALWAYS include at least one debug node in your proposed flow so the test-and-review cycle has something to observe. This is REQUIRED, not optional. Specific rules:
|
|
29
|
+
- HTTP endpoint flows (http in → function/change → http response): add a debug node tapping the function/change output BEFORE the http response node — wire the function/change output to BOTH the debug node AND the http response node.
|
|
30
|
+
- Inject-triggered flows: the debug node at the end is fine.
|
|
31
|
+
- Any other flow shape: add a debug node at the last meaningful output point.
|
|
32
|
+
Never generate a build flow without a debug node. The loop cannot review what it cannot see.
|
|
33
|
+
|
|
28
34
|
Everything below describes the envelope/rules for THIS step specifically — they work exactly as written, including the parts that say "Generate mode": for the purposes of this prompt, treat that phrase as describing this build step, not a separate mode. The "explanation" field's content rules below still apply — your "Plan:" block comes first, then that content follows immediately after it in the same field.
|
|
29
35
|
|
|
30
36
|
---
|