@tbrandenburg/node-red-agents 0.3.8 → 0.4.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.
@@ -1,641 +1,1031 @@
1
1
  <script type="text/javascript">
2
- (function () {
3
- 'use strict';
4
-
5
- const MODEL_SUGGESTIONS = [
6
- 'opencode/big-pickle',
7
- 'opencode/deepseek-v4-flash-free',
8
- 'opencode/hy3-free',
9
- 'opencode/laguna-s-2.1-free',
10
- 'opencode/mimo-v2.5-free',
11
- 'anthropic/claude-sonnet-5',
12
- 'anthropic/claude-opus-4-8',
13
- 'anthropic/claude-haiku-4-5',
14
- 'github-copilot/claude-sonnet-5',
15
- 'github-copilot/gpt-5.4',
16
- 'openai/gpt-5.4',
17
- 'openai/gpt-4.1'
18
- ];
19
-
20
- function mcpServerRow(container, data) {
21
- data = data || { name: '', type: 'remote', url: '', command: '', args: [] };
22
- const row = $('<div/>', { style: 'display:flex; gap:4px; align-items:flex-start; width:100%;' }).appendTo(container);
23
-
24
- const nameInput = $('<input/>', { type: 'text', placeholder: 'name', style: 'width:110px' })
25
- .val(data.name || '')
26
- .appendTo(row);
27
-
28
- const typeSelect = $('<select/>', { style: 'width:90px' })
29
- .append('<option value="remote">Remote</option>')
30
- .append('<option value="local">Local</option>')
31
- .val(data.type || 'remote')
32
- .appendTo(row);
33
-
34
- const fieldsContainer = $('<div/>', { style: 'flex:1; display:flex; gap:4px;' }).appendTo(row);
35
-
36
- function renderFields() {
37
- fieldsContainer.empty();
38
- if (typeSelect.val() === 'remote') {
39
- $('<input/>', { type: 'text', placeholder: 'https://mcp.example.com/...', class: 'mcp-url', style: 'width:100%' })
40
- .val(data.url || '')
41
- .appendTo(fieldsContainer);
42
- } else {
43
- $('<input/>', { type: 'text', placeholder: 'command (e.g. npx)', class: 'mcp-command', style: 'width:35%' })
44
- .val(data.command || '')
45
- .appendTo(fieldsContainer);
46
- $('<input/>', { type: 'text', placeholder: 'args (space separated)', class: 'mcp-args', style: 'width:65%' })
47
- .val(Array.isArray(data.args) ? data.args.join(' ') : (data.args || ''))
48
- .appendTo(fieldsContainer);
49
- }
50
- }
51
- typeSelect.on('change', renderFields);
52
- renderFields();
53
-
54
- row.data('mcpGetValue', function () {
55
- const type = typeSelect.val();
56
- const out = { name: nameInput.val().trim(), type: type };
57
- if (type === 'remote') {
58
- out.url = fieldsContainer.find('.mcp-url').val().trim();
59
- } else {
60
- out.command = fieldsContainer.find('.mcp-command').val().trim();
61
- const argsRaw = fieldsContainer.find('.mcp-args').val().trim();
62
- out.args = argsRaw ? argsRaw.split(/\s+/) : [];
63
- }
64
- return out;
65
- });
2
+ (function () {
3
+ "use strict";
4
+
5
+ const MODEL_SUGGESTIONS = [
6
+ "opencode/big-pickle",
7
+ "opencode/deepseek-v4-flash-free",
8
+ "opencode/hy3-free",
9
+ "opencode/laguna-s-2.1-free",
10
+ "opencode/mimo-v2.5-free",
11
+ "anthropic/claude-sonnet-5",
12
+ "anthropic/claude-opus-4-8",
13
+ "anthropic/claude-haiku-4-5",
14
+ "github-copilot/claude-sonnet-5",
15
+ "github-copilot/gpt-5.4",
16
+ "openai/gpt-5.4",
17
+ "openai/gpt-4.1",
18
+ ];
19
+
20
+ function mcpServerRow(container, data) {
21
+ data = data || { name: "", type: "remote", url: "", command: "", args: [] };
22
+ const row = $("<div/>", {
23
+ style: "display:flex; gap:4px; align-items:flex-start; width:100%;",
24
+ }).appendTo(container);
25
+
26
+ const nameInput = $("<input/>", { type: "text", placeholder: "name", style: "width:110px" })
27
+ .val(data.name || "")
28
+ .appendTo(row);
29
+
30
+ const typeSelect = $("<select/>", { style: "width:90px" })
31
+ .append('<option value="remote">Remote</option>')
32
+ .append('<option value="local">Local</option>')
33
+ .val(data.type || "remote")
34
+ .appendTo(row);
35
+
36
+ const fieldsContainer = $("<div/>", { style: "flex:1; display:flex; gap:4px;" }).appendTo(
37
+ row,
38
+ );
39
+
40
+ function renderFields() {
41
+ fieldsContainer.empty();
42
+ if (typeSelect.val() === "remote") {
43
+ $("<input/>", {
44
+ type: "text",
45
+ placeholder: "https://mcp.example.com/...",
46
+ class: "mcp-url",
47
+ style: "width:100%",
48
+ })
49
+ .val(data.url || "")
50
+ .appendTo(fieldsContainer);
51
+ } else {
52
+ $("<input/>", {
53
+ type: "text",
54
+ placeholder: "command (e.g. npx)",
55
+ class: "mcp-command",
56
+ style: "width:35%",
57
+ })
58
+ .val(data.command || "")
59
+ .appendTo(fieldsContainer);
60
+ $("<input/>", {
61
+ type: "text",
62
+ placeholder: "args (space separated)",
63
+ class: "mcp-args",
64
+ style: "width:65%",
65
+ })
66
+ .val(Array.isArray(data.args) ? data.args.join(" ") : data.args || "")
67
+ .appendTo(fieldsContainer);
66
68
  }
69
+ }
70
+ typeSelect.on("change", renderFields);
71
+ renderFields();
67
72
 
68
- // Much simpler than mcpServerRow: one plain text input per row, no
69
- // per-row type switch. Used for the SRT "Allowed domains" and
70
- // "Allowed write directories" lists.
71
- function simpleStringRow(container, value) {
72
- const input = $('<input/>', { type: 'text', style: 'width:100%' }).val(value || '').appendTo(container);
73
- container.data('getValue', function () {
74
- return input.val().trim();
75
- });
73
+ row.data("mcpGetValue", function () {
74
+ const type = typeSelect.val();
75
+ const out = { name: nameInput.val().trim(), type: type };
76
+ if (type === "remote") {
77
+ out.url = fieldsContainer.find(".mcp-url").val().trim();
78
+ } else {
79
+ out.command = fieldsContainer.find(".mcp-command").val().trim();
80
+ const argsRaw = fieldsContainer.find(".mcp-args").val().trim();
81
+ out.args = argsRaw ? argsRaw.split(/\s+/) : [];
76
82
  }
83
+ return out;
84
+ });
85
+ }
86
+
87
+ // Much simpler than mcpServerRow: one plain text input per row, no
88
+ // per-row type switch. Used for the SRT "Allowed domains" and
89
+ // "Allowed write directories" lists.
90
+ let inputsRowCounter = 0;
91
+ function inputsRow(container, data) {
92
+ data = data || { name: "", value: "", valueType: "msg" };
93
+ inputsRowCounter += 1;
94
+ const rowId = `agent-inputs-row-${inputsRowCounter}`;
95
+ const row = $("<div/>", {
96
+ style: "display:flex; gap:4px; align-items:center; width:100%;",
97
+ }).appendTo(container);
98
+
99
+ const nameInput = $("<input/>", { type: "text", placeholder: "name", style: "width:110px" })
100
+ .val(data.name || "")
101
+ .appendTo(row);
102
+
103
+ const valueInput = $("<input/>", { type: "text", id: rowId, style: "flex:1" })
104
+ .val(data.value || "")
105
+ .appendTo(row);
106
+ const valueType = $("<input/>", { type: "hidden", id: `${rowId}-type` }).appendTo(row);
107
+ valueInput.typedInput({
108
+ typeField: `#${rowId}-type`,
109
+ types: ["msg", "str", "flow", "global", "env"],
110
+ });
111
+ valueInput.typedInput("type", data.valueType || "msg");
112
+
113
+ row.data("inputsGetValue", function () {
114
+ return {
115
+ name: nameInput.val().trim(),
116
+ value: valueInput.typedInput("value"),
117
+ valueType: valueInput.typedInput("type"),
118
+ };
119
+ });
120
+ row.data("inputsValueType", valueType);
121
+ }
122
+
123
+ function simpleStringRow(container, value) {
124
+ const input = $("<input/>", { type: "text", style: "width:100%" })
125
+ .val(value || "")
126
+ .appendTo(container);
127
+ container.data("getValue", function () {
128
+ return input.val().trim();
129
+ });
130
+ }
131
+
132
+ RED.nodes.registerType("agent", {
133
+ category: "Agent",
134
+ color: "#8B5CF6",
135
+ defaults: {
136
+ name: { value: "" },
137
+ agent: { value: "opencode" },
138
+ runtime: { value: "direct" },
139
+ invocation: { value: "prompt" },
140
+
141
+ agentName: { value: "" },
142
+ agentNameType: { value: "str" },
143
+
144
+ model: { value: "" },
145
+ modelType: { value: "str" },
146
+
147
+ prompt: { value: "payload" },
148
+ promptType: { value: "msg" },
149
+
150
+ sessionIdProp: { value: "sessionID" },
151
+ sessionIdPropType: { value: "msg" },
152
+
153
+ invocationName: { value: "" },
154
+ invocationNameType: { value: "str" },
155
+
156
+ arguments: { value: "payload" },
157
+ argumentsType: { value: "msg" },
158
+
159
+ inputs: { value: [] },
160
+
161
+ cwd: { value: "cwd" },
162
+ cwdType: { value: "msg" },
163
+
164
+ auto: { value: false },
165
+
166
+ concurrency: { value: 1, validate: RED.validators.number() },
167
+
168
+ timeout: { value: "" },
169
+ timeoutType: { value: "num" },
170
+
171
+ mcpServers: { value: [] },
172
+
173
+ systemPrompt: { value: "" },
174
+ systemPromptType: { value: "str" },
175
+
176
+ effort: { value: "" },
177
+ effortType: { value: "str" },
77
178
 
78
- RED.nodes.registerType('agent', {
79
- category: 'Agent',
80
- color: '#8B5CF6',
81
- defaults: {
82
- name: { value: '' },
83
- agent: { value: 'opencode' },
84
- runtime: { value: 'direct' },
85
- invocation: { value: 'prompt' },
86
-
87
- agentName: { value: '' },
88
- agentNameType: { value: 'str' },
89
-
90
- model: { value: '' },
91
- modelType: { value: 'str' },
92
-
93
- prompt: { value: 'payload' },
94
- promptType: { value: 'msg' },
95
-
96
- sessionIdProp: { value: 'sessionID' },
97
- sessionIdPropType: { value: 'msg' },
98
-
99
- invocationName: { value: '' },
100
- invocationNameType: { value: 'str' },
101
-
102
- arguments: { value: 'payload' },
103
- argumentsType: { value: 'msg' },
104
-
105
- cwd: { value: 'cwd' },
106
- cwdType: { value: 'msg' },
107
-
108
- auto: { value: false },
109
-
110
- concurrency: { value: 1, validate: RED.validators.number() },
111
-
112
- timeout: { value: '' },
113
- timeoutType: { value: 'num' },
114
-
115
- mcpServers: { value: [] },
116
-
117
- srtBinary: { value: '' },
118
- srtSettingsMode: { value: 'file' },
119
- srtSettingsPath: { value: '' },
120
- srtAllowedDomains: { value: [] },
121
- srtAllowedWriteDirs: { value: ['.', '/tmp', '~/.local/share/opencode'] },
122
- srtStrictAllowlist: { value: true },
123
- srtAdvancedJson: {
124
- value: '',
125
- validate: function (v) {
126
- if (!v || !v.trim()) return true;
127
- try {
128
- JSON.parse(v);
129
- return true;
130
- } catch (err) {
131
- return false;
132
- }
133
- }
134
- }
135
- },
136
- inputs: 1,
137
- outputs: 2,
138
- outputLabels: ['result', 'events'],
139
- icon: 'agent.svg',
140
- paletteLabel: 'agent',
141
- label: function () {
142
- return this.name || (this.agent + ' (' + this.runtime + ')');
143
- },
144
- oneditprepare: function () {
145
- const node = this;
146
-
147
- $('#node-input-model').typedInput({
148
- typeField: '#node-input-modelType',
149
- types: ['str', 'msg', 'flow', 'global', 'env']
150
- });
151
- $('#node-input-model').attr('list', 'agent-model-suggestions');
152
- if (!$('#agent-model-suggestions').length) {
153
- const datalist = $('<datalist id="agent-model-suggestions"></datalist>').appendTo('body');
154
- MODEL_SUGGESTIONS.forEach((m) => datalist.append('<option value="' + m + '">'));
155
- }
156
-
157
- $('#node-input-agentName').typedInput({
158
- typeField: '#node-input-agentNameType',
159
- types: ['str', 'msg', 'flow', 'global', 'env']
160
- });
161
-
162
- $('#node-input-prompt').typedInput({
163
- typeField: '#node-input-promptType',
164
- types: ['msg', 'str', 'flow', 'global', 'env']
165
- });
166
- $('#node-input-sessionIdProp').typedInput({
167
- typeField: '#node-input-sessionIdPropType',
168
- types: ['msg', 'str', 'flow', 'global']
169
- });
170
- // RED.popover (Node-RED's own hover-tooltip system) rather
171
- // than a plain HTML `title` attribute: `title` is the
172
- // browser's native tooltip, whose appearance/delay/is even
173
- // shown at all depends on OS/browser/accessibility settings
174
- // outside our control (reported: not appearing at all in
175
- // Firefox). RED.popover.create with trigger:'hover' is
176
- // Node-RED's own JS-driven popover, used throughout its own
177
- // editor (e.g. the debug sidebar's copy buttons) -- it
178
- // doesn't depend on the browser's native tooltip behavior at
179
- // all, so it renders identically regardless of browser.
180
- RED.popover.create({
181
- target: $('#agent-session-id-hint'),
182
- direction: 'right',
183
- trigger: 'hover',
184
- size: 'small',
185
- width: 320,
186
- content:
187
- 'Empty/absent &rarr; starts a <b>new</b> session (today\'s default). A <b>present</b> id ' +
188
- 'resumes that session instead (<code>opencode run --session &lt;id&gt; ...</code>). The ' +
189
- 'result\'s <code>msg.sessionID</code> (also under <code>agentExecution.sessionID</code>) ' +
190
- 'can be fed straight back into this field on the next message to continue the ' +
191
- 'conversation. OpenCode only -- Pi does not support session continuation.'
192
- });
193
- $('#node-input-invocationName').typedInput({
194
- typeField: '#node-input-invocationNameType',
195
- types: ['str', 'msg', 'flow', 'global']
196
- });
197
- $('#node-input-arguments').typedInput({
198
- typeField: '#node-input-argumentsType',
199
- types: ['msg', 'str', 'flow', 'global', 'env']
200
- });
201
- $('#node-input-cwd').typedInput({
202
- typeField: '#node-input-cwdType',
203
- types: ['msg', 'str', 'flow', 'global', 'env']
204
- });
205
- $('#node-input-timeout').typedInput({
206
- typeField: '#node-input-timeoutType',
207
- types: ['num', 'msg', 'flow', 'global', 'env']
208
- });
209
-
210
- const NAME_PLACEHOLDERS = {
211
- opencode: { skill: 'code-review', command: 'review' },
212
- pi: { skill: 'code-review (looked up under .github/skills/)', command: 'review (looked up under .github/prompts/)' }
213
- };
214
-
215
- function updateInvocationRows() {
216
- const invocation = $('#node-input-invocation').val();
217
- const agent = $('#node-input-agent').val();
218
- $('.agent-row-prompt').toggle(invocation === 'prompt');
219
- $('.agent-row-name').toggle(invocation !== 'prompt');
220
- $('.agent-row-arguments').toggle(invocation !== 'prompt');
221
- const label = invocation === 'skill' ? 'Skill' : 'Command';
222
- $('#agent-name-label-text').text(label);
223
- const placeholders = NAME_PLACEHOLDERS[agent] || NAME_PLACEHOLDERS.opencode;
224
- $('#node-input-invocationName').attr('placeholder', placeholders[invocation] || '');
225
- }
226
- $('#node-input-invocation').on('change', updateInvocationRows);
227
-
228
- function updateRuntimeRows() {
229
- $('.agent-row-srt').toggle($('#node-input-runtime').val() === 'srt');
230
- }
231
- $('#node-input-runtime').on('change', updateRuntimeRows);
232
-
233
- function updateSrtModeRows() {
234
- const mode = $('#node-input-srtSettingsMode').val();
235
- $('.agent-row-srt-file').toggle(mode === 'file');
236
- $('.agent-row-srt-inline').toggle(mode === 'inline');
237
- }
238
- $('#node-input-srtSettingsMode').on('change', updateSrtModeRows);
239
-
240
- const AUTO_HINTS = {
241
- opencode: 'Maps to --auto: skips permission prompts for actions not explicitly denied.',
242
- pi: 'Pi has no permission prompts to bypass. Off = read-only tools (read/grep/find/ls). On = full tool access (bash/edit/write too).'
243
- };
244
-
245
- // Grey out (rather than hide) fields an agent doesn't
246
- // support, so it's clear the setting exists but won't take
247
- // effect for the currently selected agent -- instead of the
248
- // section silently disappearing.
249
- function setSectionSupported($section, supported) {
250
- $section.css({
251
- opacity: supported ? '' : 0.45,
252
- pointerEvents: supported ? '' : 'none'
253
- });
254
- $section.find('input, select, button, textarea').prop('disabled', !supported);
255
- }
256
-
257
- const list = $('#node-input-mcpServers-list');
258
- list.editableList({
259
- addButton: 'Add MCP Server',
260
- height: 150,
261
- addItem: function (container, index, data) {
262
- mcpServerRow(container, data);
263
- },
264
- removable: true,
265
- sortable: true
266
- });
267
- (node.mcpServers || []).forEach((entry) => list.editableList('addItem', entry));
268
-
269
- const domainsList = $('#node-input-srtAllowedDomains-list');
270
- domainsList.editableList({
271
- addButton: 'Add domain',
272
- height: 100,
273
- addItem: function (container, index, value) {
274
- simpleStringRow(container, value);
275
- },
276
- removable: true,
277
- sortable: true
278
- });
279
- (node.srtAllowedDomains || []).forEach((entry) => domainsList.editableList('addItem', entry));
280
-
281
- const writeDirsList = $('#node-input-srtAllowedWriteDirs-list');
282
- writeDirsList.editableList({
283
- addButton: 'Add directory',
284
- height: 100,
285
- addItem: function (container, index, value) {
286
- simpleStringRow(container, value);
287
- },
288
- removable: true,
289
- sortable: true
290
- });
291
- (node.srtAllowedWriteDirs || []).forEach((entry) => writeDirsList.editableList('addItem', entry));
292
-
293
- function updateAgentRows() {
294
- const agent = $('#node-input-agent').val();
295
- $('#agent-auto-hint').attr('title', AUTO_HINTS[agent] || '');
296
- const mcpSupported = agent !== 'pi';
297
- setSectionSupported($('.agent-row-mcp'), mcpSupported);
298
- $('.agent-row-mcp-hint').toggle(!mcpSupported);
299
- const sessionSupported = agent !== 'pi';
300
- setSectionSupported($('.agent-row-session'), sessionSupported);
301
- $('.agent-row-session-hint').toggle(!sessionSupported);
302
- updateInvocationRows();
303
- }
304
- $('#node-input-agent').on('change', updateAgentRows);
305
- updateAgentRows();
306
- updateRuntimeRows();
307
- updateSrtModeRows();
308
- },
309
- oneditsave: function () {
310
- const list = $('#node-input-mcpServers-list');
311
- const items = list.editableList('items');
312
- const out = [];
313
- items.each(function () {
314
- out.push($(this).data('mcpGetValue')());
315
- });
316
- this.mcpServers = out;
317
-
318
- function collectStrings(listEl) {
319
- const values = [];
320
- listEl.editableList('items').each(function () {
321
- const v = $(this).data('getValue')();
322
- if (v) values.push(v);
323
- });
324
- return values;
325
- }
326
-
327
- // Only persist the SRT inline-settings lists/fields while
328
- // they're actually the active runtime+mode -- otherwise
329
- // reset them to their defaults. Without this, switching
330
- // Runtime back to Direct (or SRT settings back to File
331
- // path) silently keeps whatever domains/directories/JSON
332
- // were entered earlier, still saved into the flow even
333
- // though the now-hidden fields are irrelevant to the
334
- // deployed config -- confusing at best, and a footgun if
335
- // the runtime is ever switched back to SRT without
336
- // re-checking these stale values.
337
- //
338
- // Note: Node-RED's own core copies each defaults-bound
339
- // plain input's *current DOM value* into `this[key]`
340
- // right after oneditsave returns, so setting e.g.
341
- // `this.srtSettingsMode` here alone is not enough -- a
342
- // hidden <select>/<input>/<textarea> still holds its old
343
- // value and would silently overwrite it straight back.
344
- // The reset has to touch the DOM element itself for any
345
- // field with a plain `#node-input-<name>` binding;
346
- // srtAllowedDomains/srtAllowedWriteDirs are the exception
347
- // (no such element exists -- only the "-list" editableList
348
- // container), so those are fine set directly on `this`.
349
- const runtime = $('#node-input-runtime').val();
350
- const srtSettingsMode = $('#node-input-srtSettingsMode').val();
351
-
352
- if (runtime !== 'srt') {
353
- $('#node-input-srtSettingsMode').val('file');
354
- $('#node-input-srtSettingsPath').val('');
355
- $('#node-input-srtBinary').val('');
356
- $('#node-input-srtStrictAllowlist').prop('checked', true);
357
- $('#node-input-srtAdvancedJson').val('');
358
- this.srtAllowedDomains = [];
359
- this.srtAllowedWriteDirs = ['.', '/tmp', '~/.local/share/opencode'];
360
- } else if (srtSettingsMode !== 'inline') {
361
- $('#node-input-srtStrictAllowlist').prop('checked', true);
362
- $('#node-input-srtAdvancedJson').val('');
363
- this.srtAllowedDomains = [];
364
- this.srtAllowedWriteDirs = ['.', '/tmp', '~/.local/share/opencode'];
365
- } else {
366
- this.srtAllowedDomains = collectStrings($('#node-input-srtAllowedDomains-list'));
367
- this.srtAllowedWriteDirs = collectStrings($('#node-input-srtAllowedWriteDirs-list'));
368
- }
179
+ allowedTools: { value: [] },
180
+ deniedTools: { value: [] },
181
+
182
+ // Node-level retry (issue #24): opt-in-by-default at a
183
+ // conservative setting. retryMaxAttempts counts total attempts
184
+ // (1 original + retries), e.g. 2 = one retry.
185
+ retryMaxAttempts: { value: 2, validate: RED.validators.number() },
186
+ retryDelayMs: { value: 3000, validate: RED.validators.number() },
187
+ retryOnError: { value: "transient" },
188
+
189
+ // output_format (issue #23): raw JSON-Schema text, fully opt-in
190
+ // (default empty/disabled). Only checked for well-formed JSON
191
+ // here (client-side) -- the actual AJV schema compile + capability
192
+ // gate happens server-side at deploy time (see agent.js), same
193
+ // division of labor as srtAdvancedJson below.
194
+ outputFormat: {
195
+ value: "",
196
+ validate: function (v) {
197
+ if (!v || !v.trim()) return true;
198
+ try {
199
+ JSON.parse(v);
200
+ return true;
201
+ } catch (err) {
202
+ return false;
369
203
  }
204
+ },
205
+ },
206
+
207
+
208
+ srtBinary: { value: "" },
209
+ srtSettingsMode: { value: "file" },
210
+ srtSettingsPath: { value: "" },
211
+ srtAllowedDomains: { value: [] },
212
+ srtAllowedWriteDirs: { value: [".", "/tmp", "~/.local/share/opencode"] },
213
+ srtStrictAllowlist: { value: true },
214
+ srtAdvancedJson: {
215
+ value: "",
216
+ validate: function (v) {
217
+ if (!v || !v.trim()) return true;
218
+ try {
219
+ JSON.parse(v);
220
+ return true;
221
+ } catch (err) {
222
+ return false;
223
+ }
224
+ },
225
+ },
226
+ },
227
+ inputs: 1,
228
+ outputs: 2,
229
+ outputLabels: ["result", "events"],
230
+ icon: "agent.svg",
231
+ paletteLabel: "agent",
232
+ label: function () {
233
+ return this.name || this.agent + " (" + this.runtime + ")";
234
+ },
235
+ oneditprepare: function () {
236
+ const node = this;
237
+
238
+ $("#node-input-model").typedInput({
239
+ typeField: "#node-input-modelType",
240
+ types: ["str", "msg", "flow", "global", "env"],
241
+ });
242
+ $("#node-input-model").attr("list", "agent-model-suggestions");
243
+ if (!$("#agent-model-suggestions").length) {
244
+ const datalist = $('<datalist id="agent-model-suggestions"></datalist>').appendTo("body");
245
+ MODEL_SUGGESTIONS.forEach((m) => datalist.append('<option value="' + m + '">'));
246
+ }
247
+
248
+ $("#node-input-agentName").typedInput({
249
+ typeField: "#node-input-agentNameType",
250
+ types: ["str", "msg", "flow", "global", "env"],
251
+ });
252
+
253
+ $("#node-input-prompt").typedInput({
254
+ typeField: "#node-input-promptType",
255
+ types: ["msg", "str", "flow", "global", "env"],
256
+ });
257
+ $("#node-input-sessionIdProp").typedInput({
258
+ typeField: "#node-input-sessionIdPropType",
259
+ types: ["msg", "str", "flow", "global"],
260
+ });
261
+ // RED.popover (Node-RED's own hover-tooltip system) rather
262
+ // than a plain HTML `title` attribute: `title` is the
263
+ // browser's native tooltip, whose appearance/delay/is even
264
+ // shown at all depends on OS/browser/accessibility settings
265
+ // outside our control (reported: not appearing at all in
266
+ // Firefox). RED.popover.create with trigger:'hover' is
267
+ // Node-RED's own JS-driven popover, used throughout its own
268
+ // editor (e.g. the debug sidebar's copy buttons) -- it
269
+ // doesn't depend on the browser's native tooltip behavior at
270
+ // all, so it renders identically regardless of browser.
271
+ RED.popover.create({
272
+ target: $("#agent-session-id-hint"),
273
+ direction: "right",
274
+ trigger: "hover",
275
+ size: "small",
276
+ width: 320,
277
+ content:
278
+ "Empty/absent &rarr; starts a <b>new</b> session (today's default). A <b>present</b> id " +
279
+ "resumes that session instead (<code>opencode run --session &lt;id&gt; ...</code>). The " +
280
+ "result's <code>msg.sessionID</code> (also under <code>agentExecution.sessionID</code>) " +
281
+ "can be fed straight back into this field on the next message to continue the " +
282
+ "conversation. OpenCode only -- Pi does not support session continuation.",
283
+ });
284
+ $("#node-input-invocationName").typedInput({
285
+ typeField: "#node-input-invocationNameType",
286
+ types: ["str", "msg", "flow", "global"],
287
+ });
288
+ $("#node-input-arguments").typedInput({
289
+ typeField: "#node-input-argumentsType",
290
+ types: ["msg", "str", "flow", "global", "env"],
291
+ });
292
+ $("#node-input-cwd").typedInput({
293
+ typeField: "#node-input-cwdType",
294
+ types: ["msg", "str", "flow", "global", "env"],
295
+ });
296
+ $("#node-input-timeout").typedInput({
297
+ typeField: "#node-input-timeoutType",
298
+ types: ["num", "msg", "flow", "global", "env"],
299
+ });
300
+ $("#node-input-systemPrompt").typedInput({
301
+ typeField: "#node-input-systemPromptType",
302
+ types: ["str", "msg", "flow", "global", "env"],
303
+ });
304
+ $("#node-input-effort").typedInput({
305
+ typeField: "#node-input-effortType",
306
+ types: ["str", "msg", "flow", "global", "env"],
307
+ });
308
+
309
+ const NAME_PLACEHOLDERS = {
310
+ opencode: { skill: "code-review", command: "review" },
311
+ pi: {
312
+ skill: "code-review (looked up under .github/skills/)",
313
+ command: "review (looked up under .github/prompts/)",
314
+ },
315
+ };
316
+
317
+ function updateInvocationRows() {
318
+ const invocation = $("#node-input-invocation").val();
319
+ const agent = $("#node-input-agent").val();
320
+ $(".agent-row-prompt").toggle(invocation === "prompt");
321
+ $(".agent-row-name").toggle(invocation !== "prompt");
322
+ $(".agent-row-arguments").toggle(invocation !== "prompt");
323
+ const label = invocation === "skill" ? "Skill" : "Command";
324
+ $("#agent-name-label-text").text(label);
325
+ const placeholders = NAME_PLACEHOLDERS[agent] || NAME_PLACEHOLDERS.opencode;
326
+ $("#node-input-invocationName").attr("placeholder", placeholders[invocation] || "");
327
+ }
328
+ $("#node-input-invocation").on("change", updateInvocationRows);
329
+
330
+ function updateRuntimeRows() {
331
+ $(".agent-row-srt").toggle($("#node-input-runtime").val() === "srt");
332
+ }
333
+ $("#node-input-runtime").on("change", updateRuntimeRows);
334
+
335
+ function updateSrtModeRows() {
336
+ const mode = $("#node-input-srtSettingsMode").val();
337
+ $(".agent-row-srt-file").toggle(mode === "file");
338
+ $(".agent-row-srt-inline").toggle(mode === "inline");
339
+ }
340
+ $("#node-input-srtSettingsMode").on("change", updateSrtModeRows);
341
+
342
+ const AUTO_HINTS = {
343
+ opencode: "Maps to --auto: skips permission prompts for actions not explicitly denied.",
344
+ pi: "Pi has no permission prompts to bypass. Off = read-only tools (read/grep/find/ls). On = full tool access (bash/edit/write too).",
345
+ };
346
+
347
+ // Grey out (rather than hide) fields an agent doesn't
348
+ // support, so it's clear the setting exists but won't take
349
+ // effect for the currently selected agent -- instead of the
350
+ // section silently disappearing.
351
+ function setSectionSupported($section, supported) {
352
+ $section.css({
353
+ opacity: supported ? "" : 0.45,
354
+ pointerEvents: supported ? "" : "none",
355
+ });
356
+ $section.find("input, select, button, textarea").prop("disabled", !supported);
357
+ }
358
+
359
+ const list = $("#node-input-mcpServers-list");
360
+ list.editableList({
361
+ addButton: "Add MCP Server",
362
+ height: 150,
363
+ addItem: function (container, index, data) {
364
+ mcpServerRow(container, data);
365
+ },
366
+ removable: true,
367
+ sortable: true,
368
+ });
369
+ (node.mcpServers || []).forEach((entry) => list.editableList("addItem", entry));
370
+
371
+ const allowedToolsList = $("#node-input-allowedTools-list");
372
+ allowedToolsList.editableList({
373
+ addButton: "Add allowed tool",
374
+ height: 100,
375
+ addItem: function (container, index, value) {
376
+ simpleStringRow(container, value);
377
+ },
378
+ removable: true,
379
+ sortable: true,
380
+ });
381
+ (node.allowedTools || []).forEach((entry) =>
382
+ allowedToolsList.editableList("addItem", entry),
383
+ );
384
+
385
+ const inputsList = $("#node-input-inputs-list");
386
+ inputsList.editableList({
387
+ addButton: "Add input",
388
+ height: 120,
389
+ addItem: function (container, index, data) {
390
+ inputsRow(container, data);
391
+ },
392
+ removable: true,
393
+ sortable: true,
394
+ });
395
+ (node.inputs || []).forEach((entry) => inputsList.editableList("addItem", entry));
396
+
397
+
398
+ const deniedToolsList = $("#node-input-deniedTools-list");
399
+ deniedToolsList.editableList({
400
+ addButton: "Add denied tool",
401
+ height: 100,
402
+ addItem: function (container, index, value) {
403
+ simpleStringRow(container, value);
404
+ },
405
+ removable: true,
406
+ sortable: true,
407
+ });
408
+ (node.deniedTools || []).forEach((entry) => deniedToolsList.editableList("addItem", entry));
409
+
410
+ const domainsList = $("#node-input-srtAllowedDomains-list");
411
+ domainsList.editableList({
412
+ addButton: "Add domain",
413
+ height: 100,
414
+ addItem: function (container, index, value) {
415
+ simpleStringRow(container, value);
416
+ },
417
+ removable: true,
418
+ sortable: true,
370
419
  });
371
- }());
420
+ (node.srtAllowedDomains || []).forEach((entry) =>
421
+ domainsList.editableList("addItem", entry),
422
+ );
423
+
424
+ const writeDirsList = $("#node-input-srtAllowedWriteDirs-list");
425
+ writeDirsList.editableList({
426
+ addButton: "Add directory",
427
+ height: 100,
428
+ addItem: function (container, index, value) {
429
+ simpleStringRow(container, value);
430
+ },
431
+ removable: true,
432
+ sortable: true,
433
+ });
434
+ (node.srtAllowedWriteDirs || []).forEach((entry) =>
435
+ writeDirsList.editableList("addItem", entry),
436
+ );
437
+
438
+ function updateAgentRows() {
439
+ const agent = $("#node-input-agent").val();
440
+ $("#agent-auto-hint").attr("title", AUTO_HINTS[agent] || "");
441
+ const mcpSupported = agent !== "pi";
442
+ setSectionSupported($(".agent-row-mcp"), mcpSupported);
443
+ $(".agent-row-mcp-hint").toggle(!mcpSupported);
444
+ const sessionSupported = agent !== "pi";
445
+ setSectionSupported($(".agent-row-session"), sessionSupported);
446
+ $(".agent-row-session-hint").toggle(!sessionSupported);
447
+ const effortSupported = agent === "opencode";
448
+ setSectionSupported($(".agent-row-effort"), effortSupported);
449
+ $(".agent-row-effort-hint").toggle(!effortSupported);
450
+ updateInvocationRows();
451
+ }
452
+ $("#node-input-agent").on("change", updateAgentRows);
453
+ updateAgentRows();
454
+ updateRuntimeRows();
455
+ updateSrtModeRows();
456
+ },
457
+ oneditsave: function () {
458
+ const list = $("#node-input-mcpServers-list");
459
+ const items = list.editableList("items");
460
+ const out = [];
461
+ items.each(function () {
462
+ out.push($(this).data("mcpGetValue")());
463
+ });
464
+ this.mcpServers = out;
465
+
466
+ function collectStrings(listEl) {
467
+ const values = [];
468
+ listEl.editableList("items").each(function () {
469
+ const v = $(this).data("getValue")();
470
+ if (v) values.push(v);
471
+ });
472
+ return values;
473
+ }
474
+
475
+ this.allowedTools = collectStrings($("#node-input-allowedTools-list"));
476
+ this.deniedTools = collectStrings($("#node-input-deniedTools-list"));
477
+
478
+ const inputsOut = [];
479
+ $("#node-input-inputs-list")
480
+ .editableList("items")
481
+ .each(function () {
482
+ const v = $(this).data("inputsGetValue")();
483
+ if (v && v.name) inputsOut.push(v);
484
+ });
485
+ this.inputs = inputsOut;
486
+
487
+
488
+ // Only persist the SRT inline-settings lists/fields while
489
+ // they're actually the active runtime+mode -- otherwise
490
+ // reset them to their defaults. Without this, switching
491
+ // Runtime back to Direct (or SRT settings back to File
492
+ // path) silently keeps whatever domains/directories/JSON
493
+ // were entered earlier, still saved into the flow even
494
+ // though the now-hidden fields are irrelevant to the
495
+ // deployed config -- confusing at best, and a footgun if
496
+ // the runtime is ever switched back to SRT without
497
+ // re-checking these stale values.
498
+ //
499
+ // Note: Node-RED's own core copies each defaults-bound
500
+ // plain input's *current DOM value* into `this[key]`
501
+ // right after oneditsave returns, so setting e.g.
502
+ // `this.srtSettingsMode` here alone is not enough -- a
503
+ // hidden <select>/<input>/<textarea> still holds its old
504
+ // value and would silently overwrite it straight back.
505
+ // The reset has to touch the DOM element itself for any
506
+ // field with a plain `#node-input-<name>` binding;
507
+ // srtAllowedDomains/srtAllowedWriteDirs are the exception
508
+ // (no such element exists -- only the "-list" editableList
509
+ // container), so those are fine set directly on `this`.
510
+ const runtime = $("#node-input-runtime").val();
511
+ const srtSettingsMode = $("#node-input-srtSettingsMode").val();
512
+
513
+ if (runtime !== "srt") {
514
+ $("#node-input-srtSettingsMode").val("file");
515
+ $("#node-input-srtSettingsPath").val("");
516
+ $("#node-input-srtBinary").val("");
517
+ $("#node-input-srtStrictAllowlist").prop("checked", true);
518
+ $("#node-input-srtAdvancedJson").val("");
519
+ this.srtAllowedDomains = [];
520
+ this.srtAllowedWriteDirs = [".", "/tmp", "~/.local/share/opencode"];
521
+ } else if (srtSettingsMode !== "inline") {
522
+ $("#node-input-srtStrictAllowlist").prop("checked", true);
523
+ $("#node-input-srtAdvancedJson").val("");
524
+ this.srtAllowedDomains = [];
525
+ this.srtAllowedWriteDirs = [".", "/tmp", "~/.local/share/opencode"];
526
+ } else {
527
+ this.srtAllowedDomains = collectStrings($("#node-input-srtAllowedDomains-list"));
528
+ this.srtAllowedWriteDirs = collectStrings($("#node-input-srtAllowedWriteDirs-list"));
529
+ }
530
+ },
531
+ });
532
+ })();
372
533
  </script>
373
534
 
374
535
  <script type="text/html" data-template-name="agent">
375
- <div class="form-row">
376
- <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
377
- <input type="text" id="node-input-name" placeholder="Agent">
378
- </div>
379
-
380
- <div class="form-row">
381
- <label for="node-input-agentName"><i class="fa fa-id-badge"></i> Agent name
382
- <i class="fa fa-info-circle" style="color:#888; cursor:help; margin-left:4px;"
383
- title="Reported as msg.agentName on the result/event outputs. Leave blank (str) to use the Name field above; set to msg/flow/global to make it dynamic per message."></i>
384
- </label>
385
- <input type="text" id="node-input-agentName" placeholder="leave blank to use the Name field above" style="width:60%">
386
- <input type="hidden" id="node-input-agentNameType">
387
- </div>
388
-
389
- <div class="form-row">
390
- <label for="node-input-agent"><i class="fa fa-cube"></i> Agent</label>
391
- <select id="node-input-agent" style="width:70%">
392
- <option value="opencode">OpenCode</option>
393
- <option value="pi">Pi</option>
394
- </select>
395
- </div>
396
-
397
- <div class="form-row">
398
- <label for="node-input-runtime"><i class="fa fa-cogs"></i> Runtime</label>
399
- <select id="node-input-runtime" style="width:70%">
400
- <option value="direct">Direct</option>
401
- <option value="srt">SRT (sandboxed)</option>
402
- </select>
403
- </div>
404
- <div class="form-row agent-row-srt">
405
- <label for="node-input-srtSettingsMode"><i class="fa fa-shield"></i> SRT settings</label>
406
- <select id="node-input-srtSettingsMode" style="width:60%">
407
- <option value="file">File path</option>
408
- <option value="inline">Inline</option>
409
- </select>
410
- </div>
411
- <div class="form-row agent-row-srt agent-row-srt-file">
412
- <label for="node-input-srtSettingsPath">&nbsp;</label>
413
- <input type="text" id="node-input-srtSettingsPath" placeholder="~/.srt-settings.json (srt's own default)" style="width:60%">
414
- </div>
415
-
416
- <div class="form-row agent-row-srt agent-row-srt-inline">
417
- <label>
418
- <i class="fa fa-globe"></i> Allowed domains
419
- <i class="fa fa-info-circle" style="color:#888; cursor:help; margin-left:4px;"
420
- title="e.g. api.githubcopilot.com, opencode.ai -- the domain your configured model's API lives on."></i>
421
- </label>
422
- <ol id="node-input-srtAllowedDomains-list"></ol>
423
- </div>
424
-
425
- <div class="form-row agent-row-srt agent-row-srt-inline">
426
- <label><i class="fa fa-folder"></i> Allowed write dirs</label>
427
- <ol id="node-input-srtAllowedWriteDirs-list"></ol>
428
- </div>
429
-
430
- <div class="form-row agent-row-srt agent-row-srt-inline">
431
- <input type="checkbox" id="node-input-srtStrictAllowlist" style="display:inline-block; width:auto; vertical-align:top;">
432
- <label for="node-input-srtStrictAllowlist" style="width:auto;">Strict allowlist</label>
433
- <i class="fa fa-info-circle" style="color:#888; cursor:help; margin-left:4px;"
434
- title="Deny any host not in the allowed domains list. There is no interactive prompt to fall back on in non-interactive mode, so this should normally stay on."></i>
435
- </div>
436
-
437
- <div class="form-row agent-row-srt agent-row-srt-inline">
438
- <details>
439
- <summary style="cursor:pointer;">Advanced (raw JSON)</summary>
440
- <textarea id="node-input-srtAdvancedJson" rows="4" style="width:100%; font-family:monospace;"
441
- placeholder='If set, replaces the domains/directories above entirely, e.g. {"network":{"allowedDomains":[],"deniedDomains":[]},"filesystem":{"allowWrite":[],"denyRead":[],"denyWrite":[]}}'></textarea>
442
- </details>
443
- </div>
444
-
445
- <div class="form-row agent-row-srt">
446
- <label for="node-input-srtBinary"><i class="fa fa-terminal"></i> SRT binary</label>
447
- <input type="text" id="node-input-srtBinary" placeholder="srt" style="width:60%">
448
- </div>
449
-
450
- <div class="form-row">
451
- <label for="node-input-invocation"><i class="fa fa-play"></i> Invocation</label>
452
- <select id="node-input-invocation" style="width:70%">
453
- <option value="prompt">Prompt</option>
454
- <option value="skill">Skill</option>
455
- <option value="command">Command / Template</option>
456
- </select>
457
- </div>
458
-
459
- <div class="form-row agent-row-session">
460
- <label for="node-input-sessionIdProp"><i class="fa fa-key"></i> Session ID
461
- <i id="agent-session-id-hint" class="fa fa-info-circle" style="color:#888; cursor:help; margin-left:4px;"></i>
462
- </label>
463
- <input type="text" id="node-input-sessionIdProp" style="width:60%">
464
- <input type="hidden" id="node-input-sessionIdPropType">
465
- </div>
466
- <div class="form-tips agent-row-session-hint" style="display:none;">Pi does not support session continuation; switch Agent to OpenCode to use it.</div>
467
-
468
- <div class="form-row agent-row-prompt">
469
- <label for="node-input-prompt"><i class="fa fa-comment"></i> Prompt</label>
470
- <input type="text" id="node-input-prompt" placeholder="leave blank to use msg.payload" style="width:60%">
471
- <input type="hidden" id="node-input-promptType">
472
- </div>
473
-
474
- <div class="form-row agent-row-name">
475
- <label for="node-input-invocationName"><i class="fa fa-magic"></i> <span id="agent-name-label-text">Command</span></label>
476
- <input type="text" id="node-input-invocationName" style="width:60%">
477
- <input type="hidden" id="node-input-invocationNameType">
478
- </div>
479
- <div class="form-row agent-row-arguments">
480
- <label for="node-input-arguments"><i class="fa fa-list"></i> Arguments</label>
481
- <input type="text" id="node-input-arguments" placeholder="leave blank to use msg.payload" style="width:60%">
482
- <input type="hidden" id="node-input-argumentsType">
483
- </div>
484
-
485
- <div class="form-row">
486
- <label for="node-input-model"><i class="fa fa-microchip"></i> Model</label>
487
- <input type="text" id="node-input-model" placeholder="leave blank for agent default" style="width:60%">
488
- <input type="hidden" id="node-input-modelType">
489
- </div>
490
-
491
- <div class="form-row">
492
- <label for="node-input-cwd"><i class="fa fa-folder-open"></i> Working directory</label>
493
- <input type="text" id="node-input-cwd" placeholder="leave blank for Node-RED's own working directory" style="width:60%">
494
- <input type="hidden" id="node-input-cwdType">
495
- </div>
496
-
497
- <div class="form-row">
498
- <input type="checkbox" id="node-input-auto" style="display:inline-block; width:auto; vertical-align:top;">
499
- <label for="node-input-auto" style="width:auto;">Auto approve (dangerous)</label>
500
- <i id="agent-auto-hint" class="fa fa-info-circle" style="color:#888; cursor:help; margin-left:4px;"></i>
501
- </div>
502
-
503
- <div class="form-row">
504
- <label for="node-input-timeout"><i class="fa fa-clock-o"></i> Timeout</label>
505
- <input type="text" id="node-input-timeout" placeholder="seconds, leave empty for no timeout" style="width:60%">
506
- <input type="hidden" id="node-input-timeoutType">
507
- </div>
508
-
509
- <div class="form-row">
510
- <label for="node-input-concurrency"><i class="fa fa-tasks"></i> Concurrency</label>
511
- <input type="text" id="node-input-concurrency" placeholder="1" style="width:80px">
512
- <i class="fa fa-info-circle" style="color:#888; cursor:help; margin-left:4px;"
513
- title="Maximum parallel executions for this node. Additional incoming messages queue (FIFO) until a slot frees up. 1 = sequential."></i>
514
- </div>
515
-
516
- <div class="form-row agent-row-mcp">
517
- <label><i class="fa fa-plug"></i> MCP Servers</label>
518
- <ol id="node-input-mcpServers-list"></ol>
519
- </div>
520
- <div class="form-tips agent-row-mcp-hint" style="display:none;">Pi does not support MCP servers; switch Agent to OpenCode to use them.</div>
536
+ <div class="form-row">
537
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
538
+ <input type="text" id="node-input-name" placeholder="Agent" />
539
+ </div>
540
+
541
+ <div class="form-row">
542
+ <label for="node-input-agentName"
543
+ ><i class="fa fa-id-badge"></i> Agent name
544
+ <i
545
+ class="fa fa-info-circle"
546
+ style="color:#888; cursor:help; margin-left:4px;"
547
+ title="Reported as msg.agentName on the result/event outputs. Leave blank (str) to use the Name field above; set to msg/flow/global to make it dynamic per message."
548
+ ></i>
549
+ </label>
550
+ <input
551
+ type="text"
552
+ id="node-input-agentName"
553
+ placeholder="leave blank to use the Name field above"
554
+ style="width:60%"
555
+ />
556
+ <input type="hidden" id="node-input-agentNameType" />
557
+ </div>
558
+
559
+ <div class="form-row">
560
+ <label for="node-input-agent"><i class="fa fa-cube"></i> Agent</label>
561
+ <select id="node-input-agent" style="width:70%">
562
+ <option value="opencode">OpenCode</option>
563
+ <option value="pi">Pi</option>
564
+ </select>
565
+ </div>
566
+
567
+ <div class="form-row">
568
+ <label for="node-input-runtime"><i class="fa fa-cogs"></i> Runtime</label>
569
+ <select id="node-input-runtime" style="width:70%">
570
+ <option value="direct">Direct</option>
571
+ <option value="srt">SRT (sandboxed)</option>
572
+ </select>
573
+ </div>
574
+ <div class="form-row agent-row-srt">
575
+ <label for="node-input-srtSettingsMode"><i class="fa fa-shield"></i> SRT settings</label>
576
+ <select id="node-input-srtSettingsMode" style="width:60%">
577
+ <option value="file">File path</option>
578
+ <option value="inline">Inline</option>
579
+ </select>
580
+ </div>
581
+ <div class="form-row agent-row-srt agent-row-srt-file">
582
+ <label for="node-input-srtSettingsPath">&nbsp;</label>
583
+ <input
584
+ type="text"
585
+ id="node-input-srtSettingsPath"
586
+ placeholder="~/.srt-settings.json (srt's own default)"
587
+ style="width:60%"
588
+ />
589
+ </div>
590
+
591
+ <div class="form-row agent-row-srt agent-row-srt-inline">
592
+ <label>
593
+ <i class="fa fa-globe"></i> Allowed domains
594
+ <i
595
+ class="fa fa-info-circle"
596
+ style="color:#888; cursor:help; margin-left:4px;"
597
+ title="e.g. api.githubcopilot.com, opencode.ai -- the domain your configured model's API lives on."
598
+ ></i>
599
+ </label>
600
+ <ol id="node-input-srtAllowedDomains-list"></ol>
601
+ </div>
602
+
603
+ <div class="form-row agent-row-srt agent-row-srt-inline">
604
+ <label><i class="fa fa-folder"></i> Allowed write dirs</label>
605
+ <ol id="node-input-srtAllowedWriteDirs-list"></ol>
606
+ </div>
607
+
608
+ <div class="form-row agent-row-srt agent-row-srt-inline">
609
+ <input
610
+ type="checkbox"
611
+ id="node-input-srtStrictAllowlist"
612
+ style="display:inline-block; width:auto; vertical-align:top;"
613
+ />
614
+ <label for="node-input-srtStrictAllowlist" style="width:auto;">Strict allowlist</label>
615
+ <i
616
+ class="fa fa-info-circle"
617
+ style="color:#888; cursor:help; margin-left:4px;"
618
+ title="Deny any host not in the allowed domains list. There is no interactive prompt to fall back on in non-interactive mode, so this should normally stay on."
619
+ ></i>
620
+ </div>
621
+
622
+ <div class="form-row agent-row-srt agent-row-srt-inline">
623
+ <details>
624
+ <summary style="cursor:pointer;">Advanced (raw JSON)</summary>
625
+ <textarea
626
+ id="node-input-srtAdvancedJson"
627
+ rows="4"
628
+ style="width:100%; font-family:monospace;"
629
+ placeholder='If set, replaces the domains/directories above entirely, e.g. {"network":{"allowedDomains":[],"deniedDomains":[]},"filesystem":{"allowWrite":[],"denyRead":[],"denyWrite":[]}}'
630
+ ></textarea>
631
+ </details>
632
+ </div>
633
+
634
+ <div class="form-row agent-row-srt">
635
+ <label for="node-input-srtBinary"><i class="fa fa-terminal"></i> SRT binary</label>
636
+ <input type="text" id="node-input-srtBinary" placeholder="srt" style="width:60%" />
637
+ </div>
638
+
639
+ <div class="form-row">
640
+ <label for="node-input-invocation"><i class="fa fa-play"></i> Invocation</label>
641
+ <select id="node-input-invocation" style="width:70%">
642
+ <option value="prompt">Prompt</option>
643
+ <option value="skill">Skill</option>
644
+ <option value="command">Command / Template</option>
645
+ </select>
646
+ </div>
647
+
648
+ <div class="form-row agent-row-session">
649
+ <label for="node-input-sessionIdProp"
650
+ ><i class="fa fa-key"></i> Session ID
651
+ <i
652
+ id="agent-session-id-hint"
653
+ class="fa fa-info-circle"
654
+ style="color:#888; cursor:help; margin-left:4px;"
655
+ ></i>
656
+ </label>
657
+ <input type="text" id="node-input-sessionIdProp" style="width:60%" />
658
+ <input type="hidden" id="node-input-sessionIdPropType" />
659
+ </div>
660
+ <div class="form-tips agent-row-session-hint" style="display:none;">
661
+ Pi does not support session continuation; switch Agent to OpenCode to use it.
662
+ </div>
663
+
664
+ <div class="form-row agent-row-prompt">
665
+ <label for="node-input-prompt"><i class="fa fa-comment"></i> Prompt</label>
666
+ <input
667
+ type="text"
668
+ id="node-input-prompt"
669
+ placeholder="leave blank to use msg.payload"
670
+ style="width:60%"
671
+ />
672
+ <input type="hidden" id="node-input-promptType" />
673
+ </div>
674
+
675
+ <div class="form-row agent-row-name">
676
+ <label for="node-input-invocationName"
677
+ ><i class="fa fa-magic"></i> <span id="agent-name-label-text">Command</span></label
678
+ >
679
+ <input type="text" id="node-input-invocationName" style="width:60%" />
680
+ <input type="hidden" id="node-input-invocationNameType" />
681
+ </div>
682
+ <div class="form-row agent-row-arguments">
683
+ <label for="node-input-arguments"><i class="fa fa-list"></i> Arguments</label>
684
+ <input
685
+ type="text"
686
+ id="node-input-arguments"
687
+ placeholder="leave blank to use msg.payload"
688
+ style="width:60%"
689
+ />
690
+ <input type="hidden" id="node-input-argumentsType" />
691
+ </div>
692
+
693
+ <div class="form-row agent-row-arguments">
694
+ <label
695
+ ><i class="fa fa-tags"></i> Inputs
696
+ <i
697
+ class="fa fa-info-circle"
698
+ style="color:#888; cursor:help; margin-left:4px;"
699
+ title="Named values substituted into Arguments as $INPUTS.<name> before invocation. Pure text templating -- unmatched tokens are left as literal text."
700
+ ></i>
701
+ </label>
702
+ <ol id="node-input-inputs-list"></ol>
703
+ </div>
704
+
705
+ <div class="form-row">
706
+ <label for="node-input-model"><i class="fa fa-microchip"></i> Model</label>
707
+ <input
708
+ type="text"
709
+ id="node-input-model"
710
+ placeholder="leave blank for agent default"
711
+ style="width:60%"
712
+ />
713
+ <input type="hidden" id="node-input-modelType" />
714
+ </div>
715
+
716
+ <div class="form-row">
717
+ <label for="node-input-cwd"><i class="fa fa-folder-open"></i> Working directory</label>
718
+ <input
719
+ type="text"
720
+ id="node-input-cwd"
721
+ placeholder="leave blank for Node-RED's own working directory"
722
+ style="width:60%"
723
+ />
724
+ <input type="hidden" id="node-input-cwdType" />
725
+ </div>
726
+
727
+ <div class="form-row">
728
+ <input
729
+ type="checkbox"
730
+ id="node-input-auto"
731
+ style="display:inline-block; width:auto; vertical-align:top;"
732
+ />
733
+ <label for="node-input-auto" style="width:auto;">Auto approve (dangerous)</label>
734
+ <i
735
+ id="agent-auto-hint"
736
+ class="fa fa-info-circle"
737
+ style="color:#888; cursor:help; margin-left:4px;"
738
+ ></i>
739
+ </div>
740
+
741
+ <div class="form-row">
742
+ <label for="node-input-timeout"><i class="fa fa-clock-o"></i> Timeout</label>
743
+ <input
744
+ type="text"
745
+ id="node-input-timeout"
746
+ placeholder="seconds, leave empty for no timeout"
747
+ style="width:60%"
748
+ />
749
+ <input type="hidden" id="node-input-timeoutType" />
750
+ </div>
751
+
752
+ <div class="form-row">
753
+ <label for="node-input-concurrency"><i class="fa fa-tasks"></i> Concurrency</label>
754
+ <input type="text" id="node-input-concurrency" placeholder="1" style="width:80px" />
755
+ <i
756
+ class="fa fa-info-circle"
757
+ style="color:#888; cursor:help; margin-left:4px;"
758
+ title="Maximum parallel executions for this node. Additional incoming messages queue (FIFO) until a slot frees up. 1 = sequential."
759
+ ></i>
760
+ </div>
761
+
762
+ <div class="form-row agent-row-mcp">
763
+ <label><i class="fa fa-plug"></i> MCP Servers</label>
764
+ <ol id="node-input-mcpServers-list"></ol>
765
+ </div>
766
+ <div class="form-tips agent-row-mcp-hint" style="display:none;">
767
+ Pi does not support MCP servers; switch Agent to OpenCode to use them.
768
+ </div>
769
+
770
+ <div class="form-row">
771
+ <label for="node-input-retryMaxAttempts"
772
+ ><i class="fa fa-refresh"></i> Retry attempts
773
+ <i
774
+ class="fa fa-info-circle"
775
+ style="color:#888; cursor:help; margin-left:4px;"
776
+ title="Total attempts for this execution, including the first (2 = one retry). Retries only happen for failed/timeout results the classifier considers retryable -- see On error below."
777
+ ></i>
778
+ </label>
779
+ <input
780
+ type="text"
781
+ id="node-input-retryMaxAttempts"
782
+ placeholder="2"
783
+ style="width:80px"
784
+ />
785
+ </div>
786
+
787
+ <div class="form-row">
788
+ <label for="node-input-retryDelayMs"><i class="fa fa-clock-o"></i> Retry delay (ms)</label>
789
+ <input type="text" id="node-input-retryDelayMs" placeholder="3000" style="width:80px" />
790
+ </div>
791
+
792
+ <div class="form-row">
793
+ <label for="node-input-retryOnError"><i class="fa fa-exclamation-triangle"></i> On error</label>
794
+ <select id="node-input-retryOnError" style="width:60%">
795
+ <option value="transient">Retry transient errors only (default)</option>
796
+ <option value="all">Retry any non-fatal error</option>
797
+ </select>
798
+ </div>
799
+
800
+ <div class="form-row">
801
+ <label for="node-input-systemPrompt"
802
+ ><i class="fa fa-file-text-o"></i> System prompt
803
+ <i
804
+ class="fa fa-info-circle"
805
+ style="color:#888; cursor:help; margin-left:4px;"
806
+ title="Not currently supported by either adapter's CLI -- always ignored with a warning. Kept as a config field for forward-compatibility."
807
+ ></i>
808
+ </label>
809
+ <input
810
+ type="text"
811
+ id="node-input-systemPrompt"
812
+ placeholder="not currently supported by any adapter"
813
+ style="width:60%"
814
+ disabled
815
+ />
816
+ <input type="hidden" id="node-input-systemPromptType" />
817
+ </div>
818
+
819
+ <div class="form-row agent-row-effort">
820
+ <label for="node-input-effort"
821
+ ><i class="fa fa-tachometer"></i> Effort
822
+ <i
823
+ class="fa fa-info-circle"
824
+ style="color:#888; cursor:help; margin-left:4px;"
825
+ title="Reasoning depth, e.g. low/medium/high/max -- maps to OpenCode's --variant. Not validated against a fixed enum; whatever string is entered is passed through."
826
+ ></i>
827
+ </label>
828
+ <input
829
+ type="text"
830
+ id="node-input-effort"
831
+ placeholder="e.g. low, medium, high"
832
+ style="width:60%"
833
+ />
834
+ <input type="hidden" id="node-input-effortType" />
835
+ </div>
836
+ <div class="form-tips agent-row-effort-hint" style="display:none;">
837
+ Pi has no verified effort/reasoning-depth flag; switch Agent to OpenCode to use it.
838
+ </div>
839
+
840
+ <div class="form-row">
841
+ <label
842
+ ><i class="fa fa-check-square-o"></i> Allowed tools
843
+ <i
844
+ class="fa fa-info-circle"
845
+ style="color:#888; cursor:help; margin-left:4px;"
846
+ title="Restricts which tools the agent may use. OpenCode: materialized as a temp agent config (tools map) selected via --agent. Pi: passed as --tools (overrides the auto-derived default)."
847
+ ></i>
848
+ </label>
849
+ <ol id="node-input-allowedTools-list"></ol>
850
+ </div>
851
+ <div class="form-row">
852
+ <label
853
+ ><i class="fa fa-ban"></i> Denied tools
854
+ <i
855
+ class="fa fa-info-circle"
856
+ style="color:#888; cursor:help; margin-left:4px;"
857
+ title="OpenCode only: tools explicitly set to false in the materialized agent config. Pi's --tools flag is allow-list-only, so this list is not used for Pi."
858
+ ></i>
859
+ </label>
860
+ <ol id="node-input-deniedTools-list"></ol>
861
+ </div>
862
+
863
+ <div class="form-row">
864
+ <label
865
+ ><i class="fa fa-code"></i> Output format (JSON Schema)
866
+ <i
867
+ class="fa fa-info-circle"
868
+ style="color:#888; cursor:help; margin-left:4px;"
869
+ title="Optional. If set, the prompt is augmented to ask for JSON matching this schema, the response is validated with AJV, and (both adapters today) up to 3 reask turns are attempted on failure. Leave blank to disable (default)."
870
+ ></i>
871
+ </label>
872
+ <textarea
873
+ id="node-input-outputFormat"
874
+ rows="4"
875
+ style="width:100%; font-family:monospace;"
876
+ placeholder='e.g. {"type":"object","properties":{"answer":{"type":"string"}},"required":["answer"]}'
877
+ ></textarea>
878
+ </div>
521
879
  </script>
522
880
 
523
881
  <script type="text/html" data-help-name="agent">
524
- <p>Runs a coding agent (<b>OpenCode</b> or <b>Pi</b>) non-interactively,
525
- either directly on this host or sandboxed through <code>srt</code>.</p>
526
- <h3>Inputs</h3>
527
- <dl class="message-properties">
528
- <dt class="optional">payload <span class="property-type">string</span></dt>
529
- <dd>Used as the prompt/arguments if the corresponding field is left at
530
- its default (<code>msg.payload</code>).</dd>
531
- <dt class="optional">cwd <span class="property-type">string</span></dt>
532
- <dd>Used as the working directory if the Working directory field is
533
- left at its default (<code>msg.cwd</code>).</dd>
534
- <dt class="optional">sessionID <span class="property-type">string</span></dt>
535
- <dd>Used to resume a previous session if the Session ID field is left
536
- at its default (<code>msg.sessionID</code>). Empty/absent starts a
537
- new session. OpenCode only.</dd>
538
- <dt class="optional">operation <span class="property-type">string</span></dt>
539
- <dd>Set to <code>"terminate"</code> to kill an in-flight (or still-
540
- queued) execution of this node instance instead of starting a new
541
- one -- see <code>executionId</code> below.</dd>
542
- <dt class="optional">executionId <span class="property-type">string</span></dt>
543
- <dd>Required when <code>operation</code> is <code>"terminate"</code>:
544
- the id of the execution to kill, as returned by an earlier run in
545
- <code>msg.agentExecution.id</code> or an Events message's
546
- <code>executionId</code>.</dd>
547
- <dt class="optional">concurrency <span class="property-type">number</span></dt>
548
- <dd>Overrides the Concurrency field at runtime, applied before this
549
- message is submitted -- no redeploy needed. Raising it starts any
550
- already-queued messages immediately; lowering it only affects
551
- future scheduling (already-running executions are never
552
- cancelled). Non-numeric/non-positive values are ignored.</dd>
553
- </dl>
554
- <h3>Outputs</h3>
555
- <ol class="node-ports">
556
- <li>Result
557
- <dl class="message-properties">
558
- <dt>payload <span class="property-type">string</span></dt>
559
- <dd>The agent's final text reply.</dd>
560
- <dt>sessionID <span class="property-type">string</span></dt>
561
- <dd>The session this execution ran in (new or resumed). Feed
562
- this back into the Session ID field to continue the
563
- conversation on the next message.</dd>
564
- <dt>agentExecution <span class="property-type">object</span></dt>
565
- <dd>id, status, exitCode, signal, timedOut, durationMs, sessionID.</dd>
566
- <dt>agentId <span class="property-type">string</span></dt>
567
- <dd>This node instance's own Node-RED id, so a flow with
568
- several agent nodes can tell which one produced a
569
- given message.</dd>
570
- <dt>agentName <span class="property-type">string</span></dt>
571
- <dd>The resolved Agent name field, falling back to this node
572
- instance's configured Name if the Agent name field is
573
- blank/unresolved for this message (e.g. via
574
- <code>msg.agentName</code> when configured as a msg
575
- property).</dd>
576
- </dl>
577
- </li>
578
- <li>Events
579
- <dl class="message-properties">
580
- <dt>payload <span class="property-type">object</span></dt>
581
- <dd>One structured event per message as the agent runs
582
- (queued/started/tool/agent/progress/completed/failed/timeout/cancelled).</dd>
583
- <dt>executionId <span class="property-type">string</span></dt>
584
- <dd>Correlates events (and the eventual result) belonging to
585
- the same execution when several run concurrently.</dd>
586
- <dt>agentId <span class="property-type">string</span></dt>
587
- <dd>This node instance's own Node-RED id (see Result output).</dd>
588
- <dt>agentName <span class="property-type">string</span></dt>
589
- <dd>The resolved Agent name for this execution (see Result output).</dd>
590
- </dl>
591
- </li>
592
- </ol>
593
- <p><b>Concurrency</b> bounds how many executions this node runs at once
594
- (default 1 = sequential). Extra incoming messages wait in an internal
595
- FIFO queue -- each waiting message emits a <code>queued</code> event
596
- immediately, then <code>started</code> once a slot frees up. The node
597
- status shows e.g. <code>2 running &middot; 3 queued</code>. Results may
598
- complete in a different order than the messages arrived; ordering is
599
- not guaranteed above concurrency 1. On redeploy/removal, queued
600
- messages are cancelled (a <code>cancelled</code> event plus a Node-RED
601
- error) and any still-running agent processes are terminated. Send
602
- <code>msg.concurrency</code> to change the bound at runtime without a
603
- redeploy -- see Inputs below.</p>
604
- <p><b>Terminating a specific execution on demand</b>: send a message with
605
- <code>msg.operation = "terminate"</code> and <code>msg.executionId</code>
606
- set to the id to kill (from a prior run's <code>msg.agentExecution.id</code>
607
- or an Events message's <code>executionId</code>). A still-queued
608
- execution is cancelled immediately (never spawned); an active one gets
609
- SIGTERM to its whole process group, escalating to SIGKILL after a grace
610
- period -- this works the same way for both the Direct and SRT runtimes.
611
- The terminated execution's own original trigger still completes on its
612
- own (status <code>failed</code>, with the kill signal) once the process
613
- actually exits; this terminate trigger's own output is only the
614
- immediate "kill requested" acknowledgement.</p>
615
- <p><b>Auto approve</b> maps to OpenCode's <code>--auto</code> (off by
616
- default -- without it, any action gated by a permission policy not
617
- already set to "allow" is auto-rejected, but the run still completes).
618
- Pi has no equivalent permission-prompt system to bypass in
619
- non-interactive mode -- for Pi this instead restricts the available
620
- tools to read-only (<code>read/grep/find/ls</code>) when off, and
621
- allows everything (including <code>bash/edit/write</code>) when on.</p>
622
- <p><b>Skill</b>/<b>Command</b> invocation: for OpenCode this is the name
623
- of a discovered skill or a <code>.opencode/command(s)/&lt;name&gt;.md</code>
624
- file, dispatched deterministically via <code>--command</code>. Pi has
625
- no equivalent dispatch mechanism -- the name is resolved to a file
626
- under <code>.github/skills/</code> (skill) or
627
- <code>.github/prompts/</code> (command), passed via
628
- <code>--skill</code>/<code>--prompt-template</code>, and the model is
629
- explicitly told in the prompt to use it. Pi also does not support
630
- configuring MCP servers through this node.</p>
631
- <p><b>Session ID</b>: leave blank to always start a fresh session (the
632
- default). Set it (typically from the previous result's
633
- <code>msg.sessionID</code>) to continue that conversation instead --
634
- maps to OpenCode's <code>opencode run --session &lt;id&gt; ...</code>.
635
- Only the OpenCode agent supports this; Pi rejects a non-empty Session
636
- ID (every Pi run uses <code>--no-session</code>).</p>
637
- <p><b>SRT</b> runtime shells out to the <code>srt</code> sandbox-runtime
638
- CLI already installed on this host; its network/filesystem policy is
639
- configured via its own settings file (default <code>~/.srt-settings.json</code>),
640
- not through this node.</p>
882
+ <p>
883
+ Runs a coding agent (<b>OpenCode</b> or <b>Pi</b>) non-interactively, either directly on this
884
+ host or sandboxed through <code>srt</code>.
885
+ </p>
886
+ <h3>Inputs</h3>
887
+ <dl class="message-properties">
888
+ <dt class="optional">payload <span class="property-type">string</span></dt>
889
+ <dd>
890
+ Used as the prompt/arguments if the corresponding field is left at its default
891
+ (<code>msg.payload</code>).
892
+ </dd>
893
+ <dt class="optional">cwd <span class="property-type">string</span></dt>
894
+ <dd>
895
+ Used as the working directory if the Working directory field is left at its default
896
+ (<code>msg.cwd</code>).
897
+ </dd>
898
+ <dt class="optional">sessionID <span class="property-type">string</span></dt>
899
+ <dd>
900
+ Used to resume a previous session if the Session ID field is left at its default
901
+ (<code>msg.sessionID</code>). Empty/absent starts a new session. OpenCode only.
902
+ </dd>
903
+ <dt class="optional">operation <span class="property-type">string</span></dt>
904
+ <dd>
905
+ Set to <code>"terminate"</code> to kill an in-flight (or still- queued) execution of this node
906
+ instance instead of starting a new one -- see <code>executionId</code> below.
907
+ </dd>
908
+ <dt class="optional">executionId <span class="property-type">string</span></dt>
909
+ <dd>
910
+ Required when <code>operation</code> is <code>"terminate"</code>: the id of the execution to
911
+ kill, as returned by an earlier run in <code>msg.agentExecution.id</code> or an Events
912
+ message's <code>executionId</code>.
913
+ </dd>
914
+ <dt class="optional">concurrency <span class="property-type">number</span></dt>
915
+ <dd>
916
+ Overrides the Concurrency field at runtime, applied before this message is submitted -- no
917
+ redeploy needed. Raising it starts any already-queued messages immediately; lowering it only
918
+ affects future scheduling (already-running executions are never cancelled).
919
+ Non-numeric/non-positive values are ignored.
920
+ </dd>
921
+ </dl>
922
+ <h3>Outputs</h3>
923
+ <ol class="node-ports">
924
+ <li>
925
+ Result
926
+ <dl class="message-properties">
927
+ <dt>payload <span class="property-type">string</span></dt>
928
+ <dd>The agent's final text reply.</dd>
929
+ <dt>sessionID <span class="property-type">string</span></dt>
930
+ <dd>
931
+ The session this execution ran in (new or resumed). Feed this back into the Session ID
932
+ field to continue the conversation on the next message.
933
+ </dd>
934
+ <dt>agentExecution <span class="property-type">object</span></dt>
935
+ <dd>id, status, exitCode, signal, timedOut, durationMs, sessionID.</dd>
936
+ <dt>agentId <span class="property-type">string</span></dt>
937
+ <dd>
938
+ This node instance's own Node-RED id, so a flow with several agent nodes can tell which
939
+ one produced a given message.
940
+ </dd>
941
+ <dt>agentName <span class="property-type">string</span></dt>
942
+ <dd>
943
+ The resolved Agent name field, falling back to this node instance's configured Name if the
944
+ Agent name field is blank/unresolved for this message (e.g. via
945
+ <code>msg.agentName</code> when configured as a msg property).
946
+ </dd>
947
+ </dl>
948
+ </li>
949
+ <li>
950
+ Events
951
+ <dl class="message-properties">
952
+ <dt>payload <span class="property-type">object</span></dt>
953
+ <dd>
954
+ One structured event per message as the agent runs
955
+ (queued/started/tool/agent/progress/completed/failed/timeout/cancelled).
956
+ </dd>
957
+ <dt>executionId <span class="property-type">string</span></dt>
958
+ <dd>
959
+ Correlates events (and the eventual result) belonging to the same execution when several
960
+ run concurrently.
961
+ </dd>
962
+ <dt>agentId <span class="property-type">string</span></dt>
963
+ <dd>This node instance's own Node-RED id (see Result output).</dd>
964
+ <dt>agentName <span class="property-type">string</span></dt>
965
+ <dd>The resolved Agent name for this execution (see Result output).</dd>
966
+ </dl>
967
+ </li>
968
+ </ol>
969
+ <p>
970
+ <b>Concurrency</b> bounds how many executions this node runs at once (default 1 = sequential).
971
+ Extra incoming messages wait in an internal FIFO queue -- each waiting message emits a
972
+ <code>queued</code> event immediately, then <code>started</code> once a slot frees up. The node
973
+ status shows e.g. <code>2 running &middot; 3 queued</code>. Results may complete in a different
974
+ order than the messages arrived; ordering is not guaranteed above concurrency 1. On
975
+ redeploy/removal, queued messages are cancelled (a <code>cancelled</code> event plus a Node-RED
976
+ error) and any still-running agent processes are terminated. Send
977
+ <code>msg.concurrency</code> to change the bound at runtime without a redeploy -- see Inputs
978
+ below.
979
+ </p>
980
+ <p>
981
+ <b>Terminating a specific execution on demand</b>: send a message with
982
+ <code>msg.operation = "terminate"</code> and <code>msg.executionId</code> set to the id to kill
983
+ (from a prior run's <code>msg.agentExecution.id</code> or an Events message's
984
+ <code>executionId</code>). A still-queued execution is cancelled immediately (never spawned); an
985
+ active one gets SIGTERM to its whole process group, escalating to SIGKILL after a grace period
986
+ -- this works the same way for both the Direct and SRT runtimes. The terminated execution's own
987
+ original trigger still completes on its own (status <code>failed</code>, with the kill signal)
988
+ once the process actually exits; this terminate trigger's own output is only the immediate "kill
989
+ requested" acknowledgement.
990
+ </p>
991
+ <p>
992
+ <b>Auto approve</b> maps to OpenCode's <code>--auto</code> (off by default -- without it, any
993
+ action gated by a permission policy not already set to "allow" is auto-rejected, but the run
994
+ still completes). Pi has no equivalent permission-prompt system to bypass in non-interactive
995
+ mode -- for Pi this instead restricts the available tools to read-only
996
+ (<code>read/grep/find/ls</code>) when off, and allows everything (including
997
+ <code>bash/edit/write</code>) when on.
998
+ </p>
999
+ <p>
1000
+ <b>Skill</b>/<b>Command</b> invocation: for OpenCode this is the name of a discovered skill or a
1001
+ <code>.opencode/command(s)/&lt;name&gt;.md</code> file, dispatched deterministically via
1002
+ <code>--command</code>. Pi has no equivalent dispatch mechanism -- the name is resolved to a
1003
+ file under <code>.github/skills/</code> (skill) or <code>.github/prompts/</code> (command),
1004
+ passed via <code>--skill</code>/<code>--prompt-template</code>, and the model is explicitly told
1005
+ in the prompt to use it. Pi also does not support configuring MCP servers through this node.
1006
+ </p>
1007
+ <p>
1008
+ <b>Session ID</b>: leave blank to always start a fresh session (the default). Set it (typically
1009
+ from the previous result's <code>msg.sessionID</code>) to continue that conversation instead --
1010
+ maps to OpenCode's <code>opencode run --session &lt;id&gt; ...</code>. Only the OpenCode agent
1011
+ supports this; Pi rejects a non-empty Session ID (every Pi run uses <code>--no-session</code>).
1012
+ </p>
1013
+ <p>
1014
+ <b>SRT</b> runtime shells out to the <code>srt</code> sandbox-runtime CLI already installed on
1015
+ this host; its network/filesystem policy is configured via its own settings file (default
1016
+ <code>~/.srt-settings.json</code>), not through this node.
1017
+ </p>
1018
+ <p>
1019
+ <b>System prompt</b>, <b>Effort</b>, and <b>Allowed/Denied tools</b> are forwarded to the
1020
+ underlying CLI only where that adapter actually supports it -- a field set for an adapter that
1021
+ doesn't will be silently dropped with a Node-RED warning, never a hard error.
1022
+ <b>System prompt</b> has no verified CLI flag for either adapter yet and is always dropped.
1023
+ <b>Effort</b> (reasoning depth, e.g. <code>low</code>/<code>medium</code>/<code>high</code>)
1024
+ maps to OpenCode's <code>--variant</code>; Pi has no equivalent.
1025
+ <b>Allowed/ Denied tools</b> are supported by both: for OpenCode they're materialized into a
1026
+ temporary, per-run agent config (delivered the same way as MCP servers, via
1027
+ <code>OPENCODE_CONFIG_CONTENT</code>) selected with <code>--agent</code>; for Pi, a non-empty
1028
+ Allowed tools list is passed as <code>--tools</code> (Pi has no deny-list mechanism, so Denied
1029
+ tools has no effect for Pi).
1030
+ </p>
641
1031
  </script>