@manny-est/node-red-flowpilot 0.5.2 → 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.
@@ -204,35 +204,38 @@
204
204
  ? linksTouchingNodes(rawNodes)
205
205
  : ((sel && sel.links) ? sel.links : []);
206
206
  }
207
- // Collect config nodes (mqtt-broker, tls-config, etc.) that the
208
- // selected nodes reference. Config nodes are shared configuration
209
- // objects not on the canvas the model needs their ids and
210
- // non-credential properties to reference or create them in Modify.
211
- // Detected via each live node's _def.defaults: any property whose
212
- // propDef.type is a type name (rather than a value type like "str")
213
- // is a config-node reference; n[k] is the referenced node's id.
214
- var configNodeMap = {};
215
- rawNodes.forEach(function (n) {
216
- var typeDef = n._def;
217
- if (!typeDef || !typeDef.defaults) { return; }
218
- Object.keys(typeDef.defaults).forEach(function (k) {
219
- var propDef = typeDef.defaults[k];
220
- if (propDef && propDef.type && typeof n[k] === "string" && n[k] &&
221
- !configNodeMap[n[k]]) {
222
- var configNode = RED.nodes.node(n[k]);
223
- if (configNode && configNode.type) {
224
- configNodeMap[n[k]] = configNode;
207
+ var configNodes;
208
+ if (currentSettings.allowConfigContext === true) {
209
+ // Collect config nodes (mqtt-broker, tls-config, etc.) that the
210
+ // selected nodes reference. Config nodes are shared configuration
211
+ // objects not on the canvas the model needs their ids and
212
+ // non-credential properties to reference or create them in Modify.
213
+ // Detected via each live node's _def.defaults: any property whose
214
+ // propDef.type is a type name (rather than a value type like "str")
215
+ // is a config-node reference; n[k] is the referenced node's id.
216
+ var configNodeMap = {};
217
+ rawNodes.forEach(function (n) {
218
+ var typeDef = n._def;
219
+ if (!typeDef || !typeDef.defaults) { return; }
220
+ Object.keys(typeDef.defaults).forEach(function (k) {
221
+ var propDef = typeDef.defaults[k];
222
+ if (propDef && propDef.type && typeof n[k] === "string" && n[k] &&
223
+ !configNodeMap[n[k]]) {
224
+ var configNode = RED.nodes.node(n[k]);
225
+ if (configNode && configNode.type) {
226
+ configNodeMap[n[k]] = configNode;
227
+ }
225
228
  }
226
- }
229
+ });
227
230
  });
228
- });
229
- var configNodes = Object.keys(configNodeMap).map(function (id) {
230
- return sanitizeNode(configNodeMap[id]);
231
- });
231
+ configNodes = Object.keys(configNodeMap).map(function (id) {
232
+ return sanitizeNode(configNodeMap[id]);
233
+ });
234
+ }
232
235
 
233
236
  return {
234
237
  nodes: rawNodes.map(sanitizeNode),
235
238
  connections: buildConnections(rawNodes, rawLinks),
236
- configNodes: configNodes.length ? configNodes : undefined
239
+ configNodes: Array.isArray(configNodes) && configNodes.length ? configNodes : undefined
237
240
  };
238
241
  }
@@ -33,11 +33,12 @@ Whenever the user describes something they want built, changed, fixed, wired up,
33
33
  To do this, end your response with a hidden data block: on its own, after all visible reply text, with nothing after it and not inside a code fence, write the literal marker on its own line followed immediately by a single JSON object:
34
34
 
35
35
  <<<FLOWPILOT_DATA>>>
36
- {"suggestedAction": {"mode": "generate" | "document" | "modify", "prompt": "...", "selectionHint": "..."}}
36
+ {"suggestedAction": {"mode": "generate" | "document" | "modify", "prompt": "...", "selectionHint": "...", "targetNodeIds": "all" | ["real-node-id", "..."]}}
37
37
 
38
38
  - "mode": which FlowPilot action the chip switches to.
39
- - "prompt": the exact instruction text to pre-fill in the user's compose box — written as a ready-to-send request to FlowPilot, in the user's voice.
39
+ - "prompt": the exact instruction text to pre-fill in the user's compose box — written as a ready-to-send request to FlowPilot, in the user's voice. Keep it as close as possible to the user's own words and requested scope. Do not expand it into a longer spec or add requirements, implementation choices, or assumptions the user did not state.
40
40
  - "selectionHint" (optional): plain-language description of which node(s) the user should select before sending (only useful for "modify"/"document", which act on a selection).
41
+ - "targetNodeIds" (only for "modify"/"document"): REQUIRED whenever the current selection, active flow/tab, or other provided context makes the target resolvable. Use "all" when the entire active flow/tab is the resolved target, or a non-empty array of real node ids when a specific subset is the resolved target. Omit it only when you genuinely cannot resolve the target from the current context. Modify and Document require a resolved node set: never say or imply in "selectionHint" that no selection is needed unless "targetNodeIds" supplies that target, and never emit an empty array.
41
42
 
42
43
  The user reviews the prepared prompt and clicks the chip themselves — nothing is sent automatically. Skip the data block only when there's truly no actionable follow-up (e.g. plain factual Q&A, status checks).
43
44
 
@@ -48,7 +49,7 @@ For example, for "Add a debug node after the inject" (asked in Chat):
48
49
  CORRECT:
49
50
  Sure — that's a quick addition. Switch to Modify with the inject node selected and I'll wire a debug node after it for you to review.
50
51
  <<<FLOWPILOT_DATA>>>
51
- {"suggestedAction": {"mode": "modify", "prompt": "Add a debug node after the inject node.", "selectionHint": "Select the inject node first."}}
52
+ {"suggestedAction": {"mode": "modify", "prompt": "Add a debug node after the inject node.", "selectionHint": "Select the inject node first.", "targetNodeIds": ["the-real-inject-node-id"]}}
52
53
 
53
54
  WRONG — do not do this, even partially:
54
55
  I'll add a debug node right after the inject node so you can inspect its output.
@@ -16,11 +16,12 @@ The user is currently in Document mode, which produces a single read-me comment
16
16
  When one of these applies, do NOT produce the {"explanation", "flow"} JSON envelope. Instead, respond in plain text (no JSON, no code fences) addressing what they actually asked, and end your reply with a hidden data block: on its own line, after all visible text, not inside a code fence:
17
17
 
18
18
  <<<FLOWPILOT_DATA>>>
19
- {"suggestedAction": {"mode": "generate" | "modify" | "chat", "prompt": "...", "selectionHint": "..."}}
19
+ {"suggestedAction": {"mode": "generate" | "modify" | "chat", "prompt": "...", "selectionHint": "...", "targetNodeIds": "all" | ["real-node-id", "..."]}}
20
20
 
21
21
  - "mode": "generate"/"modify" if their request matches one of those actions instead; "chat" if it's a question or remark with no further action needed.
22
- - "prompt": the exact instruction text to pre-fill in their compose box after switching modes, written as a ready-to-send request in the user's voice.
22
+ - "prompt": the exact instruction text to pre-fill in their compose box after switching modes, written as a ready-to-send request in the user's voice. Keep it as close as possible to the user's own words and requested scope. Do not expand it into a longer spec or add requirements, implementation choices, or assumptions the user did not state.
23
23
  - "selectionHint" (optional): for "modify", which node(s) to select first (Generate needs no selection).
24
+ - "targetNodeIds" (only for "modify"): REQUIRED whenever the current selection, active flow/tab, or other provided context makes the target resolvable. Use "all" for the entire active flow/tab, or a non-empty array of real node ids for a resolved subset. Omit it only when you genuinely cannot resolve the target from the current context. Never imply no selection is needed for Modify unless this field supplies the target, and never emit an empty array.
24
25
 
25
26
  The data block (marker and JSON) is never shown to the user — keep your visible reply complete on its own. If the request DOES call for documenting the selection, ignore this section entirely and proceed normally below.
26
27
 
package/lib/envelope.js CHANGED
@@ -101,18 +101,24 @@ function extractJsonObject(text) {
101
101
  // "no recognizable modify fields" — when the right answer was to treat
102
102
  // the whole reply as prose, since there was no real envelope at all.
103
103
  const ENVELOPE_KEYS = ["explanation", "flow", "question", "changes", "newNodes", "newWires", "removeNodes", "newGroups", "prose"];
104
+ const ENVELOPE_KEY_PATTERN = new RegExp(
105
+ "[\\\"'](?:" + ENVELOPE_KEYS.join("|") + ")[\\\"']\\s*:", "i"
106
+ );
104
107
  function looksLikeEnvelope(obj) {
105
108
  if (!obj || typeof obj !== "object" || Array.isArray(obj)) { return false; }
106
109
  return ENVELOPE_KEYS.some(function (k) { return k in obj; });
107
110
  }
108
111
 
109
112
  let lastError = null;
113
+ let envelopeSyntaxFound = false;
110
114
  let searchFrom = firstObjIdx;
111
115
  while (searchFrom !== -1 && searchFrom < s.length) {
112
116
  const end = findMatchingBrace(s, searchFrom);
117
+ const candidateText = end !== -1 ? s.slice(searchFrom, end + 1) : s.slice(searchFrom);
118
+ if (ENVELOPE_KEY_PATTERN.test(candidateText)) { envelopeSyntaxFound = true; }
113
119
  if (end !== -1) {
114
120
  try {
115
- const candidate = JSON.parse(s.slice(searchFrom, end + 1));
121
+ const candidate = JSON.parse(candidateText);
116
122
  if (looksLikeEnvelope(candidate)) { return candidate; }
117
123
  // Valid JSON, but not envelope-shaped (e.g. an illustrative
118
124
  // example embedded in prose) — keep searching rather than
@@ -123,13 +129,13 @@ function extractJsonObject(text) {
123
129
  }
124
130
  searchFrom = s.indexOf("{", searchFrom + 1);
125
131
  }
126
- // No candidate both parsed AND looked like a real envelope equivalent
127
- // to "the model just answered in prose," not "the envelope is broken."
128
- // Let callers fall back to rendering this as a normal message instead
129
- // of surfacing a parse error (same noJsonFound flag the "no { at all"
130
- // branch above uses).
132
+ // No candidate both parsed AND looked like a real envelope. A candidate
133
+ // that used a recognized envelope key was an attempted structured reply,
134
+ // even if its JSON was malformed; callers must surface that as a parse
135
+ // error rather than trusting and displaying the raw JSON as prose. Valid
136
+ // unrelated snippets embedded in prose still take the noJsonFound path.
131
137
  const err = lastError || new Error("Provider's JSON object could not be parsed.");
132
- err.noJsonFound = true;
138
+ if (!envelopeSyntaxFound) { err.noJsonFound = true; }
133
139
  throw err;
134
140
  }
135
141
 
@@ -10,17 +10,18 @@ const modeRouting = `Before generating — check this is actually a "generate" r
10
10
  The user is currently in Generate mode, which always produces a NEW, disconnected flow fragment (see the JSON format below). If their message is NOT actually asking for something new to be built, don't force it into that shape. In particular:
11
11
 
12
12
  - It asks to change, fix, rename, rewire, or remove something in their EXISTING flow/selection ("rename this node", "fix the bug in...", "change the topic to...", "delete the debug node", "wire X to Y") — that's a Modify request.
13
- - It asks you to explain, summarize, or write documentation for the existing flow/selection, with nothing new to build — that's a Document request.
14
- - It's a question, troubleshooting request, or conversational remark that doesn't call for new nodes at all — that's general Chat.
13
+ - It asks you to write documentation for the existing flow/selection as a durable artifact to keep (a read-me, walkthrough, or similar), with nothing new to build — that's a Document request.
14
+ - It's a question, an explanation request, a troubleshooting request, or a conversational remark that doesn't call for new nodes at all — that's general Chat, even if the question itself starts with "explain" (e.g. "explain what this does" is Chat, not Document, unless they explicitly want a written artifact out of it).
15
15
 
16
16
  When one of these applies, do NOT produce the {"explanation", "flow"} JSON envelope. Instead, respond in plain text (no JSON, no code fences) addressing what they actually asked — answer the question, or explain that this looks like a Modify/Document/Chat request — and end your reply with a hidden data block: on its own line, after all visible text, not inside a code fence:
17
17
 
18
18
  <<<FLOWPILOT_DATA>>>
19
- {"suggestedAction": {"mode": "modify" | "document" | "chat", "prompt": "...", "selectionHint": "..."}}
19
+ {"suggestedAction": {"mode": "modify" | "document" | "chat", "prompt": "...", "selectionHint": "...", "targetNodeIds": "all" | ["real-node-id", "..."]}}
20
20
 
21
- - "mode": "modify" or "document" if their request matches one of those actions instead; "chat" if it's a question or remark with no further action needed.
21
+ - "mode": "modify" or "document" if their request matches one of those actions instead; "chat" if it's a question, explanation, or remark with no further action needed — including questions about the existing flow, since answering IS the action (don't pick "document" just because the question mentions the flow).
22
22
  - "prompt": the exact instruction text to pre-fill in their compose box after switching modes, written as a ready-to-send request in the user's voice.
23
23
  - "selectionHint" (optional): for "modify"/"document", plain-language description of which node(s) to select first (Generate needs no selection, so this never applies to a "generate" suggestion here).
24
+ - "targetNodeIds" (optional, only for "modify"/"document"): "all" for the entire active flow/tab, or a non-empty array of real node ids for a resolved subset. Omit when no resolved target is known. Never imply no selection is needed for Modify/Document unless this field supplies the target.
24
25
 
25
26
  The data block (marker and JSON) is never shown to the user — keep your visible reply complete on its own. If the request DOES call for a new flow fragment, ignore this section entirely and proceed normally below.
26
27
 
@@ -18,33 +18,58 @@ Respond with a SINGLE JSON object and nothing else — no markdown code fences,
18
18
 
19
19
  "changes", "newNodes", "newWires", "removeNodes", and "newGroups" are all OPTIONAL. Only include them when the instruction calls for it. Values in \`set.*\` must be the exact final value you want the property to have — FlowPilot reads the live graph back after apply and checks each property against what you specified here. Keep your response as SHORT as possible: never restate a node that isn't changing.`;
20
20
 
21
+ const agentWriteRules = `IMPORTANT — WRITE-tool execution overrides the other response-envelope instructions for this request.
22
+
23
+ - You MUST execute the requested modification with the available WRITE tools. Do NOT return \`changes\`, \`newNodes\`, \`newWires\`, \`removeNodes\`, or \`newGroups\` instead of calling tools.
24
+ - Before the first call, divide the request into numbered semantic plan items. Distinct requested outcomes joined by "and" are normally separate items; do not collapse them merely because they arrived in one sentence.
25
+ - Make exactly one WRITE tool call for the next numbered plan item, then STOP and wait for its tool result before continuing. NEVER make multiple tool calls in the same assistant response, even when the calls are independent. A single \`apply_step\` may bundle the small set of related property changes, new node, and immediate wires needed to complete that one semantic item; do not split one item into calls for individual fields.
26
+ - Do not bundle unrelated requested outcomes into one plan item. For example, inserting a functional node plus its immediate rewiring is one \`apply_step\`; adding a separate comment node is a second \`apply_step\` after the first result.
27
+ - Use \`apply_step\` for property changes, node insertion, and wiring; \`remove_step\` for a node removal; and \`rename_node\` for a rename.
28
+ - After each result, use its structural checks to confirm what actually landed, then call the tool for the next plan item.
29
+ - If the request does NOT belong in Modify mode at all, call \`redirect_mode\` exactly once instead of any WRITE tool or \`ask_user\`. This is the forced-tool-turn escape hatch for Generate/Document/Chat mismatches; it must never mutate the flow.
30
+ - If a genuinely important detail is missing or uncertain for a REAL modify request, call \`ask_user\` and wait for the answer rather than guessing.
31
+ - Never use \`ask_user\` to triage a mode mismatch. \`ask_user\` is only for missing details inside a modify request that should still stay in Modify mode once answered.
32
+ - A tool result shaped as \`{"unsupported":true,"operation":"...","reason":"...","available":[...]}\` means that operation is outside the current WRITE surface. Use its reason and available list to replan; do not retry the same unsupported operation or smuggle it through the response envelope.
33
+ - If part of the request cannot be achieved with the available WRITE tools, complete every part that can be achieved and state the remainder plainly in the final explanation; do not abandon the whole request or redirect modes. Never emit flow JSON in your final message — it will be discarded.
34
+ - Only after every plan item has been executed, return the normal single JSON response with a concise explanation of what was completed. Omit \`changes\`, \`newNodes\`, \`newWires\`, \`removeNodes\`, and \`newGroups\` so the already-applied work is not proposed a second time.`;
35
+
21
36
  const modeRouting = `Before modifying — check this is actually a "modify" request:
22
37
 
23
38
  The user is currently in Modify mode, which proposes changes to their SELECTED nodes (given above as context). If their message is NOT actually asking to change, fix, rewire, add to, or remove something from THAT selection, don't force it into that shape. In particular:
24
39
 
25
40
  - It asks for an unrelated NEW flow or feature that doesn't build on the selection ("build me a separate flow that...", "create a new flow for...") — that's a Generate request.
26
- - It asks you to explain, summarize, or write documentation for the selection, with nothing to change — that's a Document request.
27
- - It's a pure question about the selection with no fix to proposesee "Diagnostic / review instructions" below for how this interacts with "review"-style requests.
41
+ - It asks you to produce documentation for the selection as an artifact the user can keep on the canvas or share later — that's a Document request.
42
+ - It asks a plain factual, diagnostic, or conversational question about the selection, and the answer itself is the goal rather than a new read-me artifact that's Chat, even if the question is about what the flow does.
43
+ - "Review this / what's wrong here?" stays in Modify ONLY when you can point to a concrete fix to propose immediately. If the answer is purely explanatory or "nothing is wrong", redirect to Chat instead.
28
44
 
29
- When one of these applies, do NOT produce the {"explanation", "changes", ...} JSON envelope. Instead, respond in plain text (no JSON, no code fences) addressing what they actually asked, and end your reply with a hidden data block: on its own line, after all visible text, not inside a code fence:
45
+ When one of these applies during an ordinary non-tool turn, do NOT produce the {"explanation", "changes", ...} JSON envelope. Instead, respond in plain text (no JSON, no code fences) addressing what they actually asked, and end your reply with a hidden data block: on its own line, after all visible text, not inside a code fence:
30
46
 
31
47
  <<<FLOWPILOT_DATA>>>
32
- {"suggestedAction": {"mode": "generate" | "document" | "chat", "prompt": "...", "selectionHint": "..."}}
48
+ {"suggestedAction": {"mode": "generate" | "document" | "chat", "prompt": "...", "selectionHint": "...", "targetNodeIds": "all" | ["real-node-id", "..."]}}
33
49
 
34
50
  - "mode": "generate"/"document" if their request matches one of those actions instead; "chat" if it's a question or remark with no further action needed.
35
- - "prompt": the exact instruction text to pre-fill in their compose box after switching modes, written as a ready-to-send request in the user's voice.
51
+ - "prompt": the exact instruction text to pre-fill in their compose box after switching modes, written as a ready-to-send request in the user's voice. Keep it as close as possible to the user's own words and requested scope. Do not expand it into a longer spec or add requirements, implementation choices, or assumptions the user did not state.
36
52
  - "selectionHint" (optional): for "document", which node(s) to select first (Generate needs no selection).
53
+ - "targetNodeIds" (only for "document"): REQUIRED whenever the current selection, active flow/tab, or other provided context makes the target resolvable. Use "all" for the entire active flow/tab, or a non-empty array of real node ids for a resolved subset. Omit it only when you genuinely cannot resolve the target from the current context. Never imply no selection is needed for Document unless this field supplies the target, and never emit an empty array.
37
54
 
38
55
  Example — for "Explain what this flow does and how the nodes connect.", give
39
56
  a complete visible explanation, then end with:
40
57
  <<<FLOWPILOT_DATA>>>
41
- {"suggestedAction": {"mode": "document", "prompt": "Explain what this flow does and how the nodes connect.", "selectionHint": "Keep these flow nodes selected."}}
58
+ {"suggestedAction": {"mode": "document", "prompt": "Explain what this flow does and how the nodes connect.", "selectionHint": "Use these flow nodes.", "targetNodeIds": ["real-node-id-1", "real-node-id-2"]}}
42
59
 
43
60
  Example — for "What would happen if the inject node had repeat set to 5?",
44
61
  answer the question without proposing a change, then end with:
45
62
  <<<FLOWPILOT_DATA>>>
46
63
  {"suggestedAction": {"mode": "chat", "prompt": "What would happen if the inject node had repeat set to 5?"}}
47
64
 
65
+ If you are on a WRITE-tool execution turn where tools are required, you CANNOT use the plain-text hidden-block form above. In that case call \`redirect_mode\` exactly once with:
66
+ - "mode": "generate", "document", or "chat" according to the same rules above.
67
+ - "prompt": the ready-to-send follow-up prompt, kept as close as possible to the user's own words and requested scope. Do not expand it into a longer spec or add requirements they did not state.
68
+ - "explanation": the visible reply the user should see.
69
+ - "selectionHint"/"targetNodeIds": only when redirecting to Document. If the target is resolvable from the current selection, active flow/tab, or other provided context, "targetNodeIds" is required and must be "all" or a non-empty array of real node ids.
70
+
71
+ Use \`redirect_mode\` only for a true mode mismatch. Do not use it for permission-seeking, clarification, or normal modify work.
72
+
48
73
  The data block (marker and JSON) is never shown to the user — keep your visible reply complete on its own. If the request DOES call for a modification of the selection, ignore this section entirely and proceed normally below.
49
74
 
50
75
  IMPORTANT: this escape hatch is ONLY for requests that belong to a different
@@ -105,10 +130,11 @@ const changeRules = `Rules for "changes" (sparse patches against the existing se
105
130
  4. To ADD a new connection between existing nodes (e.g. a missing wire), use "newWires" — it works for any pair of nodes, not just new ones. Example: to add a wire from node A's first output to node B: {"from": "<A-id>", "fromPort": 0, "to": "<B-id>"}.
106
131
  5. To REPLACE or REORGANIZE the full set of connections FROM an existing node (change which targets it reaches): put that node's complete new "wires" array in "set.wires". Give the FULL new value for every output port. Only use this when the instruction explicitly asks to reorganize or replace that node's connections — not just to add one.
107
132
  6. Do not put "wires" in "set" when you only need to ADD a connection — use "newWires" for that instead.
108
- 7. Never include "id" inside "set" it cannot change via a patch. (x/y/z are stripped automatically.)
109
- 8. An id must not appear in both "changes" and "removeNodes".
110
- 9. A node's "group" field in context is INFORMATIONAL ONLY — do not put "group" in "set" (it is stripped). To add/remove/rename a group, use "newGroups" instead. Exception: to rename the group itself, target the group's own "id" with a "changes" entry, e.g. {"id": "<group's id>", "set": {"name": "New Name"}} — never include "nodes" in that set.
111
- 10. Do not include internal default or Appearance-tab fields in \`set\` (e.g. \`info\`, \`icon\`, \`inputLabels\`, \`outputLabels\`, node-type internal flags) they are stripped server-side and never reach the canvas.`;
133
+ 7. If the instruction is to insert a node "between" two existing nodes that are already directly wired (A→B), treat it as a rewire: remove the original direct A→B wire as part of the same operation, and leave only A→new→B.
134
+ 8. Never include "id" inside "set" it cannot change via a patch. (x/y/z are stripped automatically.)
135
+ 9. An id must not appear in both "changes" and "removeNodes".
136
+ 10. A node's "group" field in context is INFORMATIONAL ONLY — do not put "group" in "set" (it is stripped). To add/remove/rename a group, use "newGroups" instead. Exception: to rename the group itself, target the group's own "id" with a "changes" entry, e.g. {"id": "<group's id>", "set": {"name": "New Name"}} — never include "nodes" in that set.
137
+ 11. Do not include internal default or Appearance-tab fields in \`set\` (e.g. \`info\`, \`icon\`, \`inputLabels\`, \`outputLabels\`, node-type internal flags) — they are stripped server-side and never reach the canvas.`;
112
138
 
113
139
  const switchRules = `Special case — a "switch" node's "rules" and "wires" must stay aligned by index:
114
140
 
@@ -264,7 +290,31 @@ the selection" section showing each one's id, type, name, and non-credential pro
264
290
  **Do NOT use "newWires" for config node connections** — newWires is for canvas port
265
291
  connections (output → input). Config node references are property VALUES, not canvas wires.`;
266
292
 
267
- const clarifyingQuestion = `Asking a clarifying question instead of modifying:
293
+ function clarifyingQuestion(opts) {
294
+ if (opts && opts.agentWriteEnabled) {
295
+ return `Asking a clarifying question instead of modifying:
296
+
297
+ You MUST ask a clarifying question when the instruction is too vague to act on safely — a key detail is missing that would mean guessing about something that matters. This includes:
298
+
299
+ - Generic instructions like "Fix this", "Fix the flow", "Fix it", "Complete this", "Finish this", "Fix up" without specifying what needs fixing
300
+ - Vague goals like "Improve this", "Optimize this", "Better this" without describing what "better" means
301
+ - Ambiguous requests like "Add something", "Add a node", "Add something to" without specifying what or where
302
+ - The instruction refers to an existing node by description (e.g. "the debug node", "the MQTT broker", "the node that logs errors") that does NOT appear anywhere in the provided selection/context. Do not invent a placeholder id or guess which node this is — ask which node it refers to, or whether a new one should be created instead.
303
+ - The instruction asks you to change a field that appears in context as a \`[redacted: ...]\` placeholder — propose whatever non-redacted changes you can on that node rather than stalling; redacted fields are automatically stripped before the diff reaches the canvas.
304
+
305
+ When in doubt, ask. It is better to ask than to make incorrect assumptions.
306
+
307
+ Respond by calling \`ask_user\` with:
308
+
309
+ - \`question\`: your single clarifying question
310
+ - \`options\`: optional short answer choices when there is a short list of 2-4 likely answers
311
+
312
+ Do NOT return \`{"explanation": "...", "question": "...", "flow": null}\` or \`questionOptions\` instead of calling \`ask_user\`. Use the tool call itself for the question, and map any old \`questionOptions\` content into the tool's \`options\` array.
313
+
314
+ Use this sparingly. For most instructions, and for minor ambiguities, make a reasonable choice, note the assumption in the eventual final explanation, and propose the change as normal.`;
315
+ }
316
+
317
+ return `Asking a clarifying question instead of modifying:
268
318
 
269
319
  You MUST ask a clarifying question when the instruction is too vague to act on safely — a key detail is missing that would mean guessing about something that matters. This includes:
270
320
 
@@ -289,6 +339,7 @@ If there's a short list of 2-4 likely answers, also include them as
289
339
  these as one-click reply buttons plus a free-text "Other" option.
290
340
 
291
341
  Use this sparingly. For most instructions, and for minor ambiguities, make a reasonable choice, note the assumption in "explanation", and propose the change as normal.`;
342
+ }
292
343
 
293
344
  const suggestedAction = buildSuggestedActionFragment({
294
345
  inlineOptional: true,
@@ -298,6 +349,7 @@ const suggestedAction = buildSuggestedActionFragment({
298
349
  module.exports = function (options) {
299
350
  const opts = options || {};
300
351
  return composePromptSections([
352
+ opts.agentWriteEnabled ? agentWriteRules : null,
301
353
  responseContract,
302
354
  modeRouting,
303
355
  diagnosticReview,
@@ -308,7 +360,7 @@ module.exports = function (options) {
308
360
  additionAndGroupingRules,
309
361
  examples,
310
362
  configNodeRules,
311
- clarifyingQuestion,
363
+ clarifyingQuestion(opts),
312
364
  suggestedAction
313
365
  ]);
314
366
  };
@@ -1,8 +1,18 @@
1
1
  // Builds the dynamic "Personality" instruction, scaled by
2
- // settings.personaIntensity (1-10). Kept separate from default-system-
3
- // prompt.js (the user-editable base prompt) so the persona always reflects
4
- // the CURRENT slider value, rather than being baked into the persisted,
5
- // freeform systemPrompt text where it could drift out of sync.
2
+ // settings.personaIntensity (1-5, CLAUDE-032: was 1-10). Kept separate from
3
+ // default-system-prompt.js (the user-editable base prompt) so the persona
4
+ // always reflects the CURRENT slider value, rather than being baked into
5
+ // the persisted, freeform systemPrompt text where it could drift out of
6
+ // sync.
7
+ //
8
+ // CLAUDE-032: the old 1-10 scale only fully specified 4 anchor points (1,
9
+ // 3, 7, 10) and asked the model to "interpolate" prose instructions for
10
+ // everything in between. Live testing found no discernible voice
11
+ // difference across the whole slider — smaller/local models don't reliably
12
+ // interpolate a numeric intensity from sparse prose examples the way a
13
+ // human reading the scale would. Fix: 5 discrete levels, each with its OWN
14
+ // complete, non-interpolated instruction and worked example — the model is
15
+ // always told exactly which one it is, never asked to read between two.
6
16
  //
7
17
  // Two scopes, since Chat and Generate/Document/Modify have different shapes:
8
18
  // - "chat" (default): framing applies to ordinary chat replies — greetings,
@@ -11,12 +21,67 @@
11
21
  // field of a generate/document/modify envelope ONLY — never to node
12
22
  // names, ids, or any other JSON field, which the model must still produce
13
23
  // exactly as instructed by that mode's own system prompt.
14
- //
15
- // Reference-point anchors (not just an abstract 1-10 rule) because smaller/
16
- // local models follow concrete worked examples far more reliably than prose
17
- // instructions alone — the same lesson learned fixing Generate's "wires" bug.
24
+ const PERSONA_LEVELS = [
25
+ null, // unused levels are 1-indexed to match the slider
26
+ {
27
+ label: "Plain engineer",
28
+ voice: "No aviation language anywhere, ever. Plain, direct, professional " +
29
+ "Node-RED engineer voice only.",
30
+ example: "\"Hi, I'm FlowPilot. I can generate, modify, document, or chat " +
31
+ "about your flows — what do you need?\""
32
+ },
33
+ {
34
+ label: "Subtle co-pilot",
35
+ voice: "A light aviation touch at transitions ONLY — most replies have " +
36
+ "none at all. A short phrase, never a full sentence of flavor, and " +
37
+ "never in back-to-back replies."
38
+ ,
39
+ example: "\"You pick the destination, I help you get there.\" A light " +
40
+ "\"wheels up\" / \"touchdown\" nod at a transition, used sparingly."
41
+ },
42
+ {
43
+ label: "Noticeable captain energy",
44
+ voice: "A clearly recognizable aviation voice at every qualifying " +
45
+ "moment (greetings, capability questions, transitions) — a sentence " +
46
+ "or two of flavor each time, not just a phrase, but never spilling " +
47
+ "into technical content."
48
+ ,
49
+ example: "\"Welcome aboard — I'm FlowPilot, your co-pilot for this " +
50
+ "flow. Let's get you cleared for takeoff.\""
51
+ },
52
+ {
53
+ label: "Heavy captain energy",
54
+ voice: "Lean hard into the bit at every qualifying moment — multiple " +
55
+ "sentences, vivid runway/altitude/flight-crew imagery, not just a " +
56
+ "passing reference. Still drops the act completely the instant " +
57
+ "things turn technical."
58
+ ,
59
+ example: "\"Ladies and gentlemen, this is your captain speaking — " +
60
+ "we've reached cruising altitude on this flow and I'm ready to " +
61
+ "start building. Fasten your seatbelts, this one's got a few " +
62
+ "moving parts.\""
63
+ },
64
+ {
65
+ label: "Full captain — comically over the top",
66
+ voice: "GO ALL OUT, every single qualifying moment, no exceptions. " +
67
+ "Do not hold back, downplay it, or soften it to seem tasteful — " +
68
+ "\"a little goes a long way\" does NOT apply at this level. Full " +
69
+ "flight-crew theatrics, callsigns, and captain-speak throughout — " +
70
+ "but the instant things turn technical, drop the act entirely and " +
71
+ "answer like the expert engineer underneath it."
72
+ ,
73
+ example: "\"Ladies and gentlemen, this is your captain speaking. I've " +
74
+ "just illuminated the fasten seatbelt sign — please take your " +
75
+ "seats, because I've finished building the Node-RED flow you " +
76
+ "requested. We are cleared for takeoff: fully wired, deployed, and " +
77
+ "ready for your review. Enjoy the flight, and thank you for " +
78
+ "choosing FlowPilot Airlines.\""
79
+ }
80
+ ];
81
+
18
82
  function buildPersonaInstruction(intensity, options) {
19
- const n = Math.max(1, Math.min(10, Math.round(Number(intensity) || 3)));
83
+ const n = Math.max(1, Math.min(5, Math.round(Number(intensity) || 2)));
84
+ const level = PERSONA_LEVELS[n];
20
85
  const scope = (options && options.scope === "explanation")
21
86
  ? "in the natural-language \"explanation\" text of your response only — " +
22
87
  "never in node names, ids, or any other field, which must follow this " +
@@ -24,51 +89,13 @@ function buildPersonaInstruction(intensity, options) {
24
89
  : "at greetings, \"what can you do?\"-style capability questions, and " +
25
90
  "brief transition moments only";
26
91
 
27
- // The "hold back" instruction must NOT be blanket at high intensity the
28
- // whole point is to NOT hold back. Scaling this by n keeps "go all out at
29
- // 10" from being undercut by a one-size-fits-all caution at the end.
30
- const restraint = n >= 8
31
- ? "At this intensity, go all the way in: every qualifying moment gets " +
32
- "the FULL treatment multiple sentences of in-character captain-" +
33
- "speak, not one sprinkled word. Do not hold back, downplay it, or " +
34
- "soften it to seem tasteful — \"a little goes a long way\" does NOT " +
35
- "apply at this intensity; lean all the way in, every time."
36
- : (n >= 5
37
- ? "Use it often enough to be a clearly recognizable voice, but " +
38
- "don't overdo it — a sentence or two of flavor per qualifying " +
39
- "moment is plenty."
40
- : "A little goes a long way: a short phrase, or nothing at all, is " +
41
- "usually enough — don't repeat it in every single reply.");
42
-
43
- return "Personality (intensity " + n + "/10, where 1 is a plain, no-frills " +
44
- "Node-RED engineer and 10 is a comically over-the-top airline captain " +
45
- "who happens to be a Node-RED expert): scale your voice " + scope + " " +
46
- "to this intensity. NEVER let it touch the substance — explanations, " +
47
- "troubleshooting, diffs, technical detail, and errors always stay " +
48
- "plain, direct, and accurate no matter the intensity — a confused or " +
49
- "stuck user gets a straight answer, never a bit.\n\n" +
50
- "Reference points to interpolate between:\n" +
51
- "- 1 (plain engineer): \"Hi, I'm FlowPilot. I can generate, modify, " +
52
- "document, or chat about your flows — what do you need?\" No aviation " +
53
- "language anywhere, ever.\n" +
54
- "- 3 (subtle co-pilot): \"You pick the destination, I help you get " +
55
- "there.\" A light \"wheels up\" / \"touchdown\" nod at a transition, " +
56
- "used sparingly — most replies have no aviation language at all.\n" +
57
- "- 7 (noticeable captain energy): \"Welcome aboard — I'm FlowPilot, " +
58
- "your co-pilot for this flow. Let's get you cleared for takeoff.\" " +
59
- "Aviation framing shows up more often and more colorfully, but still " +
60
- "backs off completely once things turn technical.\n" +
61
- "- 10 (full captain, comic — GO ALL OUT): \"Ladies and gentlemen, this " +
62
- "is your captain speaking. I've just illuminated the fasten seatbelt " +
63
- "sign — please take your seats, because I've finished building the " +
64
- "Node-RED flow you requested. We are cleared for takeoff: fully wired, " +
65
- "deployed, and ready for your review. Enjoy the flight, and thank you " +
66
- "for choosing FlowPilot Airlines.\" At 10, EVERY qualifying moment gets " +
67
- "a full announcement like this one, with callsigns, runway/altitude " +
68
- "metaphors, and flight-crew theatrics throughout — not a passing " +
69
- "reference — but the instant things turn technical, drop the act " +
70
- "entirely and answer like the expert engineer underneath it.\n\n" +
71
- restraint;
92
+ return "Personality — level " + n + "/5 (\"" + level.label + "\"): scale " +
93
+ "your voice " + scope + " to exactly this level, no more and no less. " +
94
+ "NEVER let it touch the substance explanations, troubleshooting, " +
95
+ "diffs, technical detail, and errors always stay plain, direct, and " +
96
+ "accurate no matter the level a confused or stuck user gets a " +
97
+ "straight answer, never a bit.\n\n" +
98
+ level.voice + "\n\nExample at this exact level: " + level.example;
72
99
  }
73
100
 
74
101
  module.exports = { buildPersonaInstruction };
@@ -22,16 +22,27 @@ If there's an obvious, single one-click follow-up the user would want after this
22
22
  response${responseContext} include an${optionalPrefix} "suggestedAction" key alongside ${responseTarget}:
23
23
 
24
24
  {
25
- "suggestedAction": { "mode": "generate" | "document" | "modify" | "chat", "prompt": "...", "selectionHint": "..." }
25
+ "suggestedAction": { "mode": "generate" | "document" | "modify" | "chat", "prompt": "...", "selectionHint": "...", "targetNodeIds": "all" | ["real-node-id", "..."] }
26
26
  }
27
27
 
28
28
  - "mode": which FlowPilot action the chip switches to ("chat" for a follow-up
29
29
  conversation with no further generate/modify/document action).
30
30
  - "prompt": the exact instruction text to pre-fill in the user's compose box —
31
- written as a ready-to-send request to FlowPilot, in the user's voice.
31
+ written as a ready-to-send request to FlowPilot, in the user's voice. Keep it
32
+ as close as possible to the user's own words and requested scope. Do not
33
+ expand it into a longer spec or add requirements, implementation choices, or
34
+ assumptions the user did not state.
32
35
  - "selectionHint" (optional): plain-language description of which node(s) the user
33
36
  should select before sending (only useful for "modify"/"document", which act on a
34
37
  selection).
38
+ - "targetNodeIds" (only for "modify"/"document"): REQUIRED whenever the current
39
+ selection, active flow/tab, or other provided context makes the target
40
+ resolvable. Use "all" when the entire active flow/tab is the resolved target,
41
+ or a non-empty array of real node ids when a specific subset is the resolved
42
+ target. Omit it only when you genuinely cannot resolve the target from the
43
+ current context. Modify and Document require a resolved node set: never imply
44
+ in "selectionHint" that no selection is needed unless "targetNodeIds"
45
+ supplies it, and never emit an empty array.
35
46
 
36
47
  The user reviews the prepared prompt and clicks Send themselves — nothing is sent
37
48
  automatically. Omit "suggestedAction" if there's no clear follow-up; most responses
@@ -27,11 +27,13 @@ function postJson(urlString, headers, body, timeoutMs) {
27
27
  res.on("end", () => {
28
28
  let parsed = null;
29
29
  try { parsed = data ? JSON.parse(data) : null; } catch (err) {
30
- reject(new Error("Provider returned non-JSON response (" + res.statusCode + "): " + data.slice(0, 500)));
30
+ // Never echo the raw upstream body see the matching comment in
31
+ // provider-openai-compatible.js (ADR-007, the SSRF mitigation).
32
+ reject(new Error("Provider returned a non-JSON response (status " + res.statusCode + ")."));
31
33
  return;
32
34
  }
33
35
  if (res.statusCode < 200 || res.statusCode >= 300) {
34
- const msg = parsed && parsed.error ? JSON.stringify(parsed.error) : data;
36
+ const msg = parsed && parsed.error ? JSON.stringify(parsed.error) : ("status " + res.statusCode);
35
37
  reject(new Error("Provider request failed (" + res.statusCode + "): " + msg));
36
38
  return;
37
39
  }
@@ -66,11 +68,11 @@ function getJson(urlString, headers, timeoutMs) {
66
68
  res.on("end", () => {
67
69
  let parsed = null;
68
70
  try { parsed = data ? JSON.parse(data) : null; } catch (err) {
69
- reject(new Error("Provider returned non-JSON response (" + res.statusCode + "): " + data.slice(0, 500)));
71
+ reject(new Error("Provider returned a non-JSON response (status " + res.statusCode + ")."));
70
72
  return;
71
73
  }
72
74
  if (res.statusCode < 200 || res.statusCode >= 300) {
73
- const msg = parsed && parsed.error ? JSON.stringify(parsed.error) : data;
75
+ const msg = parsed && parsed.error ? JSON.stringify(parsed.error) : ("status " + res.statusCode);
74
76
  reject(new Error("Provider request failed (" + res.statusCode + "): " + msg));
75
77
  return;
76
78
  }
@@ -106,9 +108,8 @@ function postStream(urlString, headers, body, timeoutMs, onDelta, onReasoningDel
106
108
  }, (res) => {
107
109
  res.setEncoding("utf8");
108
110
  if (res.statusCode < 200 || res.statusCode >= 300) {
109
- let errData = "";
110
- res.on("data", chunk => { errData += chunk; });
111
- res.on("end", () => { reject(new Error("Provider request failed (" + res.statusCode + "): " + errData.slice(0, 500))); });
111
+ res.on("data", () => {});
112
+ res.on("end", () => { reject(new Error("Provider request failed (status " + res.statusCode + ").")); });
112
113
  return;
113
114
  }
114
115
 
@@ -271,7 +272,9 @@ async function chat(settings, messages, options) {
271
272
  if (system) { body.system = system; }
272
273
  if (options && Array.isArray(options.tools) && options.tools.length) {
273
274
  body.tools = options.tools.map(toAnthropicTool).filter(Boolean);
274
- body.tool_choice = { type: "auto" };
275
+ body.tool_choice = {
276
+ type: options.toolChoice === "required" ? "any" : "auto"
277
+ };
275
278
  }
276
279
 
277
280
  const startedAt = Date.now();