@tbrandenburg/node-red-agents 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +34 -0
- package/nodes/agent/agent.html +550 -0
- package/nodes/agent/agent.js +396 -0
- package/nodes/agent/icons/agent.svg +27 -0
- package/nodes/agent/lib/agents/base.js +42 -0
- package/nodes/agent/lib/agents/opencode.js +141 -0
- package/nodes/agent/lib/agents/pi.js +220 -0
- package/nodes/agent/lib/execution/lifecycle.js +69 -0
- package/nodes/agent/lib/execution/scheduler.js +97 -0
- package/nodes/agent/lib/execution/status.js +24 -0
- package/nodes/agent/lib/mcp/normalize.js +31 -0
- package/nodes/agent/lib/runtimes/base.js +21 -0
- package/nodes/agent/lib/runtimes/direct.js +28 -0
- package/nodes/agent/lib/runtimes/process-exec.js +105 -0
- package/nodes/agent/lib/runtimes/srt.js +63 -0
- package/nodes/agent-server/agent-server.html +365 -0
- package/nodes/agent-server/agent-server.js +481 -0
- package/nodes/agent-server/icons/agent.svg +27 -0
- package/nodes/agent-server/lib/daemon.js +149 -0
- package/nodes/agent-server/lib/http.js +60 -0
- package/nodes/agent-server/lib/model.js +20 -0
- package/nodes/agent-server/lib/port.js +31 -0
- package/nodes/agent-server/lib/registry.js +77 -0
- package/nodes/agent-server/lib/status.js +15 -0
- package/nodes/gh/README.md +75 -0
- package/nodes/gh/examples/list-pull-requests.json +48 -0
- package/nodes/gh/examples/run-workflow.json +42 -0
- package/nodes/gh/gh.html +146 -0
- package/nodes/gh/gh.js +237 -0
- package/nodes/gh/icons/gh.svg +15 -0
- package/nodes/gh/lib/parse-args.js +67 -0
- package/package.json +60 -0
- package/shared/srt-settings.js +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tom Brandenburg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# node-red-agents
|
|
2
|
+
|
|
3
|
+
Node-RED nodes for running coding agents and GitHub CLI operations from a
|
|
4
|
+
flow.
|
|
5
|
+
|
|
6
|
+
## Nodes
|
|
7
|
+
|
|
8
|
+
- **agent** — runs a coding-agent CLI (OpenCode first, `pi` also
|
|
9
|
+
supported) either directly or sandboxed via SRT (Anthropic's
|
|
10
|
+
sandbox-runtime), one execution per input message.
|
|
11
|
+
- **agent-server** — manages a long-lived `opencode serve` daemon
|
|
12
|
+
(session-based, SRT-sandboxable), for flows that need repeated
|
|
13
|
+
low-latency calls instead of `agent`'s one-shot execution model.
|
|
14
|
+
- **gh** — runs GitHub CLI (`gh`) commands and returns parsed output.
|
|
15
|
+
|
|
16
|
+
See each node's built-in help (Node-RED editor info panel) for
|
|
17
|
+
configuration details, or `nodes/gh/README.md` for `gh`-specific usage
|
|
18
|
+
and example flows (`nodes/gh/examples/`).
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Node-RED >= 4.0.0, Node.js >= 22
|
|
23
|
+
- The [`opencode`](https://opencode.ai) CLI on `PATH` (for `agent`/`agent-server`)
|
|
24
|
+
- [`srt`](https://github.com/anthropics/sandbox-runtime) on `PATH`, only if using the SRT runtime option
|
|
25
|
+
- The [`gh`](https://cli.github.com) CLI on `PATH`, authenticated (for `gh`)
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm install @tbrandenburg/node-red-agents
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then restart Node-RED, or install via the editor: **Menu -> Manage
|
|
34
|
+
palette -> Install tab -> search "node-red-agents"**.
|
|
@@ -0,0 +1,550 @@
|
|
|
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
|
+
'lund-anthropic/claude-sonnet-5',
|
|
12
|
+
'lund-anthropic/claude-opus-4-8',
|
|
13
|
+
'lund-anthropic/claude-haiku-4-5',
|
|
14
|
+
'github-copilot/claude-sonnet-5',
|
|
15
|
+
'github-copilot/gpt-5.4',
|
|
16
|
+
'lund-openai-gpt5/gpt-5.4',
|
|
17
|
+
'lund-openai-gpt4/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
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
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
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
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
|
+
model: { value: '' },
|
|
88
|
+
modelType: { value: 'str' },
|
|
89
|
+
|
|
90
|
+
prompt: { value: 'payload' },
|
|
91
|
+
promptType: { value: 'msg' },
|
|
92
|
+
|
|
93
|
+
sessionIdProp: { value: 'sessionID' },
|
|
94
|
+
sessionIdPropType: { value: 'msg' },
|
|
95
|
+
|
|
96
|
+
invocationName: { value: '' },
|
|
97
|
+
invocationNameType: { value: 'str' },
|
|
98
|
+
|
|
99
|
+
arguments: { value: 'payload' },
|
|
100
|
+
argumentsType: { value: 'msg' },
|
|
101
|
+
|
|
102
|
+
cwd: { value: 'cwd' },
|
|
103
|
+
cwdType: { value: 'msg' },
|
|
104
|
+
|
|
105
|
+
auto: { value: false },
|
|
106
|
+
|
|
107
|
+
concurrency: { value: 1, validate: RED.validators.number() },
|
|
108
|
+
|
|
109
|
+
timeout: { value: '' },
|
|
110
|
+
timeoutType: { value: 'num' },
|
|
111
|
+
|
|
112
|
+
mcpServers: { value: [] },
|
|
113
|
+
|
|
114
|
+
srtBinary: { value: '' },
|
|
115
|
+
srtSettingsMode: { value: 'file' },
|
|
116
|
+
srtSettingsPath: { value: '' },
|
|
117
|
+
srtAllowedDomains: { value: [] },
|
|
118
|
+
srtAllowedWriteDirs: { value: ['.', '/tmp', '~/.local/share/opencode'] },
|
|
119
|
+
srtStrictAllowlist: { value: true },
|
|
120
|
+
srtAdvancedJson: { value: '' }
|
|
121
|
+
},
|
|
122
|
+
inputs: 1,
|
|
123
|
+
outputs: 2,
|
|
124
|
+
outputLabels: ['result', 'events'],
|
|
125
|
+
icon: 'agent.svg',
|
|
126
|
+
paletteLabel: 'agent',
|
|
127
|
+
label: function () {
|
|
128
|
+
return this.name || (this.agent + ' (' + this.runtime + ')');
|
|
129
|
+
},
|
|
130
|
+
oneditprepare: function () {
|
|
131
|
+
const node = this;
|
|
132
|
+
|
|
133
|
+
$('#node-input-model').typedInput({
|
|
134
|
+
typeField: '#node-input-modelType',
|
|
135
|
+
types: ['str', 'msg', 'flow', 'global', 'env']
|
|
136
|
+
});
|
|
137
|
+
$('#node-input-model').attr('list', 'agent-model-suggestions');
|
|
138
|
+
if (!$('#agent-model-suggestions').length) {
|
|
139
|
+
const datalist = $('<datalist id="agent-model-suggestions"></datalist>').appendTo('body');
|
|
140
|
+
MODEL_SUGGESTIONS.forEach((m) => datalist.append('<option value="' + m + '">'));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
$('#node-input-prompt').typedInput({
|
|
144
|
+
typeField: '#node-input-promptType',
|
|
145
|
+
types: ['msg', 'str', 'flow', 'global', 'env']
|
|
146
|
+
});
|
|
147
|
+
$('#node-input-sessionIdProp').typedInput({
|
|
148
|
+
typeField: '#node-input-sessionIdPropType',
|
|
149
|
+
types: ['msg', 'str', 'flow', 'global']
|
|
150
|
+
});
|
|
151
|
+
// RED.popover (Node-RED's own hover-tooltip system) rather
|
|
152
|
+
// than a plain HTML `title` attribute: `title` is the
|
|
153
|
+
// browser's native tooltip, whose appearance/delay/is even
|
|
154
|
+
// shown at all depends on OS/browser/accessibility settings
|
|
155
|
+
// outside our control (reported: not appearing at all in
|
|
156
|
+
// Firefox). RED.popover.create with trigger:'hover' is
|
|
157
|
+
// Node-RED's own JS-driven popover, used throughout its own
|
|
158
|
+
// editor (e.g. the debug sidebar's copy buttons) -- it
|
|
159
|
+
// doesn't depend on the browser's native tooltip behavior at
|
|
160
|
+
// all, so it renders identically regardless of browser.
|
|
161
|
+
RED.popover.create({
|
|
162
|
+
target: $('#agent-session-id-hint'),
|
|
163
|
+
direction: 'right',
|
|
164
|
+
trigger: 'hover',
|
|
165
|
+
size: 'small',
|
|
166
|
+
width: 320,
|
|
167
|
+
content:
|
|
168
|
+
'Empty/absent → starts a <b>new</b> session (today\'s default). A <b>present</b> id ' +
|
|
169
|
+
'resumes that session instead (<code>opencode run --session <id> ...</code>). The ' +
|
|
170
|
+
'result\'s <code>msg.sessionID</code> (also under <code>agentExecution.sessionID</code>) ' +
|
|
171
|
+
'can be fed straight back into this field on the next message to continue the ' +
|
|
172
|
+
'conversation. OpenCode only -- Pi does not support session continuation.'
|
|
173
|
+
});
|
|
174
|
+
$('#node-input-invocationName').typedInput({
|
|
175
|
+
typeField: '#node-input-invocationNameType',
|
|
176
|
+
types: ['str', 'msg', 'flow', 'global']
|
|
177
|
+
});
|
|
178
|
+
$('#node-input-arguments').typedInput({
|
|
179
|
+
typeField: '#node-input-argumentsType',
|
|
180
|
+
types: ['msg', 'str', 'flow', 'global', 'env']
|
|
181
|
+
});
|
|
182
|
+
$('#node-input-cwd').typedInput({
|
|
183
|
+
typeField: '#node-input-cwdType',
|
|
184
|
+
types: ['msg', 'str', 'flow', 'global', 'env']
|
|
185
|
+
});
|
|
186
|
+
$('#node-input-timeout').typedInput({
|
|
187
|
+
typeField: '#node-input-timeoutType',
|
|
188
|
+
types: ['num', 'msg', 'flow', 'global', 'env']
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const NAME_PLACEHOLDERS = {
|
|
192
|
+
opencode: { skill: 'code-review', command: 'review' },
|
|
193
|
+
pi: { skill: 'code-review (looked up under .github/skills/)', command: 'review (looked up under .github/prompts/)' }
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
function updateInvocationRows() {
|
|
197
|
+
const invocation = $('#node-input-invocation').val();
|
|
198
|
+
const agent = $('#node-input-agent').val();
|
|
199
|
+
$('.agent-row-prompt').toggle(invocation === 'prompt');
|
|
200
|
+
$('.agent-row-name').toggle(invocation !== 'prompt');
|
|
201
|
+
$('.agent-row-arguments').toggle(invocation !== 'prompt');
|
|
202
|
+
const label = invocation === 'skill' ? 'Skill' : 'Command';
|
|
203
|
+
$('#agent-name-label-text').text(label);
|
|
204
|
+
const placeholders = NAME_PLACEHOLDERS[agent] || NAME_PLACEHOLDERS.opencode;
|
|
205
|
+
$('#node-input-invocationName').attr('placeholder', placeholders[invocation] || '');
|
|
206
|
+
}
|
|
207
|
+
$('#node-input-invocation').on('change', updateInvocationRows);
|
|
208
|
+
|
|
209
|
+
function updateRuntimeRows() {
|
|
210
|
+
$('.agent-row-srt').toggle($('#node-input-runtime').val() === 'srt');
|
|
211
|
+
}
|
|
212
|
+
$('#node-input-runtime').on('change', updateRuntimeRows);
|
|
213
|
+
|
|
214
|
+
function updateSrtModeRows() {
|
|
215
|
+
const mode = $('#node-input-srtSettingsMode').val();
|
|
216
|
+
$('.agent-row-srt-file').toggle(mode === 'file');
|
|
217
|
+
$('.agent-row-srt-inline').toggle(mode === 'inline');
|
|
218
|
+
}
|
|
219
|
+
$('#node-input-srtSettingsMode').on('change', updateSrtModeRows);
|
|
220
|
+
|
|
221
|
+
const AUTO_HINTS = {
|
|
222
|
+
opencode: 'Maps to --auto: skips permission prompts for actions not explicitly denied.',
|
|
223
|
+
pi: 'Pi has no permission prompts to bypass. Off = read-only tools (read/grep/find/ls). On = full tool access (bash/edit/write too).'
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Grey out (rather than hide) fields an agent doesn't
|
|
227
|
+
// support, so it's clear the setting exists but won't take
|
|
228
|
+
// effect for the currently selected agent -- instead of the
|
|
229
|
+
// section silently disappearing.
|
|
230
|
+
function setSectionSupported($section, supported) {
|
|
231
|
+
$section.css({
|
|
232
|
+
opacity: supported ? '' : 0.45,
|
|
233
|
+
pointerEvents: supported ? '' : 'none'
|
|
234
|
+
});
|
|
235
|
+
$section.find('input, select, button, textarea').prop('disabled', !supported);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const list = $('#node-input-mcpServers-list');
|
|
239
|
+
list.editableList({
|
|
240
|
+
addButton: 'Add MCP Server',
|
|
241
|
+
height: 150,
|
|
242
|
+
addItem: function (container, index, data) {
|
|
243
|
+
mcpServerRow(container, data);
|
|
244
|
+
},
|
|
245
|
+
removable: true,
|
|
246
|
+
sortable: true
|
|
247
|
+
});
|
|
248
|
+
(node.mcpServers || []).forEach((entry) => list.editableList('addItem', entry));
|
|
249
|
+
|
|
250
|
+
const domainsList = $('#node-input-srtAllowedDomains-list');
|
|
251
|
+
domainsList.editableList({
|
|
252
|
+
addButton: 'Add domain',
|
|
253
|
+
height: 100,
|
|
254
|
+
addItem: function (container, index, value) {
|
|
255
|
+
simpleStringRow(container, value);
|
|
256
|
+
},
|
|
257
|
+
removable: true,
|
|
258
|
+
sortable: true
|
|
259
|
+
});
|
|
260
|
+
(node.srtAllowedDomains || []).forEach((entry) => domainsList.editableList('addItem', entry));
|
|
261
|
+
|
|
262
|
+
const writeDirsList = $('#node-input-srtAllowedWriteDirs-list');
|
|
263
|
+
writeDirsList.editableList({
|
|
264
|
+
addButton: 'Add directory',
|
|
265
|
+
height: 100,
|
|
266
|
+
addItem: function (container, index, value) {
|
|
267
|
+
simpleStringRow(container, value);
|
|
268
|
+
},
|
|
269
|
+
removable: true,
|
|
270
|
+
sortable: true
|
|
271
|
+
});
|
|
272
|
+
(node.srtAllowedWriteDirs || []).forEach((entry) => writeDirsList.editableList('addItem', entry));
|
|
273
|
+
|
|
274
|
+
function updateAgentRows() {
|
|
275
|
+
const agent = $('#node-input-agent').val();
|
|
276
|
+
$('#agent-auto-hint').attr('title', AUTO_HINTS[agent] || '');
|
|
277
|
+
const mcpSupported = agent !== 'pi';
|
|
278
|
+
setSectionSupported($('.agent-row-mcp'), mcpSupported);
|
|
279
|
+
$('.agent-row-mcp-hint').toggle(!mcpSupported);
|
|
280
|
+
const sessionSupported = agent !== 'pi';
|
|
281
|
+
setSectionSupported($('.agent-row-session'), sessionSupported);
|
|
282
|
+
$('.agent-row-session-hint').toggle(!sessionSupported);
|
|
283
|
+
updateInvocationRows();
|
|
284
|
+
}
|
|
285
|
+
$('#node-input-agent').on('change', updateAgentRows);
|
|
286
|
+
updateAgentRows();
|
|
287
|
+
updateRuntimeRows();
|
|
288
|
+
updateSrtModeRows();
|
|
289
|
+
},
|
|
290
|
+
oneditsave: function () {
|
|
291
|
+
const list = $('#node-input-mcpServers-list');
|
|
292
|
+
const items = list.editableList('items');
|
|
293
|
+
const out = [];
|
|
294
|
+
items.each(function () {
|
|
295
|
+
out.push($(this).data('mcpGetValue')());
|
|
296
|
+
});
|
|
297
|
+
this.mcpServers = out;
|
|
298
|
+
|
|
299
|
+
function collectStrings(listEl) {
|
|
300
|
+
const values = [];
|
|
301
|
+
listEl.editableList('items').each(function () {
|
|
302
|
+
const v = $(this).data('getValue')();
|
|
303
|
+
if (v) values.push(v);
|
|
304
|
+
});
|
|
305
|
+
return values;
|
|
306
|
+
}
|
|
307
|
+
this.srtAllowedDomains = collectStrings($('#node-input-srtAllowedDomains-list'));
|
|
308
|
+
this.srtAllowedWriteDirs = collectStrings($('#node-input-srtAllowedWriteDirs-list'));
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}());
|
|
312
|
+
</script>
|
|
313
|
+
|
|
314
|
+
<script type="text/html" data-template-name="agent">
|
|
315
|
+
<div class="form-row">
|
|
316
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
317
|
+
<input type="text" id="node-input-name" placeholder="Agent">
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<div class="form-row">
|
|
321
|
+
<label for="node-input-agent"><i class="fa fa-cube"></i> Agent</label>
|
|
322
|
+
<select id="node-input-agent" style="width:70%">
|
|
323
|
+
<option value="opencode">OpenCode</option>
|
|
324
|
+
<option value="pi">Pi</option>
|
|
325
|
+
</select>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<div class="form-row">
|
|
329
|
+
<label for="node-input-runtime"><i class="fa fa-cogs"></i> Runtime</label>
|
|
330
|
+
<select id="node-input-runtime" style="width:70%">
|
|
331
|
+
<option value="direct">Direct</option>
|
|
332
|
+
<option value="srt">SRT (sandboxed)</option>
|
|
333
|
+
</select>
|
|
334
|
+
</div>
|
|
335
|
+
<div class="form-row agent-row-srt">
|
|
336
|
+
<label for="node-input-srtSettingsMode"><i class="fa fa-shield"></i> SRT settings</label>
|
|
337
|
+
<select id="node-input-srtSettingsMode" style="width:60%">
|
|
338
|
+
<option value="file">File path</option>
|
|
339
|
+
<option value="inline">Inline</option>
|
|
340
|
+
</select>
|
|
341
|
+
</div>
|
|
342
|
+
<div class="form-row agent-row-srt agent-row-srt-file">
|
|
343
|
+
<label for="node-input-srtSettingsPath"> </label>
|
|
344
|
+
<input type="text" id="node-input-srtSettingsPath" placeholder="~/.srt-settings.json (srt's own default)" style="width:60%">
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<div class="form-row agent-row-srt agent-row-srt-inline">
|
|
348
|
+
<label>
|
|
349
|
+
<i class="fa fa-globe"></i> Allowed domains
|
|
350
|
+
<i class="fa fa-info-circle" style="color:#888; cursor:help; margin-left:4px;"
|
|
351
|
+
title="e.g. api.githubcopilot.com, opencode.ai -- the domain your configured model's API lives on."></i>
|
|
352
|
+
</label>
|
|
353
|
+
<ol id="node-input-srtAllowedDomains-list"></ol>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<div class="form-row agent-row-srt agent-row-srt-inline">
|
|
357
|
+
<label><i class="fa fa-folder"></i> Allowed write dirs</label>
|
|
358
|
+
<ol id="node-input-srtAllowedWriteDirs-list"></ol>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<div class="form-row agent-row-srt agent-row-srt-inline">
|
|
362
|
+
<input type="checkbox" id="node-input-srtStrictAllowlist" style="display:inline-block; width:auto; vertical-align:top;">
|
|
363
|
+
<label for="node-input-srtStrictAllowlist" style="width:auto;">Strict allowlist</label>
|
|
364
|
+
<i class="fa fa-info-circle" style="color:#888; cursor:help; margin-left:4px;"
|
|
365
|
+
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>
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
<div class="form-row agent-row-srt agent-row-srt-inline">
|
|
369
|
+
<details>
|
|
370
|
+
<summary style="cursor:pointer;">Advanced (raw JSON)</summary>
|
|
371
|
+
<textarea id="node-input-srtAdvancedJson" rows="4" style="width:100%; font-family:monospace;"
|
|
372
|
+
placeholder='If set, replaces the domains/directories above entirely, e.g. {"network":{"allowedDomains":[],"deniedDomains":[]},"filesystem":{"allowWrite":[],"denyRead":[],"denyWrite":[]}}'></textarea>
|
|
373
|
+
</details>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<div class="form-row agent-row-srt">
|
|
377
|
+
<label for="node-input-srtBinary"><i class="fa fa-terminal"></i> SRT binary</label>
|
|
378
|
+
<input type="text" id="node-input-srtBinary" placeholder="srt" style="width:60%">
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<div class="form-row">
|
|
382
|
+
<label for="node-input-invocation"><i class="fa fa-play"></i> Invocation</label>
|
|
383
|
+
<select id="node-input-invocation" style="width:70%">
|
|
384
|
+
<option value="prompt">Prompt</option>
|
|
385
|
+
<option value="skill">Skill</option>
|
|
386
|
+
<option value="command">Command / Template</option>
|
|
387
|
+
</select>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<div class="form-row agent-row-session">
|
|
391
|
+
<label for="node-input-sessionIdProp"><i class="fa fa-key"></i> Session ID
|
|
392
|
+
<i id="agent-session-id-hint" class="fa fa-info-circle" style="color:#888; cursor:help; margin-left:4px;"></i>
|
|
393
|
+
</label>
|
|
394
|
+
<input type="text" id="node-input-sessionIdProp" style="width:60%">
|
|
395
|
+
<input type="hidden" id="node-input-sessionIdPropType">
|
|
396
|
+
</div>
|
|
397
|
+
<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>
|
|
398
|
+
|
|
399
|
+
<div class="form-row agent-row-prompt">
|
|
400
|
+
<label for="node-input-prompt"><i class="fa fa-comment"></i> Prompt</label>
|
|
401
|
+
<input type="text" id="node-input-prompt" placeholder="leave blank to use msg.payload" style="width:60%">
|
|
402
|
+
<input type="hidden" id="node-input-promptType">
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<div class="form-row agent-row-name">
|
|
406
|
+
<label for="node-input-invocationName"><i class="fa fa-magic"></i> <span id="agent-name-label-text">Command</span></label>
|
|
407
|
+
<input type="text" id="node-input-invocationName" style="width:60%">
|
|
408
|
+
<input type="hidden" id="node-input-invocationNameType">
|
|
409
|
+
</div>
|
|
410
|
+
<div class="form-row agent-row-arguments">
|
|
411
|
+
<label for="node-input-arguments"><i class="fa fa-list"></i> Arguments</label>
|
|
412
|
+
<input type="text" id="node-input-arguments" placeholder="leave blank to use msg.payload" style="width:60%">
|
|
413
|
+
<input type="hidden" id="node-input-argumentsType">
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<div class="form-row">
|
|
417
|
+
<label for="node-input-model"><i class="fa fa-microchip"></i> Model</label>
|
|
418
|
+
<input type="text" id="node-input-model" placeholder="leave blank for agent default" style="width:60%">
|
|
419
|
+
<input type="hidden" id="node-input-modelType">
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<div class="form-row">
|
|
423
|
+
<label for="node-input-cwd"><i class="fa fa-folder-open"></i> Working directory</label>
|
|
424
|
+
<input type="text" id="node-input-cwd" placeholder="leave blank for Node-RED's own working directory" style="width:60%">
|
|
425
|
+
<input type="hidden" id="node-input-cwdType">
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
<div class="form-row">
|
|
429
|
+
<input type="checkbox" id="node-input-auto" style="display:inline-block; width:auto; vertical-align:top;">
|
|
430
|
+
<label for="node-input-auto" style="width:auto;">Auto approve (dangerous)</label>
|
|
431
|
+
<i id="agent-auto-hint" class="fa fa-info-circle" style="color:#888; cursor:help; margin-left:4px;"></i>
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
<div class="form-row">
|
|
435
|
+
<label for="node-input-timeout"><i class="fa fa-clock-o"></i> Timeout</label>
|
|
436
|
+
<input type="text" id="node-input-timeout" placeholder="seconds, leave empty for no timeout" style="width:60%">
|
|
437
|
+
<input type="hidden" id="node-input-timeoutType">
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
<div class="form-row">
|
|
441
|
+
<label for="node-input-concurrency"><i class="fa fa-tasks"></i> Concurrency</label>
|
|
442
|
+
<input type="text" id="node-input-concurrency" placeholder="1" style="width:80px">
|
|
443
|
+
<i class="fa fa-info-circle" style="color:#888; cursor:help; margin-left:4px;"
|
|
444
|
+
title="Maximum parallel executions for this node. Additional incoming messages queue (FIFO) until a slot frees up. 1 = sequential."></i>
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
<div class="form-row agent-row-mcp">
|
|
448
|
+
<label><i class="fa fa-plug"></i> MCP Servers</label>
|
|
449
|
+
<ol id="node-input-mcpServers-list"></ol>
|
|
450
|
+
</div>
|
|
451
|
+
<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>
|
|
452
|
+
</script>
|
|
453
|
+
|
|
454
|
+
<script type="text/html" data-help-name="agent">
|
|
455
|
+
<p>Runs a coding agent (<b>OpenCode</b> or <b>Pi</b>) non-interactively,
|
|
456
|
+
either directly on this host or sandboxed through <code>srt</code>.</p>
|
|
457
|
+
<h3>Inputs</h3>
|
|
458
|
+
<dl class="message-properties">
|
|
459
|
+
<dt class="optional">payload <span class="property-type">string</span></dt>
|
|
460
|
+
<dd>Used as the prompt/arguments if the corresponding field is left at
|
|
461
|
+
its default (<code>msg.payload</code>).</dd>
|
|
462
|
+
<dt class="optional">cwd <span class="property-type">string</span></dt>
|
|
463
|
+
<dd>Used as the working directory if the Working directory field is
|
|
464
|
+
left at its default (<code>msg.cwd</code>).</dd>
|
|
465
|
+
<dt class="optional">sessionID <span class="property-type">string</span></dt>
|
|
466
|
+
<dd>Used to resume a previous session if the Session ID field is left
|
|
467
|
+
at its default (<code>msg.sessionID</code>). Empty/absent starts a
|
|
468
|
+
new session. OpenCode only.</dd>
|
|
469
|
+
<dt class="optional">operation <span class="property-type">string</span></dt>
|
|
470
|
+
<dd>Set to <code>"terminate"</code> to kill an in-flight (or still-
|
|
471
|
+
queued) execution of this node instance instead of starting a new
|
|
472
|
+
one -- see <code>executionId</code> below.</dd>
|
|
473
|
+
<dt class="optional">executionId <span class="property-type">string</span></dt>
|
|
474
|
+
<dd>Required when <code>operation</code> is <code>"terminate"</code>:
|
|
475
|
+
the id of the execution to kill, as returned by an earlier run in
|
|
476
|
+
<code>msg.agentExecution.id</code> or an Events message's
|
|
477
|
+
<code>executionId</code>.</dd>
|
|
478
|
+
</dl>
|
|
479
|
+
<h3>Outputs</h3>
|
|
480
|
+
<ol class="node-ports">
|
|
481
|
+
<li>Result
|
|
482
|
+
<dl class="message-properties">
|
|
483
|
+
<dt>payload <span class="property-type">string</span></dt>
|
|
484
|
+
<dd>The agent's final text reply.</dd>
|
|
485
|
+
<dt>sessionID <span class="property-type">string</span></dt>
|
|
486
|
+
<dd>The session this execution ran in (new or resumed). Feed
|
|
487
|
+
this back into the Session ID field to continue the
|
|
488
|
+
conversation on the next message.</dd>
|
|
489
|
+
<dt>agentExecution <span class="property-type">object</span></dt>
|
|
490
|
+
<dd>id, status, exitCode, signal, timedOut, durationMs, sessionID.</dd>
|
|
491
|
+
</dl>
|
|
492
|
+
</li>
|
|
493
|
+
<li>Events
|
|
494
|
+
<dl class="message-properties">
|
|
495
|
+
<dt>payload <span class="property-type">object</span></dt>
|
|
496
|
+
<dd>One structured event per message as the agent runs
|
|
497
|
+
(queued/started/tool/agent/progress/completed/failed/timeout/cancelled).</dd>
|
|
498
|
+
<dt>executionId <span class="property-type">string</span></dt>
|
|
499
|
+
<dd>Correlates events (and the eventual result) belonging to
|
|
500
|
+
the same execution when several run concurrently.</dd>
|
|
501
|
+
</dl>
|
|
502
|
+
</li>
|
|
503
|
+
</ol>
|
|
504
|
+
<p><b>Concurrency</b> bounds how many executions this node runs at once
|
|
505
|
+
(default 1 = sequential). Extra incoming messages wait in an internal
|
|
506
|
+
FIFO queue -- each waiting message emits a <code>queued</code> event
|
|
507
|
+
immediately, then <code>started</code> once a slot frees up. The node
|
|
508
|
+
status shows e.g. <code>2 running · 3 queued</code>. Results may
|
|
509
|
+
complete in a different order than the messages arrived; ordering is
|
|
510
|
+
not guaranteed above concurrency 1. On redeploy/removal, queued
|
|
511
|
+
messages are cancelled (a <code>cancelled</code> event plus a Node-RED
|
|
512
|
+
error) and any still-running agent processes are terminated.</p>
|
|
513
|
+
<p><b>Terminating a specific execution on demand</b>: send a message with
|
|
514
|
+
<code>msg.operation = "terminate"</code> and <code>msg.executionId</code>
|
|
515
|
+
set to the id to kill (from a prior run's <code>msg.agentExecution.id</code>
|
|
516
|
+
or an Events message's <code>executionId</code>). A still-queued
|
|
517
|
+
execution is cancelled immediately (never spawned); an active one gets
|
|
518
|
+
SIGTERM to its whole process group, escalating to SIGKILL after a grace
|
|
519
|
+
period -- this works the same way for both the Direct and SRT runtimes.
|
|
520
|
+
The terminated execution's own original trigger still completes on its
|
|
521
|
+
own (status <code>failed</code>, with the kill signal) once the process
|
|
522
|
+
actually exits; this terminate trigger's own output is only the
|
|
523
|
+
immediate "kill requested" acknowledgement.</p>
|
|
524
|
+
<p><b>Auto approve</b> maps to OpenCode's <code>--auto</code> (off by
|
|
525
|
+
default -- without it, any action gated by a permission policy not
|
|
526
|
+
already set to "allow" is auto-rejected, but the run still completes).
|
|
527
|
+
Pi has no equivalent permission-prompt system to bypass in
|
|
528
|
+
non-interactive mode -- for Pi this instead restricts the available
|
|
529
|
+
tools to read-only (<code>read/grep/find/ls</code>) when off, and
|
|
530
|
+
allows everything (including <code>bash/edit/write</code>) when on.</p>
|
|
531
|
+
<p><b>Skill</b>/<b>Command</b> invocation: for OpenCode this is the name
|
|
532
|
+
of a discovered skill or a <code>.opencode/command(s)/<name>.md</code>
|
|
533
|
+
file, dispatched deterministically via <code>--command</code>. Pi has
|
|
534
|
+
no equivalent dispatch mechanism -- the name is resolved to a file
|
|
535
|
+
under <code>.github/skills/</code> (skill) or
|
|
536
|
+
<code>.github/prompts/</code> (command), passed via
|
|
537
|
+
<code>--skill</code>/<code>--prompt-template</code>, and the model is
|
|
538
|
+
explicitly told in the prompt to use it. Pi also does not support
|
|
539
|
+
configuring MCP servers through this node.</p>
|
|
540
|
+
<p><b>Session ID</b>: leave blank to always start a fresh session (the
|
|
541
|
+
default). Set it (typically from the previous result's
|
|
542
|
+
<code>msg.sessionID</code>) to continue that conversation instead --
|
|
543
|
+
maps to OpenCode's <code>opencode run --session <id> ...</code>.
|
|
544
|
+
Only the OpenCode agent supports this; Pi rejects a non-empty Session
|
|
545
|
+
ID (every Pi run uses <code>--no-session</code>).</p>
|
|
546
|
+
<p><b>SRT</b> runtime shells out to the <code>srt</code> sandbox-runtime
|
|
547
|
+
CLI already installed on this host; its network/filesystem policy is
|
|
548
|
+
configured via its own settings file (default <code>~/.srt-settings.json</code>),
|
|
549
|
+
not through this node.</p>
|
|
550
|
+
</script>
|