@harbinger-ai/harbinger 0.1.3 → 0.1.4
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/lib/bounty/actions.js +45 -0
- package/lib/chat/actions.js +338 -20
- package/lib/chat/components/agents-page.js +535 -162
- package/lib/chat/components/agents-page.jsx +528 -230
- package/lib/chat/components/icons.js +105 -0
- package/lib/chat/components/icons.jsx +129 -0
- package/lib/chat/components/page-layout.js +41 -2
- package/lib/chat/components/page-layout.jsx +40 -2
- package/lib/chat/components/settings-providers-page.js +647 -112
- package/lib/chat/components/settings-providers-page.jsx +641 -134
- package/lib/chat/components/targets-page.js +554 -96
- package/lib/chat/components/targets-page.jsx +464 -114
- package/package.json +1 -1
|
@@ -2,8 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
4
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
5
|
-
import { UsersIcon, RefreshIcon, SpinnerIcon, ChevronDownIcon, PlusIcon, PencilIcon, CheckIcon, XIcon, SearchIcon } from './icons.js';
|
|
6
|
-
import { getAgentProfilesWithStatus, getAgentProfile, updateAgentFile, createAgent, createAgentJob } from '../actions.js';
|
|
5
|
+
import { UsersIcon, RefreshIcon, SpinnerIcon, ChevronDownIcon, PlusIcon, PencilIcon, CheckIcon, XIcon, SearchIcon, CpuIcon, PackageIcon, DatabaseIcon, FolderIcon, ClockIcon, SlidersIcon, TagIcon, HashIcon, PlayIcon } from './icons.js';
|
|
6
|
+
import { getAgentProfilesWithStatus, getAgentProfile, updateAgentFile, createAgent, createAgentJob, getAvailableMcpTools, getLlmProviders } from '../actions.js';
|
|
7
|
+
|
|
8
|
+
const PROVIDER_MODELS = {
|
|
9
|
+
anthropic: ['claude-sonnet-4-20250514', 'claude-opus-4-20250514', 'claude-haiku-4-5-20251001'],
|
|
10
|
+
openai: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1', 'o1-mini', 'o3-mini'],
|
|
11
|
+
google: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash'],
|
|
12
|
+
custom: [],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const ROLE_OPTIONS = ['Recon', 'Exploitation', 'Reporting', 'Infrastructure', 'Research', 'Analysis', 'Development', 'Custom'];
|
|
16
|
+
const SPEC_OPTIONS = ['Web', 'API', 'Mobile', 'Cloud', 'Network', 'Blockchain', 'IoT', 'Social Engineering', 'Custom'];
|
|
17
|
+
const RUN_MODES = ['manual', 'scheduled', 'continuous', 'event-triggered'];
|
|
18
|
+
const SOUL_TEMPLATES = {
|
|
19
|
+
recon: `# {{CODENAME}}\n\nYou are {{NAME}}, an autonomous reconnaissance specialist.\n\n## Mission\nDiscover and enumerate all attack surface for the assigned target.\n\n## Approach\n- Start with passive reconnaissance\n- Enumerate subdomains, ports, services\n- Identify technologies and frameworks\n- Map the application architecture\n- Document all findings systematically\n\n## Personality\n- Methodical and thorough\n- Never skip steps\n- Always verify findings`,
|
|
20
|
+
exploit: `# {{CODENAME}}\n\nYou are {{NAME}}, an autonomous exploitation specialist.\n\n## Mission\nIdentify and validate security vulnerabilities in assigned targets.\n\n## Approach\n- Analyze attack surface from recon data\n- Test for common vulnerability classes (OWASP Top 10)\n- Develop and validate proof-of-concept exploits\n- Assess impact and severity\n- Document reproduction steps clearly\n\n## Personality\n- Creative and persistent\n- Ethical — never cause damage\n- Report accurately, no exaggeration`,
|
|
21
|
+
report: `# {{CODENAME}}\n\nYou are {{NAME}}, an autonomous report writer.\n\n## Mission\nCreate clear, actionable security reports from findings data.\n\n## Approach\n- Gather all findings and evidence\n- Assess severity using CVSS\n- Write clear reproduction steps\n- Suggest remediation\n- Format for platform submission\n\n## Personality\n- Precise and concise\n- Technical but accessible\n- Focus on impact`,
|
|
22
|
+
custom: `# {{CODENAME}}\n\nYou are {{NAME}}, a specialized AI agent.\n\n## Role\n{{ROLE}}\n\n## Specialization\n{{SPEC}}\n\n## Mission\nDescribe the agent's primary objective here.\n\n## Approach\n- Step 1\n- Step 2\n- Step 3`,
|
|
23
|
+
};
|
|
7
24
|
|
|
8
25
|
// ─── Agent Card ──────────────────────────────────────────────────────────────
|
|
9
26
|
|
|
@@ -22,44 +39,29 @@ function AgentCard({ agent, onViewProfile, onAssignTask, index }) {
|
|
|
22
39
|
}`}
|
|
23
40
|
>
|
|
24
41
|
<div className="flex items-center gap-4 p-4">
|
|
25
|
-
{/* Avatar */}
|
|
26
42
|
<div className="shrink-0 w-12 h-12 rounded-lg bg-[--cyan]/10 border border-[--cyan]/20 flex items-center justify-center">
|
|
27
43
|
<span className="text-xl font-mono font-bold text-[--cyan]">{initial}</span>
|
|
28
44
|
</div>
|
|
29
|
-
|
|
30
|
-
{/* Info */}
|
|
31
45
|
<div className="flex-1 min-w-0">
|
|
32
46
|
<div className="flex items-center gap-2">
|
|
33
47
|
<p className="text-sm font-mono font-semibold text-foreground">@{codename.toUpperCase()}</p>
|
|
34
|
-
<div className={`w-2 h-2 rounded-full shrink-0 ${
|
|
35
|
-
isActive ? 'bg-green-500 animate-pulse' : 'bg-muted-foreground/40'
|
|
36
|
-
}`} />
|
|
48
|
+
<div className={`w-2 h-2 rounded-full shrink-0 ${isActive ? 'bg-green-500 animate-pulse' : 'bg-muted-foreground/40'}`} />
|
|
37
49
|
{isActive && (
|
|
38
50
|
<span className="inline-flex items-center gap-1 rounded-full bg-green-500/10 text-green-500 border border-green-500/20 px-2 py-0.5 text-[9px] font-mono font-medium">
|
|
39
51
|
{agent.activeJobs} job{agent.activeJobs !== 1 ? 's' : ''} running
|
|
40
52
|
</span>
|
|
41
53
|
)}
|
|
42
54
|
</div>
|
|
43
|
-
{agent.role &&
|
|
44
|
-
|
|
45
|
-
)}
|
|
46
|
-
{agent.specialization && (
|
|
47
|
-
<p className="text-[10px] text-muted-foreground/70 mt-0.5 font-mono truncate">{agent.specialization}</p>
|
|
48
|
-
)}
|
|
55
|
+
{agent.role && <p className="text-xs text-muted-foreground mt-0.5 font-mono truncate">{agent.role}</p>}
|
|
56
|
+
{agent.specialization && <p className="text-[10px] text-muted-foreground/70 mt-0.5 font-mono truncate">{agent.specialization}</p>}
|
|
49
57
|
</div>
|
|
50
|
-
|
|
51
|
-
{/* Actions */}
|
|
52
58
|
<div className="flex items-center gap-2 shrink-0">
|
|
53
|
-
<button
|
|
54
|
-
|
|
55
|
-
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] hover:border-[--cyan]/30 hover:text-[--cyan] transition-colors"
|
|
56
|
-
>
|
|
59
|
+
<button onClick={() => onViewProfile(agent.id)}
|
|
60
|
+
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] hover:border-[--cyan]/30 hover:text-[--cyan] transition-colors">
|
|
57
61
|
View
|
|
58
62
|
</button>
|
|
59
|
-
<button
|
|
60
|
-
|
|
61
|
-
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors"
|
|
62
|
-
>
|
|
63
|
+
<button onClick={() => onAssignTask(agent)}
|
|
64
|
+
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors">
|
|
63
65
|
Assign Task
|
|
64
66
|
</button>
|
|
65
67
|
</div>
|
|
@@ -115,16 +117,12 @@ function AgentProfilePanel({ agentId, onClose }) {
|
|
|
115
117
|
{ name: 'SKILLS.md', content: profile.skills, label: 'Skills' },
|
|
116
118
|
{ name: 'TOOLS.md', content: profile.tools, label: 'Tools' },
|
|
117
119
|
{ name: 'HEARTBEAT.md', content: profile.heartbeat, label: 'Heartbeat' },
|
|
120
|
+
{ name: 'IDENTITY.md', content: null, label: 'Identity' },
|
|
118
121
|
];
|
|
119
122
|
|
|
120
123
|
return (
|
|
121
|
-
<motion.div
|
|
122
|
-
|
|
123
|
-
animate={{ opacity: 1, y: 0 }}
|
|
124
|
-
exit={{ opacity: 0, y: 12 }}
|
|
125
|
-
className="rounded-lg border border-[--cyan]/20 bg-[--card] overflow-hidden"
|
|
126
|
-
>
|
|
127
|
-
{/* Header */}
|
|
124
|
+
<motion.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 12 }}
|
|
125
|
+
className="rounded-lg border border-[--cyan]/20 bg-[--card] overflow-hidden">
|
|
128
126
|
<div className="flex items-center justify-between p-4 border-b border-white/[0.06]">
|
|
129
127
|
<div className="flex items-center gap-3">
|
|
130
128
|
<div className="w-10 h-10 rounded-lg bg-[--cyan]/10 border border-[--cyan]/20 flex items-center justify-center">
|
|
@@ -135,51 +133,28 @@ function AgentProfilePanel({ agentId, onClose }) {
|
|
|
135
133
|
{profile.role && <p className="text-[10px] font-mono text-muted-foreground">{profile.role}</p>}
|
|
136
134
|
</div>
|
|
137
135
|
</div>
|
|
138
|
-
<button onClick={onClose} className="text-muted-foreground hover:text-foreground transition-colors">
|
|
139
|
-
<XIcon size={16} />
|
|
140
|
-
</button>
|
|
136
|
+
<button onClick={onClose} className="text-muted-foreground hover:text-foreground transition-colors"><XIcon size={16} /></button>
|
|
141
137
|
</div>
|
|
142
138
|
|
|
143
|
-
{/* Identity */}
|
|
144
139
|
<div className="p-4 border-b border-white/[0.06]">
|
|
145
140
|
<span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider">Identity</span>
|
|
146
141
|
<div className="grid grid-cols-2 gap-2 mt-2">
|
|
147
|
-
{profile.name &&
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
</div>
|
|
152
|
-
)}
|
|
153
|
-
<div>
|
|
154
|
-
<span className="text-[10px] font-mono text-muted-foreground uppercase">Codename</span>
|
|
155
|
-
<p className="text-xs font-mono">{codename}</p>
|
|
156
|
-
</div>
|
|
157
|
-
{profile.role && (
|
|
158
|
-
<div>
|
|
159
|
-
<span className="text-[10px] font-mono text-muted-foreground uppercase">Role</span>
|
|
160
|
-
<p className="text-xs font-mono">{profile.role}</p>
|
|
161
|
-
</div>
|
|
162
|
-
)}
|
|
163
|
-
{profile.specialization && (
|
|
164
|
-
<div>
|
|
165
|
-
<span className="text-[10px] font-mono text-muted-foreground uppercase">Specialization</span>
|
|
166
|
-
<p className="text-xs font-mono">{profile.specialization}</p>
|
|
167
|
-
</div>
|
|
168
|
-
)}
|
|
142
|
+
{profile.name && <div><span className="text-[10px] font-mono text-muted-foreground uppercase">Name</span><p className="text-xs font-mono">{profile.name}</p></div>}
|
|
143
|
+
<div><span className="text-[10px] font-mono text-muted-foreground uppercase">Codename</span><p className="text-xs font-mono">{codename}</p></div>
|
|
144
|
+
{profile.role && <div><span className="text-[10px] font-mono text-muted-foreground uppercase">Role</span><p className="text-xs font-mono">{profile.role}</p></div>}
|
|
145
|
+
{profile.specialization && <div><span className="text-[10px] font-mono text-muted-foreground uppercase">Specialization</span><p className="text-xs font-mono">{profile.specialization}</p></div>}
|
|
169
146
|
</div>
|
|
170
147
|
</div>
|
|
171
148
|
|
|
172
|
-
{/* Config */}
|
|
173
149
|
{profile.config && (
|
|
174
150
|
<div className="p-4 border-b border-white/[0.06]">
|
|
175
151
|
<span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider">Config</span>
|
|
176
|
-
<pre className="mt-2 text-[11px] bg-black/30 rounded-md p-2.5 font-mono overflow-auto max-h-
|
|
152
|
+
<pre className="mt-2 text-[11px] bg-black/30 rounded-md p-2.5 font-mono overflow-auto max-h-32 text-foreground/80 border border-white/[0.04] scrollbar-thin">
|
|
177
153
|
{JSON.stringify(profile.config, null, 2)}
|
|
178
154
|
</pre>
|
|
179
155
|
</div>
|
|
180
156
|
)}
|
|
181
157
|
|
|
182
|
-
{/* Files */}
|
|
183
158
|
<div className="p-4">
|
|
184
159
|
<span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider">Files</span>
|
|
185
160
|
<div className="flex flex-col gap-2 mt-2">
|
|
@@ -188,48 +163,23 @@ function AgentProfilePanel({ agentId, onClose }) {
|
|
|
188
163
|
<div className="flex items-center justify-between">
|
|
189
164
|
<span className="text-[10px] font-mono text-muted-foreground">{f.name}</span>
|
|
190
165
|
<div className="flex items-center gap-1">
|
|
191
|
-
{f.content ?
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
<span className="text-[9px] font-mono text-muted-foreground/50">empty</span>
|
|
195
|
-
)}
|
|
196
|
-
<button
|
|
197
|
-
onClick={() => editingFile === f.name ? setEditingFile(null) : startEdit(f.name, f.content)}
|
|
198
|
-
className="text-muted-foreground hover:text-[--cyan] transition-colors p-0.5"
|
|
199
|
-
>
|
|
200
|
-
<PencilIcon size={10} />
|
|
201
|
-
</button>
|
|
166
|
+
{f.content ? <span className="text-[9px] font-mono text-green-500">exists</span> : <span className="text-[9px] font-mono text-muted-foreground/50">empty</span>}
|
|
167
|
+
<button onClick={() => editingFile === f.name ? setEditingFile(null) : startEdit(f.name, f.content)}
|
|
168
|
+
className="text-muted-foreground hover:text-[--cyan] transition-colors p-0.5"><PencilIcon size={10} /></button>
|
|
202
169
|
</div>
|
|
203
170
|
</div>
|
|
204
|
-
|
|
205
171
|
<AnimatePresence>
|
|
206
172
|
{editingFile === f.name && (
|
|
207
|
-
<motion.div
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
exit={{ height: 0, opacity: 0 }}
|
|
211
|
-
className="overflow-hidden"
|
|
212
|
-
>
|
|
213
|
-
<textarea
|
|
214
|
-
value={editContent}
|
|
215
|
-
onChange={(e) => setEditContent(e.target.value)}
|
|
216
|
-
rows={8}
|
|
217
|
-
className="w-full mt-1 text-[11px] border border-white/[0.06] rounded-md p-2.5 bg-black/20 font-mono text-foreground/80 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors resize-y"
|
|
218
|
-
/>
|
|
173
|
+
<motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} className="overflow-hidden">
|
|
174
|
+
<textarea value={editContent} onChange={(e) => setEditContent(e.target.value)} rows={8}
|
|
175
|
+
className="w-full mt-1 text-[11px] border border-white/[0.06] rounded-md p-2.5 bg-black/20 font-mono text-foreground/80 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors resize-y" />
|
|
219
176
|
<div className="flex gap-2 mt-1">
|
|
220
|
-
<button
|
|
221
|
-
|
|
222
|
-
disabled={saving}
|
|
223
|
-
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-[10px] font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors disabled:opacity-50"
|
|
224
|
-
>
|
|
177
|
+
<button onClick={handleSave} disabled={saving}
|
|
178
|
+
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-[10px] font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors disabled:opacity-50">
|
|
225
179
|
{saving ? <SpinnerIcon size={10} /> : <CheckIcon size={10} />} Save
|
|
226
180
|
</button>
|
|
227
|
-
<button
|
|
228
|
-
|
|
229
|
-
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-[10px] font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors text-muted-foreground"
|
|
230
|
-
>
|
|
231
|
-
Cancel
|
|
232
|
-
</button>
|
|
181
|
+
<button onClick={() => setEditingFile(null)}
|
|
182
|
+
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-[10px] font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors text-muted-foreground">Cancel</button>
|
|
233
183
|
</div>
|
|
234
184
|
</motion.div>
|
|
235
185
|
)}
|
|
@@ -260,46 +210,27 @@ function AssignTaskDialog({ agent, onClose }) {
|
|
|
260
210
|
const codename = agent.codename || agent.name || agent.id;
|
|
261
211
|
|
|
262
212
|
return (
|
|
263
|
-
<motion.div
|
|
264
|
-
|
|
265
|
-
animate={{ opacity: 1, scale: 1 }}
|
|
266
|
-
exit={{ opacity: 0, scale: 0.95 }}
|
|
267
|
-
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
268
|
-
>
|
|
213
|
+
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }}
|
|
214
|
+
className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
269
215
|
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
|
|
270
216
|
<div className="relative w-full max-w-lg rounded-lg border border-[--cyan]/20 bg-[--card] shadow-2xl">
|
|
271
217
|
<div className="flex items-center justify-between p-4 border-b border-white/[0.06]">
|
|
272
218
|
<div className="flex items-center gap-2">
|
|
273
|
-
<div className="flex items-center gap-1">
|
|
274
|
-
|
|
275
|
-
<div className="w-2 h-2 rounded-full bg-[#febc2e]" />
|
|
276
|
-
<div className="w-2 h-2 rounded-full bg-[#28c840]" />
|
|
277
|
-
</div>
|
|
278
|
-
<span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider ml-1">
|
|
279
|
-
Assign Task to @{codename.toUpperCase()}
|
|
280
|
-
</span>
|
|
219
|
+
<div className="flex items-center gap-1"><div className="w-2 h-2 rounded-full bg-[#ff5f57]" /><div className="w-2 h-2 rounded-full bg-[#febc2e]" /><div className="w-2 h-2 rounded-full bg-[#28c840]" /></div>
|
|
220
|
+
<span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider ml-1">Assign Task to @{codename.toUpperCase()}</span>
|
|
281
221
|
</div>
|
|
282
222
|
<button onClick={onClose} className="text-muted-foreground hover:text-foreground"><XIcon size={14} /></button>
|
|
283
223
|
</div>
|
|
284
224
|
<div className="p-4">
|
|
285
|
-
<textarea
|
|
286
|
-
|
|
287
|
-
onChange={(e) => setPrompt(e.target.value)}
|
|
288
|
-
placeholder="Describe the task for this agent..."
|
|
289
|
-
rows={4}
|
|
290
|
-
className="w-full text-sm border border-white/[0.06] rounded-md p-3 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors resize-y"
|
|
291
|
-
autoFocus
|
|
292
|
-
/>
|
|
225
|
+
<textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Describe the task for this agent..." rows={4}
|
|
226
|
+
className="w-full text-sm border border-white/[0.06] rounded-md p-3 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors resize-y" autoFocus />
|
|
293
227
|
{result && (
|
|
294
228
|
<p className={`text-xs font-mono mt-2 ${result.error ? 'text-[--destructive]' : 'text-green-500'}`}>
|
|
295
229
|
{result.error || 'Job created successfully'}
|
|
296
230
|
</p>
|
|
297
231
|
)}
|
|
298
232
|
<div className="flex justify-end gap-2 mt-3">
|
|
299
|
-
<button onClick={onClose}
|
|
300
|
-
className="inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors text-muted-foreground">
|
|
301
|
-
Cancel
|
|
302
|
-
</button>
|
|
233
|
+
<button onClick={onClose} className="inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors text-muted-foreground">Cancel</button>
|
|
303
234
|
<button onClick={handleSubmit} disabled={submitting || !prompt}
|
|
304
235
|
className="inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors disabled:opacity-50">
|
|
305
236
|
{submitting ? <SpinnerIcon size={12} /> : 'Create Job'}
|
|
@@ -311,95 +242,491 @@ function AssignTaskDialog({ agent, onClose }) {
|
|
|
311
242
|
);
|
|
312
243
|
}
|
|
313
244
|
|
|
314
|
-
// ───
|
|
245
|
+
// ─── Tab Button ──────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
function TabBtn({ active, onClick, icon: Icon, label }) {
|
|
248
|
+
return (
|
|
249
|
+
<button onClick={onClick}
|
|
250
|
+
className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-mono font-medium rounded-md border transition-colors ${
|
|
251
|
+
active
|
|
252
|
+
? 'bg-[--cyan]/10 text-[--cyan] border-[--cyan]/20'
|
|
253
|
+
: 'border-white/[0.06] text-muted-foreground hover:text-foreground hover:bg-white/[0.04]'
|
|
254
|
+
}`}>
|
|
255
|
+
{Icon && <Icon size={12} />}
|
|
256
|
+
{label}
|
|
257
|
+
</button>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ─── Form Input Helpers ──────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
function FormField({ label, children }) {
|
|
264
|
+
return (
|
|
265
|
+
<div>
|
|
266
|
+
<label className="block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1">{label}</label>
|
|
267
|
+
{children}
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const inputClass = "w-full text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors";
|
|
273
|
+
const selectClass = "w-full text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono focus:outline-none focus:border-[--cyan]/40 transition-colors";
|
|
274
|
+
const textareaClass = "w-full text-sm border border-white/[0.06] rounded-md p-3 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors resize-y";
|
|
275
|
+
|
|
276
|
+
// ─── Create Agent Form (Enhanced) ────────────────────────────────────────────
|
|
315
277
|
|
|
316
278
|
function CreateAgentForm({ onCreated, onClose }) {
|
|
279
|
+
const [tab, setTab] = useState('basic');
|
|
280
|
+
const [creating, setCreating] = useState(false);
|
|
281
|
+
const [testing, setTesting] = useState(false);
|
|
282
|
+
const [testResult, setTestResult] = useState(null);
|
|
283
|
+
const [result, setResult] = useState(null);
|
|
284
|
+
const [mcpTools, setMcpTools] = useState([]);
|
|
285
|
+
const [loadingTools, setLoadingTools] = useState(false);
|
|
286
|
+
|
|
287
|
+
// Basic
|
|
317
288
|
const [name, setName] = useState('');
|
|
318
289
|
const [codename, setCodename] = useState('');
|
|
319
290
|
const [role, setRole] = useState('');
|
|
320
291
|
const [specialization, setSpecialization] = useState('');
|
|
292
|
+
const [description, setDescription] = useState('');
|
|
293
|
+
const [goal, setGoal] = useState('');
|
|
294
|
+
const [tags, setTags] = useState([]);
|
|
295
|
+
const [tagInput, setTagInput] = useState('');
|
|
296
|
+
|
|
297
|
+
// LLM
|
|
298
|
+
const [llmProvider, setLlmProvider] = useState('');
|
|
299
|
+
const [llmModel, setLlmModel] = useState('');
|
|
300
|
+
const [temperature, setTemperature] = useState(0.7);
|
|
301
|
+
const [maxTokens, setMaxTokens] = useState(4096);
|
|
302
|
+
|
|
303
|
+
// Soul / Skills / Heartbeat
|
|
304
|
+
const [soulTemplate, setSoulTemplate] = useState('custom');
|
|
321
305
|
const [soul, setSoul] = useState('');
|
|
322
|
-
const [
|
|
323
|
-
const [
|
|
306
|
+
const [skills, setSkills] = useState('');
|
|
307
|
+
const [heartbeat, setHeartbeat] = useState('');
|
|
308
|
+
|
|
309
|
+
// Tools
|
|
310
|
+
const [selectedTools, setSelectedTools] = useState([]);
|
|
311
|
+
|
|
312
|
+
// Scheduling
|
|
313
|
+
const [runMode, setRunMode] = useState('manual');
|
|
314
|
+
const [schedule, setSchedule] = useState('');
|
|
315
|
+
const [maxConcurrent, setMaxConcurrent] = useState(1);
|
|
316
|
+
const [timeout, setTimeout_] = useState(0);
|
|
317
|
+
|
|
318
|
+
// Resources
|
|
319
|
+
const [cpu, setCpu] = useState('1');
|
|
320
|
+
const [memory, setMemory] = useState('1GB');
|
|
321
|
+
const [disk, setDisk] = useState('5GB');
|
|
322
|
+
const [network, setNetwork] = useState('full');
|
|
323
|
+
|
|
324
|
+
// Filesystem
|
|
325
|
+
const [workDir, setWorkDir] = useState('/workspace');
|
|
326
|
+
const [allowedPaths, setAllowedPaths] = useState('');
|
|
327
|
+
|
|
328
|
+
// Env vars
|
|
329
|
+
const [envVars, setEnvVars] = useState([]);
|
|
330
|
+
|
|
331
|
+
// Team
|
|
332
|
+
const [swarm, setSwarm] = useState('');
|
|
333
|
+
const [supervisor, setSupervisor] = useState('');
|
|
334
|
+
|
|
335
|
+
// Auto-generate codename from name
|
|
336
|
+
useEffect(() => {
|
|
337
|
+
if (name && !codename) {
|
|
338
|
+
setCodename(name.toUpperCase().replace(/[^A-Z0-9]/g, '_').replace(/_+/g, '_'));
|
|
339
|
+
}
|
|
340
|
+
}, [name]);
|
|
341
|
+
|
|
342
|
+
// Load MCP tools on Tools tab
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
if (tab === 'tools' && mcpTools.length === 0 && !loadingTools) {
|
|
345
|
+
setLoadingTools(true);
|
|
346
|
+
getAvailableMcpTools().then(t => { setMcpTools(t); setLoadingTools(false); }).catch(() => setLoadingTools(false));
|
|
347
|
+
}
|
|
348
|
+
}, [tab]);
|
|
349
|
+
|
|
350
|
+
// Apply soul template
|
|
351
|
+
function applySoulTemplate(templateKey) {
|
|
352
|
+
setSoulTemplate(templateKey);
|
|
353
|
+
let content = SOUL_TEMPLATES[templateKey] || SOUL_TEMPLATES.custom;
|
|
354
|
+
content = content.replace(/\{\{CODENAME\}\}/g, codename || 'AGENT');
|
|
355
|
+
content = content.replace(/\{\{NAME\}\}/g, name || 'Agent');
|
|
356
|
+
content = content.replace(/\{\{ROLE\}\}/g, role || 'General Agent');
|
|
357
|
+
content = content.replace(/\{\{SPEC\}\}/g, specialization || 'General');
|
|
358
|
+
setSoul(content);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function addTag() {
|
|
362
|
+
const t = tagInput.trim().toLowerCase();
|
|
363
|
+
if (t && !tags.includes(t)) setTags([...tags, t]);
|
|
364
|
+
setTagInput('');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function addEnvVar() {
|
|
368
|
+
setEnvVars([...envVars, { key: '', value: '' }]);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function handleTest() {
|
|
372
|
+
setTesting(true);
|
|
373
|
+
setTestResult(null);
|
|
374
|
+
const issues = [];
|
|
375
|
+
if (!name && !codename) issues.push('Name or codename is required');
|
|
376
|
+
if (llmProvider && !llmModel) issues.push('LLM model not selected');
|
|
377
|
+
if (runMode === 'scheduled' && !schedule) issues.push('Schedule expression is required for scheduled mode');
|
|
378
|
+
setTestResult(issues.length === 0 ? { success: true } : { issues });
|
|
379
|
+
setTesting(false);
|
|
380
|
+
}
|
|
324
381
|
|
|
325
382
|
async function handleCreate() {
|
|
326
383
|
if (!name && !codename) return;
|
|
327
384
|
setCreating(true);
|
|
328
|
-
const
|
|
385
|
+
const identity = {
|
|
386
|
+
name, codename, role, specialization, description, goal, tags,
|
|
387
|
+
soul: soul || undefined,
|
|
388
|
+
skills: skills || undefined,
|
|
389
|
+
heartbeat: heartbeat || undefined,
|
|
390
|
+
llm: llmProvider ? { provider: llmProvider, model: llmModel, temperature, maxTokens } : undefined,
|
|
391
|
+
scheduling: runMode !== 'manual' ? { mode: runMode, schedule, maxConcurrent, timeout: timeout || undefined } : undefined,
|
|
392
|
+
resources: { cpu, memory, disk, network },
|
|
393
|
+
mcpTools: selectedTools.length > 0 ? selectedTools : undefined,
|
|
394
|
+
envVars: envVars.reduce((acc, v) => { if (v.key) acc[v.key] = v.value; return acc; }, {}),
|
|
395
|
+
team: (swarm || supervisor) ? { swarm: swarm || undefined, supervisor: supervisor || undefined } : undefined,
|
|
396
|
+
};
|
|
397
|
+
const res = await createAgent(identity);
|
|
329
398
|
setResult(res);
|
|
330
|
-
if (!res.error)
|
|
331
|
-
onCreated();
|
|
332
|
-
setName(''); setCodename(''); setRole(''); setSpecialization(''); setSoul('');
|
|
333
|
-
}
|
|
399
|
+
if (!res.error) onCreated();
|
|
334
400
|
setCreating(false);
|
|
335
401
|
}
|
|
336
402
|
|
|
403
|
+
const TABS = [
|
|
404
|
+
{ id: 'basic', label: 'Basic', icon: UsersIcon },
|
|
405
|
+
{ id: 'llm', label: 'LLM', icon: CpuIcon },
|
|
406
|
+
{ id: 'soul', label: 'Soul', icon: PencilIcon },
|
|
407
|
+
{ id: 'tools', label: 'Tools', icon: PackageIcon },
|
|
408
|
+
{ id: 'schedule', label: 'Schedule', icon: ClockIcon },
|
|
409
|
+
{ id: 'resources', label: 'Resources', icon: SlidersIcon },
|
|
410
|
+
{ id: 'files', label: 'Files', icon: FolderIcon },
|
|
411
|
+
{ id: 'env', label: 'Env', icon: HashIcon },
|
|
412
|
+
{ id: 'team', label: 'Team', icon: UsersIcon },
|
|
413
|
+
];
|
|
414
|
+
|
|
337
415
|
return (
|
|
338
|
-
<motion.div
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
className="overflow-hidden"
|
|
343
|
-
>
|
|
344
|
-
<div className="rounded-lg border border-[--cyan]/20 bg-[--card] p-5 mt-4">
|
|
345
|
-
<div className="flex items-center justify-between mb-4">
|
|
416
|
+
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} exit={{ opacity: 0, height: 0 }} className="overflow-hidden">
|
|
417
|
+
<div className="rounded-lg border border-[--cyan]/20 bg-[--card] mt-4 overflow-hidden">
|
|
418
|
+
{/* Header */}
|
|
419
|
+
<div className="flex items-center justify-between p-4 border-b border-white/[0.06]">
|
|
346
420
|
<div className="flex items-center gap-2">
|
|
347
|
-
<div className="flex items-center gap-1">
|
|
348
|
-
|
|
349
|
-
<div className="w-2 h-2 rounded-full bg-[#febc2e]" />
|
|
350
|
-
<div className="w-2 h-2 rounded-full bg-[#28c840]" />
|
|
351
|
-
</div>
|
|
352
|
-
<span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider ml-1">
|
|
353
|
-
Create New Agent
|
|
354
|
-
</span>
|
|
421
|
+
<div className="flex items-center gap-1"><div className="w-2 h-2 rounded-full bg-[#ff5f57]" /><div className="w-2 h-2 rounded-full bg-[#febc2e]" /><div className="w-2 h-2 rounded-full bg-[#28c840]" /></div>
|
|
422
|
+
<span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider ml-1">Create New Agent</span>
|
|
355
423
|
</div>
|
|
356
424
|
<button onClick={onClose} className="text-muted-foreground hover:text-foreground"><XIcon size={14} /></button>
|
|
357
425
|
</div>
|
|
358
426
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="Reaper"
|
|
363
|
-
className="w-full text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors" />
|
|
364
|
-
</div>
|
|
365
|
-
<div>
|
|
366
|
-
<label className="block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1">Codename</label>
|
|
367
|
-
<input value={codename} onChange={(e) => setCodename(e.target.value)} placeholder="REAPER"
|
|
368
|
-
className="w-full text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors" />
|
|
369
|
-
</div>
|
|
370
|
-
<div>
|
|
371
|
-
<label className="block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1">Role</label>
|
|
372
|
-
<input value={role} onChange={(e) => setRole(e.target.value)} placeholder="Autonomous Recon Specialist"
|
|
373
|
-
className="w-full text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors" />
|
|
374
|
-
</div>
|
|
375
|
-
<div>
|
|
376
|
-
<label className="block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1">Specialization</label>
|
|
377
|
-
<input value={specialization} onChange={(e) => setSpecialization(e.target.value)} placeholder="Bug Bounty Recon"
|
|
378
|
-
className="w-full text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors" />
|
|
379
|
-
</div>
|
|
427
|
+
{/* Tabs */}
|
|
428
|
+
<div className="flex flex-wrap gap-1.5 p-4 pb-0 overflow-x-auto scrollbar-thin">
|
|
429
|
+
{TABS.map(t => <TabBtn key={t.id} active={tab === t.id} onClick={() => setTab(t.id)} icon={t.icon} label={t.label} />)}
|
|
380
430
|
</div>
|
|
381
431
|
|
|
382
|
-
<div className="
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
className="
|
|
386
|
-
|
|
432
|
+
<div className="p-4">
|
|
433
|
+
{/* ── Basic Tab ──────────────────────────────────────────────────── */}
|
|
434
|
+
{tab === 'basic' && (
|
|
435
|
+
<div className="flex flex-col gap-4">
|
|
436
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
437
|
+
<FormField label="Name">
|
|
438
|
+
<input value={name} onChange={e => setName(e.target.value)} placeholder="Reaper" className={inputClass} />
|
|
439
|
+
</FormField>
|
|
440
|
+
<FormField label="Codename (auto-generated)">
|
|
441
|
+
<input value={codename} onChange={e => setCodename(e.target.value)} placeholder="REAPER" className={inputClass} />
|
|
442
|
+
</FormField>
|
|
443
|
+
<FormField label="Role">
|
|
444
|
+
<select value={role} onChange={e => setRole(e.target.value)} className={selectClass}>
|
|
445
|
+
<option value="">Select role...</option>
|
|
446
|
+
{ROLE_OPTIONS.map(r => <option key={r} value={r}>{r}</option>)}
|
|
447
|
+
</select>
|
|
448
|
+
</FormField>
|
|
449
|
+
<FormField label="Specialization">
|
|
450
|
+
<select value={specialization} onChange={e => setSpecialization(e.target.value)} className={selectClass}>
|
|
451
|
+
<option value="">Select specialization...</option>
|
|
452
|
+
{SPEC_OPTIONS.map(s => <option key={s} value={s}>{s}</option>)}
|
|
453
|
+
</select>
|
|
454
|
+
</FormField>
|
|
455
|
+
</div>
|
|
456
|
+
<FormField label="Description">
|
|
457
|
+
<input value={description} onChange={e => setDescription(e.target.value)} placeholder="Brief purpose statement" className={inputClass} />
|
|
458
|
+
</FormField>
|
|
459
|
+
<FormField label="Goal">
|
|
460
|
+
<textarea value={goal} onChange={e => setGoal(e.target.value)} placeholder="Specific mission objective..." rows={2} className={textareaClass} />
|
|
461
|
+
</FormField>
|
|
462
|
+
<FormField label="Tags">
|
|
463
|
+
<div className="flex flex-wrap gap-1.5 mb-2">
|
|
464
|
+
{tags.map(t => (
|
|
465
|
+
<span key={t} className="inline-flex items-center gap-1 rounded-full bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 px-2 py-0.5 text-[10px] font-mono">
|
|
466
|
+
{t}
|
|
467
|
+
<button onClick={() => setTags(tags.filter(x => x !== t))} className="hover:text-[--destructive]"><XIcon size={8} /></button>
|
|
468
|
+
</span>
|
|
469
|
+
))}
|
|
470
|
+
</div>
|
|
471
|
+
<div className="flex gap-2">
|
|
472
|
+
<input value={tagInput} onChange={e => setTagInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && (e.preventDefault(), addTag())}
|
|
473
|
+
placeholder="Add tag..." className={inputClass} />
|
|
474
|
+
<button onClick={addTag} className="shrink-0 inline-flex items-center gap-1 rounded-md px-3 py-2 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors">
|
|
475
|
+
<PlusIcon size={12} />
|
|
476
|
+
</button>
|
|
477
|
+
</div>
|
|
478
|
+
</FormField>
|
|
479
|
+
</div>
|
|
480
|
+
)}
|
|
387
481
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
482
|
+
{/* ── LLM Tab ────────────────────────────────────────────────────── */}
|
|
483
|
+
{tab === 'llm' && (
|
|
484
|
+
<div className="flex flex-col gap-4">
|
|
485
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
486
|
+
<FormField label="Provider">
|
|
487
|
+
<select value={llmProvider} onChange={e => { setLlmProvider(e.target.value); setLlmModel(''); }} className={selectClass}>
|
|
488
|
+
<option value="">Default (inherit)</option>
|
|
489
|
+
<option value="anthropic">Anthropic</option>
|
|
490
|
+
<option value="openai">OpenAI</option>
|
|
491
|
+
<option value="google">Google</option>
|
|
492
|
+
<option value="custom">Custom</option>
|
|
493
|
+
</select>
|
|
494
|
+
</FormField>
|
|
495
|
+
<FormField label="Model">
|
|
496
|
+
<select value={llmModel} onChange={e => setLlmModel(e.target.value)} className={selectClass} disabled={!llmProvider}>
|
|
497
|
+
<option value="">Select model...</option>
|
|
498
|
+
{(PROVIDER_MODELS[llmProvider] || []).map(m => <option key={m} value={m}>{m}</option>)}
|
|
499
|
+
</select>
|
|
500
|
+
{llmProvider === 'custom' && (
|
|
501
|
+
<input value={llmModel} onChange={e => setLlmModel(e.target.value)} placeholder="model-name" className={`${inputClass} mt-2`} />
|
|
502
|
+
)}
|
|
503
|
+
</FormField>
|
|
504
|
+
</div>
|
|
505
|
+
<FormField label={`Temperature: ${temperature}`}>
|
|
506
|
+
<input type="range" min="0" max="1" step="0.05" value={temperature} onChange={e => setTemperature(parseFloat(e.target.value))}
|
|
507
|
+
className="w-full accent-[--cyan]" />
|
|
508
|
+
<div className="flex justify-between text-[9px] font-mono text-muted-foreground mt-1">
|
|
509
|
+
<span>Precise (0)</span><span>Creative (1)</span>
|
|
510
|
+
</div>
|
|
511
|
+
</FormField>
|
|
512
|
+
<FormField label="Max Tokens">
|
|
513
|
+
<div className="flex items-center gap-2">
|
|
514
|
+
<input type="number" value={maxTokens} onChange={e => setMaxTokens(parseInt(e.target.value) || 4096)} className={inputClass} />
|
|
515
|
+
<div className="flex gap-1">
|
|
516
|
+
{[4096, 8192, 16384].map(v => (
|
|
517
|
+
<button key={v} onClick={() => setMaxTokens(v)}
|
|
518
|
+
className={`px-2 py-1 text-[10px] font-mono rounded border transition-colors ${maxTokens === v ? 'bg-[--cyan]/10 text-[--cyan] border-[--cyan]/20' : 'border-white/[0.06] text-muted-foreground hover:text-foreground'}`}>
|
|
519
|
+
{v >= 1000 ? `${v/1000}k` : v}
|
|
520
|
+
</button>
|
|
521
|
+
))}
|
|
522
|
+
</div>
|
|
523
|
+
</div>
|
|
524
|
+
</FormField>
|
|
525
|
+
</div>
|
|
526
|
+
)}
|
|
393
527
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
className="
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
528
|
+
{/* ── Soul Tab ───────────────────────────────────────────────────── */}
|
|
529
|
+
{tab === 'soul' && (
|
|
530
|
+
<div className="flex flex-col gap-4">
|
|
531
|
+
<FormField label="Template">
|
|
532
|
+
<div className="flex gap-2 mb-2">
|
|
533
|
+
{Object.entries({ recon: 'Recon Agent', exploit: 'Exploit Dev', report: 'Report Writer', custom: 'Custom' }).map(([k, label]) => (
|
|
534
|
+
<button key={k} onClick={() => applySoulTemplate(k)}
|
|
535
|
+
className={`px-3 py-1.5 text-xs font-mono rounded-md border transition-colors ${
|
|
536
|
+
soulTemplate === k ? 'bg-[--cyan]/10 text-[--cyan] border-[--cyan]/20' : 'border-white/[0.06] text-muted-foreground hover:text-foreground hover:bg-white/[0.04]'
|
|
537
|
+
}`}>{label}</button>
|
|
538
|
+
))}
|
|
539
|
+
</div>
|
|
540
|
+
</FormField>
|
|
541
|
+
<FormField label="SOUL.md">
|
|
542
|
+
<textarea value={soul} onChange={e => setSoul(e.target.value)} rows={10} placeholder="Agent personality and system prompt..."
|
|
543
|
+
className={textareaClass} />
|
|
544
|
+
</FormField>
|
|
545
|
+
<FormField label="SKILLS.md (optional)">
|
|
546
|
+
<textarea value={skills} onChange={e => setSkills(e.target.value)} rows={4} placeholder="Define agent skills and capabilities..."
|
|
547
|
+
className={textareaClass} />
|
|
548
|
+
</FormField>
|
|
549
|
+
<FormField label="HEARTBEAT.md (optional)">
|
|
550
|
+
<textarea value={heartbeat} onChange={e => setHeartbeat(e.target.value)} rows={3} placeholder="Self-monitoring behavior..."
|
|
551
|
+
className={textareaClass} />
|
|
552
|
+
</FormField>
|
|
553
|
+
</div>
|
|
554
|
+
)}
|
|
555
|
+
|
|
556
|
+
{/* ── Tools Tab ──────────────────────────────────────────────────── */}
|
|
557
|
+
{tab === 'tools' && (
|
|
558
|
+
<div className="flex flex-col gap-4">
|
|
559
|
+
<p className="text-xs font-mono text-muted-foreground">Select MCP tools this agent can use.</p>
|
|
560
|
+
{loadingTools ? (
|
|
561
|
+
<div className="flex items-center gap-2 py-4"><SpinnerIcon size={14} /><span className="text-xs font-mono text-muted-foreground">Loading tools...</span></div>
|
|
562
|
+
) : mcpTools.length === 0 ? (
|
|
563
|
+
<div className="text-xs font-mono text-muted-foreground py-4">No MCP tools configured. Add tools in Settings > MCP.</div>
|
|
564
|
+
) : (
|
|
565
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 max-h-[300px] overflow-y-auto scrollbar-thin">
|
|
566
|
+
{mcpTools.map(tool => {
|
|
567
|
+
const checked = selectedTools.includes(tool.name);
|
|
568
|
+
return (
|
|
569
|
+
<label key={tool.name}
|
|
570
|
+
className={`flex items-start gap-2.5 p-2.5 rounded-lg border cursor-pointer transition-colors ${
|
|
571
|
+
checked ? 'border-[--cyan]/30 bg-[--cyan]/5' : 'border-white/[0.06] bg-[--card] hover:border-white/[0.12]'
|
|
572
|
+
}`}>
|
|
573
|
+
<input type="checkbox" checked={checked}
|
|
574
|
+
onChange={e => setSelectedTools(e.target.checked ? [...selectedTools, tool.name] : selectedTools.filter(t => t !== tool.name))}
|
|
575
|
+
className="mt-0.5 accent-[--cyan]" />
|
|
576
|
+
<div className="min-w-0">
|
|
577
|
+
<p className="text-xs font-mono font-medium truncate">{tool.name}</p>
|
|
578
|
+
{tool.description && <p className="text-[10px] font-mono text-muted-foreground truncate mt-0.5">{tool.description}</p>}
|
|
579
|
+
</div>
|
|
580
|
+
</label>
|
|
581
|
+
);
|
|
582
|
+
})}
|
|
583
|
+
</div>
|
|
584
|
+
)}
|
|
585
|
+
{selectedTools.length > 0 && (
|
|
586
|
+
<div className="flex flex-wrap gap-1.5">
|
|
587
|
+
<span className="text-[10px] font-mono text-muted-foreground">Selected:</span>
|
|
588
|
+
{selectedTools.map(t => (
|
|
589
|
+
<span key={t} className="inline-flex items-center gap-1 rounded-full bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 px-2 py-0.5 text-[9px] font-mono">
|
|
590
|
+
{t}<button onClick={() => setSelectedTools(selectedTools.filter(x => x !== t))}><XIcon size={8} /></button>
|
|
591
|
+
</span>
|
|
592
|
+
))}
|
|
593
|
+
</div>
|
|
594
|
+
)}
|
|
595
|
+
</div>
|
|
596
|
+
)}
|
|
597
|
+
|
|
598
|
+
{/* ── Schedule Tab ───────────────────────────────────────────────── */}
|
|
599
|
+
{tab === 'schedule' && (
|
|
600
|
+
<div className="flex flex-col gap-4">
|
|
601
|
+
<FormField label="Run Mode">
|
|
602
|
+
<select value={runMode} onChange={e => setRunMode(e.target.value)} className={selectClass}>
|
|
603
|
+
{RUN_MODES.map(m => <option key={m} value={m}>{m}</option>)}
|
|
604
|
+
</select>
|
|
605
|
+
</FormField>
|
|
606
|
+
{runMode === 'scheduled' && (
|
|
607
|
+
<FormField label="Cron Schedule">
|
|
608
|
+
<input value={schedule} onChange={e => setSchedule(e.target.value)} placeholder="0 */6 * * *" className={inputClass} />
|
|
609
|
+
<p className="text-[9px] font-mono text-muted-foreground mt-1">Standard cron expression (min hour dom month dow)</p>
|
|
610
|
+
</FormField>
|
|
611
|
+
)}
|
|
612
|
+
<div className="grid grid-cols-2 gap-3">
|
|
613
|
+
<FormField label="Max Concurrent Jobs">
|
|
614
|
+
<select value={maxConcurrent} onChange={e => setMaxConcurrent(parseInt(e.target.value))} className={selectClass}>
|
|
615
|
+
{[1, 2, 5, 10].map(v => <option key={v} value={v}>{v}</option>)}
|
|
616
|
+
</select>
|
|
617
|
+
</FormField>
|
|
618
|
+
<FormField label="Timeout (seconds, 0=none)">
|
|
619
|
+
<input type="number" value={timeout} onChange={e => setTimeout_(parseInt(e.target.value) || 0)} className={inputClass} min="0" />
|
|
620
|
+
</FormField>
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
623
|
+
)}
|
|
624
|
+
|
|
625
|
+
{/* ── Resources Tab ──────────────────────────────────────────────── */}
|
|
626
|
+
{tab === 'resources' && (
|
|
627
|
+
<div className="flex flex-col gap-4">
|
|
628
|
+
<div className="grid grid-cols-2 gap-3">
|
|
629
|
+
<FormField label="CPU Cores">
|
|
630
|
+
<select value={cpu} onChange={e => setCpu(e.target.value)} className={selectClass}>
|
|
631
|
+
{['0.5', '1', '2', '4'].map(v => <option key={v} value={v}>{v} core{v !== '1' ? 's' : ''}</option>)}
|
|
632
|
+
</select>
|
|
633
|
+
</FormField>
|
|
634
|
+
<FormField label="Memory">
|
|
635
|
+
<select value={memory} onChange={e => setMemory(e.target.value)} className={selectClass}>
|
|
636
|
+
{['512MB', '1GB', '2GB', '4GB', '8GB'].map(v => <option key={v} value={v}>{v}</option>)}
|
|
637
|
+
</select>
|
|
638
|
+
</FormField>
|
|
639
|
+
<FormField label="Disk">
|
|
640
|
+
<select value={disk} onChange={e => setDisk(e.target.value)} className={selectClass}>
|
|
641
|
+
{['1GB', '5GB', '10GB', 'Unlimited'].map(v => <option key={v} value={v}>{v}</option>)}
|
|
642
|
+
</select>
|
|
643
|
+
</FormField>
|
|
644
|
+
<FormField label="Network">
|
|
645
|
+
<select value={network} onChange={e => setNetwork(e.target.value)} className={selectClass}>
|
|
646
|
+
<option value="isolated">Isolated</option>
|
|
647
|
+
<option value="internal">Internal only</option>
|
|
648
|
+
<option value="full">Full internet</option>
|
|
649
|
+
</select>
|
|
650
|
+
</FormField>
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
)}
|
|
654
|
+
|
|
655
|
+
{/* ── Files Tab ──────────────────────────────────────────────────── */}
|
|
656
|
+
{tab === 'files' && (
|
|
657
|
+
<div className="flex flex-col gap-4">
|
|
658
|
+
<FormField label="Working Directory">
|
|
659
|
+
<input value={workDir} onChange={e => setWorkDir(e.target.value)} className={inputClass} />
|
|
660
|
+
</FormField>
|
|
661
|
+
<FormField label="Allowed Paths (comma-separated)">
|
|
662
|
+
<input value={allowedPaths} onChange={e => setAllowedPaths(e.target.value)} placeholder="/workspace, /tmp" className={inputClass} />
|
|
663
|
+
</FormField>
|
|
664
|
+
</div>
|
|
665
|
+
)}
|
|
666
|
+
|
|
667
|
+
{/* ── Env Tab ────────────────────────────────────────────────────── */}
|
|
668
|
+
{tab === 'env' && (
|
|
669
|
+
<div className="flex flex-col gap-3">
|
|
670
|
+
<p className="text-xs font-mono text-muted-foreground">Agent-specific environment variables.</p>
|
|
671
|
+
{envVars.map((v, i) => (
|
|
672
|
+
<div key={i} className="flex gap-2">
|
|
673
|
+
<input value={v.key} onChange={e => { const nv = [...envVars]; nv[i] = { ...nv[i], key: e.target.value }; setEnvVars(nv); }}
|
|
674
|
+
placeholder="KEY" className={inputClass} />
|
|
675
|
+
<input value={v.value} onChange={e => { const nv = [...envVars]; nv[i] = { ...nv[i], value: e.target.value }; setEnvVars(nv); }}
|
|
676
|
+
placeholder="value" className={inputClass} />
|
|
677
|
+
<button onClick={() => setEnvVars(envVars.filter((_, j) => j !== i))}
|
|
678
|
+
className="shrink-0 p-2 text-muted-foreground hover:text-[--destructive] transition-colors"><TrashIcon size={12} /></button>
|
|
679
|
+
</div>
|
|
680
|
+
))}
|
|
681
|
+
<button onClick={addEnvVar}
|
|
682
|
+
className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-mono font-medium border border-dashed border-white/[0.12] hover:bg-white/[0.04] transition-colors text-muted-foreground hover:text-foreground self-start">
|
|
683
|
+
<PlusIcon size={12} /> Add Variable
|
|
684
|
+
</button>
|
|
685
|
+
</div>
|
|
686
|
+
)}
|
|
687
|
+
|
|
688
|
+
{/* ── Team Tab ───────────────────────────────────────────────────── */}
|
|
689
|
+
{tab === 'team' && (
|
|
690
|
+
<div className="flex flex-col gap-4">
|
|
691
|
+
<FormField label="Part of Swarm">
|
|
692
|
+
<select value={swarm} onChange={e => setSwarm(e.target.value)} className={selectClass}>
|
|
693
|
+
<option value="">None</option>
|
|
694
|
+
<option value="recon-team">Recon Team</option>
|
|
695
|
+
<option value="exploit-team">Exploit Team</option>
|
|
696
|
+
<option value="full-swarm">Full Swarm</option>
|
|
697
|
+
</select>
|
|
698
|
+
</FormField>
|
|
699
|
+
<FormField label="Supervisor Agent">
|
|
700
|
+
<input value={supervisor} onChange={e => setSupervisor(e.target.value)} placeholder="@OVERSEER" className={inputClass} />
|
|
701
|
+
</FormField>
|
|
702
|
+
</div>
|
|
703
|
+
)}
|
|
704
|
+
|
|
705
|
+
{/* ── Actions ────────────────────────────────────────────────────── */}
|
|
706
|
+
<div className="flex items-center gap-2 mt-4 pt-4 border-t border-white/[0.06]">
|
|
707
|
+
<button onClick={handleCreate} disabled={creating || (!name && !codename)}
|
|
708
|
+
className="inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors disabled:opacity-50">
|
|
709
|
+
{creating ? <SpinnerIcon size={12} /> : <PlusIcon size={12} />} Create Agent
|
|
710
|
+
</button>
|
|
711
|
+
<button onClick={handleTest} disabled={testing}
|
|
712
|
+
className="inline-flex items-center gap-1.5 rounded-md px-3 py-2 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors text-muted-foreground hover:text-foreground disabled:opacity-50">
|
|
713
|
+
{testing ? <SpinnerIcon size={12} /> : <PlayIcon size={12} />} Validate
|
|
714
|
+
</button>
|
|
715
|
+
<button onClick={onClose}
|
|
716
|
+
className="inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors text-muted-foreground">
|
|
717
|
+
Cancel
|
|
718
|
+
</button>
|
|
719
|
+
{result && (
|
|
720
|
+
<p className={`text-xs font-mono ml-2 ${result.error ? 'text-[--destructive]' : 'text-green-500'}`}>
|
|
721
|
+
{result.error || `Agent created: ${result.id}`}
|
|
722
|
+
</p>
|
|
723
|
+
)}
|
|
724
|
+
{testResult && (
|
|
725
|
+
<p className={`text-xs font-mono ml-2 ${testResult.success ? 'text-green-500' : 'text-yellow-500'}`}>
|
|
726
|
+
{testResult.success ? 'Configuration valid' : testResult.issues?.join(', ')}
|
|
727
|
+
</p>
|
|
728
|
+
)}
|
|
729
|
+
</div>
|
|
403
730
|
</div>
|
|
404
731
|
</div>
|
|
405
732
|
</motion.div>
|
|
@@ -440,12 +767,10 @@ export function AgentsPage() {
|
|
|
440
767
|
|
|
441
768
|
if (loading) {
|
|
442
769
|
return (
|
|
443
|
-
<div className="flex flex-col gap-4
|
|
770
|
+
<div className="flex flex-col gap-4">
|
|
444
771
|
<div className="h-8 w-48 animate-pulse rounded-lg bg-white/[0.04]" />
|
|
445
772
|
<div className="flex flex-col gap-3">
|
|
446
|
-
{[...Array(4)].map((_, i) =>
|
|
447
|
-
<div key={i} className="h-20 animate-pulse rounded-lg bg-white/[0.04] border border-white/[0.06]" />
|
|
448
|
-
))}
|
|
773
|
+
{[...Array(4)].map((_, i) => <div key={i} className="h-20 animate-pulse rounded-lg bg-white/[0.04] border border-white/[0.06]" />)}
|
|
449
774
|
</div>
|
|
450
775
|
</div>
|
|
451
776
|
);
|
|
@@ -453,33 +778,24 @@ export function AgentsPage() {
|
|
|
453
778
|
|
|
454
779
|
return (
|
|
455
780
|
<>
|
|
456
|
-
{/* Header */}
|
|
457
781
|
<div className="flex items-center justify-between mb-6">
|
|
458
782
|
<div>
|
|
459
783
|
<h1 className="text-2xl font-mono font-semibold text-[--cyan] text-glow-cyan">Agents</h1>
|
|
460
784
|
<p className="text-[11px] text-muted-foreground mt-1 font-mono">Manage agent profiles, roles, and assignments</p>
|
|
461
785
|
</div>
|
|
462
786
|
<div className="flex items-center gap-2">
|
|
463
|
-
<span className="inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-[10px] font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20">
|
|
464
|
-
{agents.length} agents
|
|
465
|
-
</span>
|
|
787
|
+
<span className="inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-[10px] font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20">{agents.length} agents</span>
|
|
466
788
|
{activeCount > 0 && (
|
|
467
789
|
<span className="inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-[10px] font-mono font-medium bg-green-500/10 text-green-500 border border-green-500/20">
|
|
468
|
-
<span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
|
|
469
|
-
{activeCount} active
|
|
790
|
+
<span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />{activeCount} active
|
|
470
791
|
</span>
|
|
471
792
|
)}
|
|
472
|
-
<button
|
|
473
|
-
|
|
474
|
-
className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors"
|
|
475
|
-
>
|
|
793
|
+
<button onClick={() => setShowCreate(!showCreate)}
|
|
794
|
+
className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors">
|
|
476
795
|
<PlusIcon size={12} /> New Agent
|
|
477
796
|
</button>
|
|
478
|
-
<button
|
|
479
|
-
|
|
480
|
-
disabled={refreshing}
|
|
481
|
-
className="inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-mono font-medium border border-white/[0.06] text-muted-foreground hover:text-foreground hover:bg-white/[0.04] disabled:opacity-50 transition-colors"
|
|
482
|
-
>
|
|
797
|
+
<button onClick={() => { setRefreshing(true); load(); }} disabled={refreshing}
|
|
798
|
+
className="inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-mono font-medium border border-white/[0.06] text-muted-foreground hover:text-foreground hover:bg-white/[0.04] disabled:opacity-50 transition-colors">
|
|
483
799
|
{refreshing ? <SpinnerIcon size={14} /> : <RefreshIcon size={14} />}
|
|
484
800
|
</button>
|
|
485
801
|
</div>
|
|
@@ -505,32 +821,24 @@ export function AgentsPage() {
|
|
|
505
821
|
{agents.length > 0 && (
|
|
506
822
|
<div className="relative mb-4">
|
|
507
823
|
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"><SearchIcon size={14} /></div>
|
|
508
|
-
<input
|
|
509
|
-
|
|
510
|
-
value={search}
|
|
511
|
-
onChange={e => setSearch(e.target.value)}
|
|
512
|
-
className="w-full text-sm border border-white/[0.06] rounded-md pl-9 pr-3 py-2 bg-black/20 font-mono placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors"
|
|
513
|
-
/>
|
|
824
|
+
<input placeholder="Search agents..." value={search} onChange={e => setSearch(e.target.value)}
|
|
825
|
+
className="w-full text-sm border border-white/[0.06] rounded-md pl-9 pr-3 py-2 bg-black/20 font-mono placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors" />
|
|
514
826
|
</div>
|
|
515
827
|
)}
|
|
516
828
|
|
|
517
829
|
{/* Create form */}
|
|
518
830
|
<AnimatePresence>
|
|
519
|
-
{showCreate && (
|
|
520
|
-
<CreateAgentForm onCreated={() => { load(); setShowCreate(false); }} onClose={() => setShowCreate(false)} />
|
|
521
|
-
)}
|
|
831
|
+
{showCreate && <CreateAgentForm onCreated={() => { load(); setShowCreate(false); }} onClose={() => setShowCreate(false)} />}
|
|
522
832
|
</AnimatePresence>
|
|
523
833
|
|
|
524
834
|
{/* Agent list */}
|
|
525
835
|
<div className="flex flex-col gap-3 mt-4">
|
|
526
836
|
{filtered.length === 0 && agents.length === 0 ? (
|
|
527
837
|
<div className="flex flex-col items-center justify-center py-16 text-center rounded-lg border border-white/[0.06] bg-[--card]">
|
|
528
|
-
<div className="rounded-full bg-[--cyan]/10 p-4 mb-4">
|
|
529
|
-
<UsersIcon size={24} className="text-[--cyan]" />
|
|
530
|
-
</div>
|
|
838
|
+
<div className="rounded-full bg-[--cyan]/10 p-4 mb-4"><UsersIcon size={24} className="text-[--cyan]" /></div>
|
|
531
839
|
<p className="text-sm font-mono font-medium mb-1">No agents configured</p>
|
|
532
840
|
<p className="text-[11px] text-muted-foreground font-mono max-w-sm">
|
|
533
|
-
Create agent profiles in the <span className="text-[--cyan]">agents/</span> directory or click
|
|
841
|
+
Create agent profiles in the <span className="text-[--cyan]">agents/</span> directory or click “New Agent” above.
|
|
534
842
|
</p>
|
|
535
843
|
</div>
|
|
536
844
|
) : filtered.length === 0 ? (
|
|
@@ -540,13 +848,7 @@ export function AgentsPage() {
|
|
|
540
848
|
</div>
|
|
541
849
|
) : (
|
|
542
850
|
filtered.map((agent, i) => (
|
|
543
|
-
<AgentCard
|
|
544
|
-
key={agent.id}
|
|
545
|
-
agent={agent}
|
|
546
|
-
onViewProfile={setViewingProfile}
|
|
547
|
-
onAssignTask={setAssigningTask}
|
|
548
|
-
index={i}
|
|
549
|
-
/>
|
|
851
|
+
<AgentCard key={agent.id} agent={agent} onViewProfile={setViewingProfile} onAssignTask={setAssigningTask} index={i} />
|
|
550
852
|
))
|
|
551
853
|
)}
|
|
552
854
|
</div>
|
|
@@ -554,17 +856,13 @@ export function AgentsPage() {
|
|
|
554
856
|
{/* Profile panel */}
|
|
555
857
|
<AnimatePresence>
|
|
556
858
|
{viewingProfile && (
|
|
557
|
-
<div className="mt-4">
|
|
558
|
-
<AgentProfilePanel agentId={viewingProfile} onClose={() => setViewingProfile(null)} />
|
|
559
|
-
</div>
|
|
859
|
+
<div className="mt-4"><AgentProfilePanel agentId={viewingProfile} onClose={() => setViewingProfile(null)} /></div>
|
|
560
860
|
)}
|
|
561
861
|
</AnimatePresence>
|
|
562
862
|
|
|
563
863
|
{/* Assign task dialog */}
|
|
564
864
|
<AnimatePresence>
|
|
565
|
-
{assigningTask && (
|
|
566
|
-
<AssignTaskDialog agent={assigningTask} onClose={() => setAssigningTask(null)} />
|
|
567
|
-
)}
|
|
865
|
+
{assigningTask && <AssignTaskDialog agent={assigningTask} onClose={() => setAssigningTask(null)} />}
|
|
568
866
|
</AnimatePresence>
|
|
569
867
|
</>
|
|
570
868
|
);
|