@aion0/forge 0.5.20 → 0.5.22

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.
Files changed (40) hide show
  1. package/.forge/agent-context.json +1 -1
  2. package/RELEASE_NOTES.md +32 -6
  3. package/app/api/code/route.ts +10 -4
  4. package/app/api/plugins/route.ts +75 -0
  5. package/components/Dashboard.tsx +1 -0
  6. package/components/PipelineEditor.tsx +135 -9
  7. package/components/PluginsPanel.tsx +472 -0
  8. package/components/ProjectDetail.tsx +36 -98
  9. package/components/SessionView.tsx +4 -4
  10. package/components/SettingsModal.tsx +160 -66
  11. package/components/SkillsPanel.tsx +14 -5
  12. package/components/TerminalLauncher.tsx +398 -0
  13. package/components/WebTerminal.tsx +84 -84
  14. package/components/WorkspaceView.tsx +371 -87
  15. package/lib/agents/index.ts +7 -4
  16. package/lib/builtin-plugins/docker.yaml +70 -0
  17. package/lib/builtin-plugins/http.yaml +66 -0
  18. package/lib/builtin-plugins/jenkins.yaml +92 -0
  19. package/lib/builtin-plugins/llm-vision.yaml +85 -0
  20. package/lib/builtin-plugins/playwright.yaml +111 -0
  21. package/lib/builtin-plugins/shell-command.yaml +60 -0
  22. package/lib/builtin-plugins/slack.yaml +48 -0
  23. package/lib/builtin-plugins/webhook.yaml +56 -0
  24. package/lib/forge-mcp-server.ts +116 -2
  25. package/lib/pipeline.ts +62 -5
  26. package/lib/plugins/executor.ts +347 -0
  27. package/lib/plugins/registry.ts +228 -0
  28. package/lib/plugins/types.ts +103 -0
  29. package/lib/project-sessions.ts +7 -2
  30. package/lib/session-utils.ts +7 -3
  31. package/lib/terminal-standalone.ts +6 -34
  32. package/lib/workspace/agent-worker.ts +1 -1
  33. package/lib/workspace/orchestrator.ts +414 -136
  34. package/lib/workspace/presets.ts +5 -3
  35. package/lib/workspace/session-monitor.ts +14 -10
  36. package/lib/workspace/types.ts +3 -1
  37. package/lib/workspace-standalone.ts +38 -21
  38. package/next-env.d.ts +1 -1
  39. package/package.json +1 -1
  40. package/qa/.forge/agent-context.json +1 -1
@@ -3,4 +3,4 @@
3
3
  "agentId": "engineer-1774920478256",
4
4
  "agentLabel": "Engineer",
5
5
  "forgePort": 8403
6
- }
6
+ }
package/RELEASE_NOTES.md CHANGED
@@ -1,12 +1,38 @@
1
- # Forge v0.5.20
1
+ # Forge v0.5.22
2
2
 
3
- Released: 2026-04-01
3
+ Released: 2026-04-03
4
4
 
5
- ## Changes since v0.5.19
5
+ ## Changes since v0.5.21
6
+
7
+ ### Features
8
+ - feat: workspace model config, headless mode for non-claude agents
9
+ - feat: unified terminal picker, model resolution, VibeCoding agent fix
10
+ - feat: smith 'starting' state, terminal picker, boundSessionId preservation
11
+ - feat: playwright plugin supports headed mode (show browser window)
12
+ - feat: QA preset auto-creates playwright config, test dir, and starts dev server
13
+ - feat: Plugin system enhancements — instances, MCP tools, agent integration
14
+ - feat: Pipeline supports plugin nodes (mode: plugin)
15
+ - feat: Plugin system — types, registry, executor, built-in plugins, API
6
16
 
7
17
  ### Bug Fixes
8
- - fix: hook done always sets done, no state check
9
- - fix: hook done accepts idle→done, not just running→done
18
+ - fix: reduce verbose agent-to-agent notifications
19
+ - fix: plugin config defaults and UI improvements
20
+ - fix: settings agent config save, cliType unification, add form improvements
21
+ - fix: smith restart race condition and session binding improvements
22
+ - fix: terminal-standalone cleanup and correctness fixes
23
+ - fix: workspace-standalone and session-monitor correctness/perf fixes
24
+ - fix: plugin shell executor hardened against child process crashes
25
+ - fix: plugin shell executor uses async exec instead of execSync
26
+ - fix: QA preset uses bash commands as primary, MCP tools as optional
27
+ - fix: playwright plugin uses mode-prefixed actions to avoid shell multiline issues
28
+ - fix: plugin instance config saves schema defaults when user doesn't modify fields
29
+ - fix: playwright check_url falls back to config.base_url when params.url empty
30
+ - fix: plugin instance form uses proper input types (select/boolean/number)
31
+ - fix: plugin system bug fixes from code review
32
+ - fix: plugin executor handles empty cwd + builtin dir fallback to source
33
+
34
+ ### Refactoring
35
+ - refactor: orchestrator perf + correctness improvements
10
36
 
11
37
 
12
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.19...v0.5.20
38
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.21...v0.5.22
@@ -96,10 +96,16 @@ export async function GET(req: Request) {
96
96
  const { execSync } = require('node:child_process');
97
97
  const safeQuery = searchQuery.replace(/['"\\]/g, '\\$&');
98
98
  // Use grep -rn with limits to prevent huge output
99
- const result = execSync(
100
- `grep -rn --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.py' --include='*.java' --include='*.go' --include='*.rs' --include='*.md' --include='*.json' --include='*.yaml' --include='*.yml' --include='*.css' --include='*.html' --include='*.vue' --include='*.svelte' -m 5 '${safeQuery}' . 2>/dev/null | head -100`,
101
- { cwd: resolvedDir, encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] }
102
- ).trim();
99
+ let result = '';
100
+ try {
101
+ result = execSync(
102
+ `grep -rn --exclude-dir=node_modules --exclude-dir=.next --exclude-dir=.git --exclude-dir=dist --exclude-dir=build --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.py' --include='*.java' --include='*.go' --include='*.rs' --include='*.md' --include='*.json' --include='*.yaml' --include='*.yml' --include='*.css' --include='*.html' --include='*.vue' --include='*.svelte' -m 5 '${safeQuery}' . | head -100`,
103
+ { cwd: resolvedDir, encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] }
104
+ ).trim();
105
+ } catch (e: any) {
106
+ // grep exit code 1 = no match (not an error)
107
+ result = e.stdout?.trim() || '';
108
+ }
103
109
  const matches = result ? result.split('\n').map((line: string) => {
104
110
  const match = line.match(/^\.\/(.+?):(\d+):(.*)$/);
105
111
  if (!match) return null;
@@ -0,0 +1,75 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { listPlugins, getPlugin, installPlugin, uninstallPlugin, updatePluginConfig, listInstalledPlugins, getInstalledPlugin } from '@/lib/plugins/registry';
3
+ import { executePluginAction } from '@/lib/plugins/executor';
4
+
5
+ // GET: list plugins or get plugin details
6
+ export async function GET(req: Request) {
7
+ const url = new URL(req.url);
8
+ const id = url.searchParams.get('id');
9
+ const installed = url.searchParams.get('installed');
10
+
11
+ if (id) {
12
+ // Try as plugin definition first, then as installed instance
13
+ const plugin = getPlugin(id);
14
+ const inst = getInstalledPlugin(id);
15
+ if (!plugin && !inst) return NextResponse.json({ error: 'Plugin not found' }, { status: 404 });
16
+ return NextResponse.json({
17
+ plugin: inst?.definition || plugin,
18
+ installed: !!inst,
19
+ config: inst?.config,
20
+ instanceName: inst?.instanceName,
21
+ source: inst?.source,
22
+ });
23
+ }
24
+
25
+ if (installed === 'true') {
26
+ return NextResponse.json({ plugins: listInstalledPlugins() });
27
+ }
28
+
29
+ return NextResponse.json({ plugins: listPlugins() });
30
+ }
31
+
32
+ // POST: install, uninstall, update config, or test a plugin action
33
+ export async function POST(req: Request) {
34
+ const body = await req.json();
35
+ const { action, id, config, actionName, params } = body;
36
+
37
+ switch (action) {
38
+ case 'install': {
39
+ if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 });
40
+ const ok = installPlugin(id, config || {}, body.source ? { source: body.source, name: body.name } : undefined);
41
+ return NextResponse.json({ ok });
42
+ }
43
+ case 'create_instance': {
44
+ const { source, name, instanceId } = body;
45
+ if (!source || !name) return NextResponse.json({ error: 'source and name required' }, { status: 400 });
46
+ const iid = instanceId || name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
47
+ // Check for duplicate ID
48
+ const existing = getInstalledPlugin(iid);
49
+ if (existing) {
50
+ return NextResponse.json({ error: `Instance ID "${iid}" already exists (used by ${existing.instanceName || existing.definition.name}). Choose a different name.` }, { status: 409 });
51
+ }
52
+ const ok = installPlugin(iid, config || {}, { source, name });
53
+ return NextResponse.json({ ok, instanceId: iid });
54
+ }
55
+ case 'uninstall': {
56
+ if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 });
57
+ const ok = uninstallPlugin(id);
58
+ return NextResponse.json({ ok });
59
+ }
60
+ case 'update_config': {
61
+ if (!id || !config) return NextResponse.json({ error: 'id and config required' }, { status: 400 });
62
+ const ok = updatePluginConfig(id, config);
63
+ return NextResponse.json({ ok });
64
+ }
65
+ case 'test': {
66
+ if (!id || !actionName) return NextResponse.json({ error: 'id and actionName required' }, { status: 400 });
67
+ const inst = getInstalledPlugin(id);
68
+ if (!inst) return NextResponse.json({ error: 'Plugin not installed' }, { status: 400 });
69
+ const result = await executePluginAction(inst, actionName, params || {});
70
+ return NextResponse.json(result);
71
+ }
72
+ default:
73
+ return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
74
+ }
75
+ }
@@ -710,6 +710,7 @@ export default function Dashboard({ user }: { user: any }) {
710
710
  </Suspense>
711
711
  )}
712
712
 
713
+
713
714
  {/* Usage */}
714
715
  {viewMode === 'usage' && (
715
716
  <Suspense fallback={<div className="flex-1 flex items-center justify-center text-[var(--text-secondary)]">Loading...</div>}>
@@ -37,8 +37,10 @@ function PipelineNode({ id, data }: NodeProps<Node<NodeData>>) {
37
37
 
38
38
  <div className="px-3 py-2 border-b border-[#3a3a5a] flex items-center gap-2">
39
39
  <span className={`text-[8px] px-1 py-0.5 rounded font-medium ${
40
- (data as any).mode === 'shell' ? 'bg-yellow-500/20 text-yellow-400' : 'bg-purple-500/20 text-purple-400'
41
- }`}>{(data as any).mode === 'shell' ? 'shell' : ((data as any).agent || 'default')}</span>
40
+ (data as any).mode === 'shell' ? 'bg-yellow-500/20 text-yellow-400' :
41
+ (data as any).mode === 'plugin' ? 'bg-green-500/20 text-green-400' :
42
+ 'bg-purple-500/20 text-purple-400'
43
+ }`}>{(data as any).mode === 'plugin' ? `🔌 ${(data as any).plugin || 'plugin'}` : (data as any).mode === 'shell' ? 'shell' : ((data as any).agent || 'default')}</span>
42
44
  <span className="text-xs font-semibold text-white">{data.label}</span>
43
45
  <div className="ml-auto flex gap-1">
44
46
  <button onClick={() => data.onEdit(id)} className="text-[9px] text-[var(--accent)] hover:text-white">edit</button>
@@ -66,10 +68,10 @@ const nodeTypes = { pipeline: PipelineNode };
66
68
  // ─── Node Edit Modal ──────────────────────────────────────
67
69
 
68
70
  function NodeEditModal({ node, projects, agents, onSave, onClose }: {
69
- node: { id: string; project: string; prompt: string; agent?: string; mode?: string; outputs: { name: string; extract: string }[] };
71
+ node: { id: string; project: string; prompt: string; agent?: string; mode?: string; plugin?: string; pluginAction?: string; pluginParams?: Record<string, any>; pluginWait?: boolean; outputs: { name: string; extract: string }[] };
70
72
  projects: { name: string; root: string }[];
71
73
  agents: { id: string; name: string }[];
72
- onSave: (data: { id: string; project: string; prompt: string; agent?: string; mode?: string; outputs: { name: string; extract: string }[] }) => void;
74
+ onSave: (data: any) => void;
73
75
  onClose: () => void;
74
76
  }) {
75
77
  const [id, setId] = useState(node.id);
@@ -77,6 +79,32 @@ function NodeEditModal({ node, projects, agents, onSave, onClose }: {
77
79
  const [prompt, setPrompt] = useState(node.prompt);
78
80
  const [agent, setAgent] = useState(node.agent || '');
79
81
  const [mode, setMode] = useState(node.mode || 'claude');
82
+ // Plugin fields
83
+ const [pluginId, setPluginId] = useState(node.plugin || '');
84
+ const [pluginAction, setPluginAction] = useState(node.pluginAction || '');
85
+ const [pluginParams, setPluginParams] = useState(JSON.stringify(node.pluginParams || {}, null, 2));
86
+ const [pluginWait, setPluginWait] = useState(node.pluginWait || false);
87
+ const [availablePlugins, setAvailablePlugins] = useState<{ id: string; name: string; icon: string; installed: boolean; actions?: Record<string, any> }[]>([]);
88
+ const [selectedPluginDef, setSelectedPluginDef] = useState<any>(null);
89
+
90
+ // Fetch installed plugins
91
+ useEffect(() => {
92
+ fetch('/api/plugins?installed=true')
93
+ .then(r => r.json())
94
+ .then(d => {
95
+ const plugins = (d.plugins || []).map((p: any) => ({
96
+ id: p.id, name: p.definition?.name || p.id, icon: p.definition?.icon || '🔌',
97
+ installed: true, actions: p.definition?.actions || {},
98
+ params: p.definition?.params || {},
99
+ }));
100
+ setAvailablePlugins(plugins);
101
+ if (pluginId) {
102
+ const sel = plugins.find((p: any) => p.id === pluginId);
103
+ if (sel) setSelectedPluginDef(sel);
104
+ }
105
+ })
106
+ .catch(() => {});
107
+ }, []);
80
108
  const [outputs, setOutputs] = useState(node.outputs);
81
109
 
82
110
  return (
@@ -116,11 +144,12 @@ function NodeEditModal({ node, projects, agents, onSave, onClose }: {
116
144
  <label className="text-[10px] text-gray-400 block mb-1">Mode</label>
117
145
  <select
118
146
  value={mode}
119
- onChange={e => setMode(e.target.value)}
147
+ onChange={e => { setMode(e.target.value); if (e.target.value !== 'plugin') { setPluginId(''); setSelectedPluginDef(null); } }}
120
148
  className="w-full text-xs bg-[#12122a] border border-[#3a3a5a] rounded px-2 py-1.5 text-white"
121
149
  >
122
150
  <option value="claude">Agent</option>
123
151
  <option value="shell">Shell</option>
152
+ <option value="plugin">Plugin</option>
124
153
  </select>
125
154
  </div>
126
155
  {mode !== 'shell' && (
@@ -139,8 +168,71 @@ function NodeEditModal({ node, projects, agents, onSave, onClose }: {
139
168
  </div>
140
169
  )}
141
170
  </div>
171
+ {/* Plugin config */}
172
+ {mode === 'plugin' && (
173
+ <div className="space-y-2 p-2 bg-[#12122a] rounded border border-[#3a3a5a]">
174
+ <div>
175
+ <label className="text-[10px] text-gray-400 block mb-1">Plugin</label>
176
+ <select
177
+ value={pluginId}
178
+ onChange={e => {
179
+ setPluginId(e.target.value);
180
+ const sel = availablePlugins.find(p => p.id === e.target.value);
181
+ setSelectedPluginDef(sel || null);
182
+ setPluginAction('');
183
+ }}
184
+ className="w-full text-xs bg-[#1e1e3a] border border-[#3a3a5a] rounded px-2 py-1.5 text-white"
185
+ >
186
+ <option value="">Select plugin...</option>
187
+ {availablePlugins.map(p => (
188
+ <option key={p.id} value={p.id}>{p.icon} {p.name}</option>
189
+ ))}
190
+ </select>
191
+ {availablePlugins.length === 0 && (
192
+ <div className="text-[9px] text-yellow-400 mt-1">No plugins installed. Install from Settings → Plugins.</div>
193
+ )}
194
+ </div>
195
+ {selectedPluginDef && (
196
+ <>
197
+ <div>
198
+ <label className="text-[10px] text-gray-400 block mb-1">Action</label>
199
+ <select
200
+ value={pluginAction}
201
+ onChange={e => setPluginAction(e.target.value)}
202
+ className="w-full text-xs bg-[#1e1e3a] border border-[#3a3a5a] rounded px-2 py-1.5 text-white"
203
+ >
204
+ <option value="">Default</option>
205
+ {Object.keys(selectedPluginDef.actions || {}).map((a: string) => (
206
+ <option key={a} value={a}>{a}</option>
207
+ ))}
208
+ </select>
209
+ </div>
210
+ <div>
211
+ <label className="text-[10px] text-gray-400 block mb-1">Parameters (JSON)</label>
212
+ <textarea
213
+ value={pluginParams}
214
+ onChange={e => setPluginParams(e.target.value)}
215
+ rows={4}
216
+ className="w-full text-xs bg-[#1e1e3a] border border-[#3a3a5a] rounded px-2 py-1.5 text-white font-mono resize-y"
217
+ placeholder='{ "key": "value" }'
218
+ />
219
+ {selectedPluginDef.params && Object.keys(selectedPluginDef.params).length > 0 && (
220
+ <div className="text-[8px] text-gray-500 mt-0.5">
221
+ Available: {Object.entries(selectedPluginDef.params).map(([k, v]: [string, any]) => `${k}${v.required ? '*' : ''}`).join(', ')}
222
+ </div>
223
+ )}
224
+ </div>
225
+ <div className="flex items-center gap-2">
226
+ <input type="checkbox" id="pluginWait" checked={pluginWait} onChange={e => setPluginWait(e.target.checked)} className="accent-[var(--accent)]" />
227
+ <label htmlFor="pluginWait" className="text-[10px] text-gray-400">Wait for completion (poll)</label>
228
+ </div>
229
+ </>
230
+ )}
231
+ </div>
232
+ )}
233
+
142
234
  <div>
143
- <label className="text-[10px] text-gray-400 block mb-1">Prompt</label>
235
+ <label className="text-[10px] text-gray-400 block mb-1">{mode === 'plugin' ? 'Notes (optional)' : 'Prompt'}</label>
144
236
  <textarea
145
237
  value={prompt}
146
238
  onChange={e => setPrompt(e.target.value)}
@@ -181,7 +273,18 @@ function NodeEditModal({ node, projects, agents, onSave, onClose }: {
181
273
  <div className="px-4 py-3 border-t border-[#3a3a5a] flex gap-2 justify-end">
182
274
  <button onClick={onClose} className="text-xs px-3 py-1 text-gray-400 hover:text-white">Cancel</button>
183
275
  <button
184
- onClick={() => onSave({ id, project, prompt, agent: agent || undefined, mode, outputs: outputs.filter(o => o.name) })}
276
+ onClick={() => {
277
+ let parsedParams: Record<string, any> = {};
278
+ try { parsedParams = JSON.parse(pluginParams || '{}'); } catch {}
279
+ onSave({
280
+ id, project, prompt, agent: agent || undefined, mode,
281
+ plugin: mode === 'plugin' ? pluginId : undefined,
282
+ pluginAction: mode === 'plugin' ? pluginAction || undefined : undefined,
283
+ pluginParams: mode === 'plugin' ? parsedParams : undefined,
284
+ pluginWait: mode === 'plugin' ? pluginWait : undefined,
285
+ outputs: outputs.filter(o => o.name),
286
+ });
287
+ }}
185
288
  className="text-xs px-3 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90"
186
289
  >
187
290
  Save
@@ -201,7 +304,7 @@ export default function PipelineEditor({ onSave, onClose, initialYaml }: {
201
304
  }) {
202
305
  const [nodes, setNodes, onNodesChange] = useNodesState<Node<NodeData>>([]);
203
306
  const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
204
- const [editingNode, setEditingNode] = useState<{ id: string; project: string; prompt: string; agent?: string; mode?: string; outputs: { name: string; extract: string }[] } | null>(null);
307
+ const [editingNode, setEditingNode] = useState<{ id: string; project: string; prompt: string; agent?: string; mode?: string; plugin?: string; pluginAction?: string; pluginParams?: Record<string, any>; pluginWait?: boolean; outputs: { name: string; extract: string }[] } | null>(null);
205
308
  const [workflowName, setWorkflowName] = useState('');
206
309
  const [availableAgents, setAvailableAgents] = useState<{ id: string; name: string }[]>([]);
207
310
  const [workflowDesc, setWorkflowDesc] = useState('');
@@ -240,6 +343,12 @@ export default function PipelineEditor({ onSave, onClose, initialYaml }: {
240
343
  label: id,
241
344
  project: def.project || '',
242
345
  prompt: def.prompt || '',
346
+ agent: def.agent,
347
+ mode: def.mode || (def.plugin ? 'plugin' : undefined),
348
+ plugin: def.plugin,
349
+ pluginAction: def.plugin_action,
350
+ pluginParams: def.params,
351
+ pluginWait: def.wait,
243
352
  outputs: (def.outputs || []).map((o: any) => ({ name: o.name, extract: o.extract || 'result' })),
244
353
  onEdit: (nid: string) => handleEditNode(nid),
245
354
  onDelete: (nid: string) => handleDeleteNode(nid),
@@ -297,6 +406,12 @@ export default function PipelineEditor({ onSave, onClose, initialYaml }: {
297
406
  id: node.id,
298
407
  project: node.data.project,
299
408
  prompt: node.data.prompt,
409
+ agent: (node.data as any).agent,
410
+ mode: (node.data as any).mode,
411
+ plugin: (node.data as any).plugin,
412
+ pluginAction: (node.data as any).pluginAction,
413
+ pluginParams: (node.data as any).pluginParams,
414
+ pluginWait: (node.data as any).pluginWait,
300
415
  outputs: node.data.outputs,
301
416
  });
302
417
  }
@@ -309,7 +424,7 @@ export default function PipelineEditor({ onSave, onClose, initialYaml }: {
309
424
  setEdges(eds => eds.filter(e => e.source !== id && e.target !== id));
310
425
  }, [setNodes, setEdges]);
311
426
 
312
- const handleSaveNode = useCallback((data: { id: string; project: string; prompt: string; agent?: string; mode?: string; outputs: { name: string; extract: string }[] }) => {
427
+ const handleSaveNode = useCallback((data: { id: string; project: string; prompt: string; agent?: string; mode?: string; plugin?: string; pluginAction?: string; pluginParams?: Record<string, any>; pluginWait?: boolean; outputs: { name: string; extract: string }[] }) => {
313
428
  setNodes(nds => nds.map(n => {
314
429
  if (n.id === editingNode?.id) {
315
430
  return {
@@ -322,6 +437,10 @@ export default function PipelineEditor({ onSave, onClose, initialYaml }: {
322
437
  prompt: data.prompt,
323
438
  agent: data.agent,
324
439
  mode: data.mode,
440
+ plugin: data.plugin,
441
+ pluginAction: data.pluginAction,
442
+ pluginParams: data.pluginParams,
443
+ pluginWait: data.pluginWait,
325
444
  outputs: data.outputs,
326
445
  },
327
446
  };
@@ -356,6 +475,13 @@ export default function PipelineEditor({ onSave, onClose, initialYaml }: {
356
475
  prompt: node.data.prompt,
357
476
  };
358
477
  if ((node.data as any).mode === 'shell') nodeDef.mode = 'shell';
478
+ if ((node.data as any).mode === 'plugin') {
479
+ nodeDef.mode = 'plugin';
480
+ nodeDef.plugin = (node.data as any).plugin;
481
+ if ((node.data as any).pluginAction) nodeDef.plugin_action = (node.data as any).pluginAction;
482
+ if ((node.data as any).pluginParams && Object.keys((node.data as any).pluginParams).length > 0) nodeDef.params = (node.data as any).pluginParams;
483
+ if ((node.data as any).pluginWait) nodeDef.wait = true;
484
+ }
359
485
  if ((node.data as any).agent) nodeDef.agent = (node.data as any).agent;
360
486
  if (deps.length > 0) nodeDef.depends_on = deps;
361
487
  if (node.data.outputs.length > 0) nodeDef.outputs = node.data.outputs;