@harbinger-ai/harbinger 0.1.1 → 0.1.2
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/chat/actions.js +19 -0
- package/lib/chat/components/app-sidebar.js +17 -1
- package/lib/chat/components/app-sidebar.jsx +19 -1
- package/lib/chat/components/findings-page.js +164 -103
- package/lib/chat/components/findings-page.jsx +156 -101
- package/lib/chat/components/icons.js +22 -0
- package/lib/chat/components/icons.jsx +20 -0
- package/lib/chat/components/index.js +1 -0
- package/lib/chat/components/mission-control.js +490 -0
- package/lib/chat/components/mission-control.jsx +618 -0
- package/lib/chat/components/registry-page.js +267 -133
- package/lib/chat/components/registry-page.jsx +299 -138
- package/lib/chat/components/targets-page.js +269 -200
- package/lib/chat/components/targets-page.jsx +181 -111
- package/package.json +1 -1
|
@@ -1,77 +1,142 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
5
|
+
import { PackageIcon, SearchIcon, SpinnerIcon, CheckIcon, ChevronDownIcon, DownloadIcon, GlobeIcon } from './icons.js';
|
|
6
|
+
import { getCatalog, getInstalledTools, installTool, uninstallTool, toggleTool, installFromGithub, getContainers, spawnContainer, stopContainer } from '../../registry/actions.js';
|
|
6
7
|
|
|
7
8
|
const CATEGORY_COLORS = {
|
|
8
|
-
recon: 'bg-blue-500/10 text-blue-500',
|
|
9
|
-
scanning: 'bg-cyan-500/10 text-cyan-500',
|
|
10
|
-
web: 'bg-orange-500/10 text-orange-500',
|
|
11
|
-
osint: 'bg-green-500/10 text-green-500',
|
|
12
|
-
cloud: 'bg-purple-500/10 text-purple-500',
|
|
13
|
-
credential: 'bg-red-500/10 text-red-500',
|
|
14
|
-
exploitation: 'bg-red-600/10 text-red-600',
|
|
15
|
-
binary: 'bg-gray-500/10 text-gray-500',
|
|
16
|
-
forensics: 'bg-yellow-500/10 text-yellow-500',
|
|
17
|
-
automation: 'bg-indigo-500/10 text-indigo-500',
|
|
18
|
-
custom: 'bg-
|
|
9
|
+
recon: { bg: 'bg-blue-500/10', text: 'text-blue-500', border: 'border-blue-500/20' },
|
|
10
|
+
scanning: { bg: 'bg-cyan-500/10', text: 'text-cyan-500', border: 'border-cyan-500/20' },
|
|
11
|
+
web: { bg: 'bg-orange-500/10', text: 'text-orange-500', border: 'border-orange-500/20' },
|
|
12
|
+
osint: { bg: 'bg-green-500/10', text: 'text-green-500', border: 'border-green-500/20' },
|
|
13
|
+
cloud: { bg: 'bg-purple-500/10', text: 'text-purple-500', border: 'border-purple-500/20' },
|
|
14
|
+
credential: { bg: 'bg-red-500/10', text: 'text-red-500', border: 'border-red-500/20' },
|
|
15
|
+
exploitation: { bg: 'bg-red-600/10', text: 'text-red-600', border: 'border-red-600/20' },
|
|
16
|
+
binary: { bg: 'bg-gray-500/10', text: 'text-gray-500', border: 'border-gray-500/20' },
|
|
17
|
+
forensics: { bg: 'bg-yellow-500/10', text: 'text-yellow-500', border: 'border-yellow-500/20' },
|
|
18
|
+
automation: { bg: 'bg-indigo-500/10', text: 'text-indigo-500', border: 'border-indigo-500/20' },
|
|
19
|
+
custom: { bg: 'bg-white/5', text: 'text-muted-foreground', border: 'border-white/10' },
|
|
19
20
|
};
|
|
20
21
|
|
|
21
|
-
function
|
|
22
|
+
function getCatStyle(cat) {
|
|
23
|
+
return CATEGORY_COLORS[cat] || CATEGORY_COLORS.custom;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function CatalogToolCard({ tool, installed, onInstall, installing, index }) {
|
|
27
|
+
const cat = getCatStyle(tool.category);
|
|
22
28
|
return (
|
|
23
|
-
<div
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
<motion.div
|
|
30
|
+
initial={{ opacity: 0, y: 8 }}
|
|
31
|
+
animate={{ opacity: 1, y: 0 }}
|
|
32
|
+
transition={{ duration: 0.25, delay: index * 0.02 }}
|
|
33
|
+
className="group rounded-lg border border-white/[0.06] bg-[--card] hover:border-[--cyan]/20 transition-colors overflow-hidden"
|
|
34
|
+
>
|
|
35
|
+
<div className="flex items-center gap-3 p-3">
|
|
36
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
37
|
+
<div className="w-2 h-2 rounded-full bg-[#ff5f57]" />
|
|
38
|
+
<div className="w-2 h-2 rounded-full bg-[#febc2e]" />
|
|
39
|
+
<div className="w-2 h-2 rounded-full bg-[#28c840]" />
|
|
40
|
+
</div>
|
|
41
|
+
<div className="flex-1 min-w-0">
|
|
42
|
+
<p className="text-sm font-mono font-medium truncate">{tool.name}</p>
|
|
43
|
+
<p className="text-[11px] text-muted-foreground mt-0.5 line-clamp-1 font-mono">{tool.description}</p>
|
|
44
|
+
</div>
|
|
45
|
+
<span className={`shrink-0 inline-flex rounded-full px-2 py-0.5 text-[10px] font-mono font-medium border ${cat.bg} ${cat.text} ${cat.border}`}>
|
|
46
|
+
{tool.category}
|
|
47
|
+
</span>
|
|
48
|
+
{installed ? (
|
|
49
|
+
<span className="shrink-0 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-[10px] font-mono font-medium">
|
|
50
|
+
<CheckIcon size={10} /> installed
|
|
51
|
+
</span>
|
|
52
|
+
) : (
|
|
53
|
+
<button
|
|
54
|
+
onClick={() => onInstall(tool.id)}
|
|
55
|
+
disabled={installing === tool.id}
|
|
56
|
+
className="shrink-0 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 disabled:opacity-50"
|
|
57
|
+
>
|
|
58
|
+
{installing === tool.id ? <SpinnerIcon size={12} /> : <DownloadIcon size={12} />}
|
|
59
|
+
Install
|
|
60
|
+
</button>
|
|
61
|
+
)}
|
|
28
62
|
</div>
|
|
29
|
-
|
|
30
|
-
{installed ? (
|
|
31
|
-
<span className="shrink-0 inline-flex items-center gap-1 rounded-full bg-green-500/10 text-green-500 px-2 py-0.5 text-[10px] font-medium"><CheckIcon size={10} /> installed</span>
|
|
32
|
-
) : (
|
|
33
|
-
<button
|
|
34
|
-
onClick={() => onInstall(tool.id)}
|
|
35
|
-
disabled={installing === tool.id}
|
|
36
|
-
className="shrink-0 inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium border hover:bg-accent/50 transition-colors disabled:opacity-50"
|
|
37
|
-
>
|
|
38
|
-
{installing === tool.id ? <SpinnerIcon size={12} /> : <DownloadIcon size={12} />}
|
|
39
|
-
Install
|
|
40
|
-
</button>
|
|
41
|
-
)}
|
|
42
|
-
</div>
|
|
63
|
+
</motion.div>
|
|
43
64
|
);
|
|
44
65
|
}
|
|
45
66
|
|
|
46
|
-
function InstalledToolCard({ tool, onUninstall, onToggle, onSpawn }) {
|
|
67
|
+
function InstalledToolCard({ tool, onUninstall, onToggle, onSpawn, index }) {
|
|
47
68
|
const [expanded, setExpanded] = useState(false);
|
|
48
69
|
const disabled = !tool.enabled;
|
|
70
|
+
const cat = getCatStyle(tool.category);
|
|
49
71
|
|
|
50
72
|
return (
|
|
51
|
-
<div
|
|
52
|
-
|
|
53
|
-
|
|
73
|
+
<motion.div
|
|
74
|
+
initial={{ opacity: 0, y: 8 }}
|
|
75
|
+
animate={{ opacity: 1, y: 0 }}
|
|
76
|
+
transition={{ duration: 0.25, delay: index * 0.03 }}
|
|
77
|
+
className={`rounded-lg border border-white/[0.06] bg-[--card] transition-all ${disabled ? 'opacity-50' : 'hover:border-[--cyan]/20'}`}
|
|
78
|
+
>
|
|
79
|
+
<button onClick={() => setExpanded(!expanded)} className="flex items-center gap-3 w-full text-left p-3 hover:bg-white/[0.02] rounded-lg transition-colors">
|
|
80
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
81
|
+
<div className="w-2 h-2 rounded-full bg-[#ff5f57]" />
|
|
82
|
+
<div className="w-2 h-2 rounded-full bg-[#febc2e]" />
|
|
83
|
+
<div className="w-2 h-2 rounded-full bg-[#28c840]" />
|
|
84
|
+
</div>
|
|
54
85
|
<div className="flex-1 min-w-0">
|
|
55
|
-
<p className="text-sm font-medium truncate">{tool.name}</p>
|
|
56
|
-
<p className="text-[11px] text-muted-foreground mt-0.5 truncate">{tool.description}</p>
|
|
86
|
+
<p className="text-sm font-mono font-medium truncate">{tool.name}</p>
|
|
87
|
+
<p className="text-[11px] text-muted-foreground mt-0.5 truncate font-mono">{tool.description}</p>
|
|
57
88
|
</div>
|
|
58
|
-
<span className={`shrink-0 inline-flex rounded-full px-2 py-0.5 text-[10px] font-medium ${
|
|
89
|
+
<span className={`shrink-0 inline-flex rounded-full px-2 py-0.5 text-[10px] font-mono font-medium border ${cat.bg} ${cat.text} ${cat.border}`}>{tool.category}</span>
|
|
59
90
|
<span className={`shrink-0 transition-transform ${expanded ? 'rotate-180' : ''}`}><ChevronDownIcon size={14} /></span>
|
|
60
91
|
</button>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
|
|
93
|
+
<AnimatePresence>
|
|
94
|
+
{expanded && (
|
|
95
|
+
<motion.div
|
|
96
|
+
initial={{ height: 0, opacity: 0 }}
|
|
97
|
+
animate={{ height: 'auto', opacity: 1 }}
|
|
98
|
+
exit={{ height: 0, opacity: 0 }}
|
|
99
|
+
transition={{ duration: 0.2 }}
|
|
100
|
+
className="overflow-hidden"
|
|
101
|
+
>
|
|
102
|
+
<div className="border-t border-white/[0.06] px-4 py-3 flex flex-col gap-2">
|
|
103
|
+
{tool.dockerImage && (
|
|
104
|
+
<div className="flex gap-2 text-xs items-baseline">
|
|
105
|
+
<span className="text-muted-foreground font-mono text-[10px] uppercase tracking-wider w-14 shrink-0">Docker</span>
|
|
106
|
+
<span className="font-mono text-foreground/80">{tool.dockerImage}</span>
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
{tool.installCmd && (
|
|
110
|
+
<div className="flex gap-2 text-xs items-baseline">
|
|
111
|
+
<span className="text-muted-foreground font-mono text-[10px] uppercase tracking-wider w-14 shrink-0">Install</span>
|
|
112
|
+
<code className="font-mono bg-black/30 px-2 py-0.5 rounded text-[11px] text-foreground/80 border border-white/[0.04] break-all">{tool.installCmd}</code>
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
{tool.sourceUrl && (
|
|
116
|
+
<div className="flex gap-2 text-xs items-baseline">
|
|
117
|
+
<span className="text-muted-foreground font-mono text-[10px] uppercase tracking-wider w-14 shrink-0">Source</span>
|
|
118
|
+
<a href={tool.sourceUrl} target="_blank" rel="noopener" className="text-[--cyan] hover:underline truncate font-mono">{tool.sourceUrl}</a>
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
<div className="flex items-center gap-2 mt-2">
|
|
122
|
+
{tool.dockerImage && (
|
|
123
|
+
<button onClick={() => onSpawn(tool.id)} 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">
|
|
124
|
+
Spawn
|
|
125
|
+
</button>
|
|
126
|
+
)}
|
|
127
|
+
<button onClick={() => onToggle(tool.id)} 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] transition-colors">
|
|
128
|
+
{tool.enabled ? 'Disable' : 'Enable'}
|
|
129
|
+
</button>
|
|
130
|
+
<div className="flex-1" />
|
|
131
|
+
<button onClick={() => onUninstall(tool.id)} className="text-xs font-mono text-muted-foreground hover:text-[--destructive] transition-colors">
|
|
132
|
+
Uninstall
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</motion.div>
|
|
137
|
+
)}
|
|
138
|
+
</AnimatePresence>
|
|
139
|
+
</motion.div>
|
|
75
140
|
);
|
|
76
141
|
}
|
|
77
142
|
|
|
@@ -91,17 +156,29 @@ function GithubInstaller({ onInstall }) {
|
|
|
91
156
|
}
|
|
92
157
|
|
|
93
158
|
return (
|
|
94
|
-
<div className="rounded-lg border bg-card p-4 mb-4">
|
|
95
|
-
<
|
|
96
|
-
|
|
159
|
+
<div className="rounded-lg border border-white/[0.06] bg-[--card] p-4 mb-4">
|
|
160
|
+
<div className="flex items-center gap-2 mb-2">
|
|
161
|
+
<GlobeIcon size={14} />
|
|
162
|
+
<p className="text-sm font-mono font-medium text-[--cyan]">Install from GitHub</p>
|
|
163
|
+
</div>
|
|
164
|
+
<p className="text-[11px] text-muted-foreground mb-3 font-mono">Paste a GitHub repository URL to add any tool to your registry.</p>
|
|
97
165
|
<div className="flex gap-2">
|
|
98
|
-
<input
|
|
99
|
-
|
|
166
|
+
<input
|
|
167
|
+
placeholder="https://github.com/owner/repo"
|
|
168
|
+
value={url}
|
|
169
|
+
onChange={e => setUrl(e.target.value)}
|
|
170
|
+
className="flex-1 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"
|
|
171
|
+
/>
|
|
172
|
+
<button
|
|
173
|
+
onClick={handleInstall}
|
|
174
|
+
disabled={loading || !url}
|
|
175
|
+
className="inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium bg-[--cyan] text-[--primary-foreground] hover:opacity-90 disabled:opacity-50 transition-opacity"
|
|
176
|
+
>
|
|
100
177
|
{loading ? <SpinnerIcon size={12} /> : <DownloadIcon size={12} />} Install
|
|
101
178
|
</button>
|
|
102
179
|
</div>
|
|
103
180
|
{result && (
|
|
104
|
-
<p className={`text-xs mt-2 ${result.error ? 'text-destructive' : 'text-green-500'}`}>
|
|
181
|
+
<p className={`text-xs font-mono mt-2 ${result.error ? 'text-[--destructive]' : 'text-green-500'}`}>
|
|
105
182
|
{result.error || `Installed ${result.name}`}
|
|
106
183
|
</p>
|
|
107
184
|
)}
|
|
@@ -117,7 +194,7 @@ export function RegistryPage() {
|
|
|
117
194
|
const [search, setSearch] = useState('');
|
|
118
195
|
const [activeCategory, setActiveCategory] = useState('all');
|
|
119
196
|
const [installing, setInstalling] = useState(null);
|
|
120
|
-
const [tab, setTab] = useState('catalog');
|
|
197
|
+
const [tab, setTab] = useState('catalog');
|
|
121
198
|
|
|
122
199
|
async function load() {
|
|
123
200
|
const [c, i, cont] = await Promise.all([getCatalog(), getInstalledTools(), getContainers()]);
|
|
@@ -130,6 +207,7 @@ export function RegistryPage() {
|
|
|
130
207
|
useEffect(() => { load(); }, []);
|
|
131
208
|
|
|
132
209
|
const installedSlugs = new Set(installed.map(t => t.slug));
|
|
210
|
+
const runningContainers = containers.filter(c => c.status === 'running').length;
|
|
133
211
|
|
|
134
212
|
async function handleInstall(catalogId) {
|
|
135
213
|
setInstalling(catalogId);
|
|
@@ -155,101 +233,184 @@ export function RegistryPage() {
|
|
|
155
233
|
return true;
|
|
156
234
|
});
|
|
157
235
|
|
|
158
|
-
if (loading)
|
|
236
|
+
if (loading) {
|
|
237
|
+
return (
|
|
238
|
+
<div className="flex flex-col gap-3">
|
|
239
|
+
{[...Array(5)].map((_, i) => (
|
|
240
|
+
<div key={i} className="h-14 animate-pulse rounded-lg bg-white/[0.04] border border-white/[0.06]" />
|
|
241
|
+
))}
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const tabs = [
|
|
247
|
+
{ id: 'catalog', label: 'CATALOG', count: catalog.tools.length },
|
|
248
|
+
{ id: 'installed', label: 'INSTALLED', count: installed.length },
|
|
249
|
+
{ id: 'containers', label: 'CONTAINERS', count: runningContainers },
|
|
250
|
+
];
|
|
159
251
|
|
|
160
252
|
return (
|
|
161
253
|
<>
|
|
162
|
-
|
|
254
|
+
{/* Header */}
|
|
255
|
+
<div className="flex items-center justify-between mb-6">
|
|
163
256
|
<div>
|
|
164
|
-
<h1 className="text-2xl font-semibold">Toolbox</h1>
|
|
165
|
-
<p className="text-
|
|
257
|
+
<h1 className="text-2xl font-mono font-semibold text-[--cyan] text-glow-cyan">Toolbox</h1>
|
|
258
|
+
<p className="text-[11px] text-muted-foreground mt-1 font-mono">Security tool registry and container management</p>
|
|
259
|
+
</div>
|
|
260
|
+
<div className="flex items-center gap-2">
|
|
261
|
+
<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">
|
|
262
|
+
{catalog.tools.length} tools
|
|
263
|
+
</span>
|
|
264
|
+
<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">
|
|
265
|
+
{installed.length} installed
|
|
266
|
+
</span>
|
|
267
|
+
{runningContainers > 0 && (
|
|
268
|
+
<span className="inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-[10px] font-mono font-medium bg-orange-500/10 text-orange-500 border border-orange-500/20">
|
|
269
|
+
<span className="w-1.5 h-1.5 rounded-full bg-orange-500 animate-pulse" />
|
|
270
|
+
{runningContainers} running
|
|
271
|
+
</span>
|
|
272
|
+
)}
|
|
166
273
|
</div>
|
|
167
274
|
</div>
|
|
168
275
|
|
|
169
276
|
{/* Tabs */}
|
|
170
|
-
<div className="flex gap-1 border-b border-
|
|
171
|
-
{
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
277
|
+
<div className="flex gap-1 border-b border-white/[0.06] mb-4">
|
|
278
|
+
{tabs.map(t => (
|
|
279
|
+
<button
|
|
280
|
+
key={t.id}
|
|
281
|
+
onClick={() => setTab(t.id)}
|
|
282
|
+
className={`px-4 py-2.5 text-[11px] font-mono font-medium uppercase tracking-wider border-b-2 transition-colors ${
|
|
283
|
+
tab === t.id
|
|
284
|
+
? 'border-[--cyan] text-[--cyan]'
|
|
285
|
+
: 'border-transparent text-muted-foreground hover:text-foreground'
|
|
286
|
+
}`}
|
|
287
|
+
>
|
|
288
|
+
{t.label} <span className="text-muted-foreground">({t.count})</span>
|
|
289
|
+
</button>
|
|
177
290
|
))}
|
|
178
291
|
</div>
|
|
179
292
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
{
|
|
183
|
-
|
|
184
|
-
<div className="
|
|
185
|
-
<
|
|
186
|
-
|
|
293
|
+
<AnimatePresence mode="wait">
|
|
294
|
+
{tab === 'catalog' && (
|
|
295
|
+
<motion.div key="catalog" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>
|
|
296
|
+
{/* Search */}
|
|
297
|
+
<div className="flex flex-col sm:flex-row gap-3 mb-4">
|
|
298
|
+
<div className="relative flex-1">
|
|
299
|
+
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"><SearchIcon size={14} /></div>
|
|
300
|
+
<input
|
|
301
|
+
placeholder="Search tools..."
|
|
302
|
+
value={search}
|
|
303
|
+
onChange={e => setSearch(e.target.value)}
|
|
304
|
+
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"
|
|
305
|
+
/>
|
|
306
|
+
</div>
|
|
187
307
|
</div>
|
|
188
|
-
</div>
|
|
189
|
-
<div className="flex gap-1 mb-4 overflow-x-auto pb-1">
|
|
190
|
-
<button onClick={() => setActiveCategory('all')} className={`shrink-0 px-3 py-1 rounded-full text-xs font-medium transition-colors ${activeCategory === 'all' ? 'bg-foreground text-background' : 'bg-muted text-muted-foreground hover:text-foreground'}`}>All ({catalog.tools.length})</button>
|
|
191
|
-
{catalog.categories.map(c => (
|
|
192
|
-
<button key={c.id} onClick={() => setActiveCategory(c.id)} className={`shrink-0 px-3 py-1 rounded-full text-xs font-medium transition-colors ${activeCategory === c.id ? 'bg-foreground text-background' : 'bg-muted text-muted-foreground hover:text-foreground'}`}>
|
|
193
|
-
{c.name} ({catalog.tools.filter(t => t.category === c.id).length})
|
|
194
|
-
</button>
|
|
195
|
-
))}
|
|
196
|
-
</div>
|
|
197
|
-
|
|
198
|
-
<GithubInstaller onInstall={handleGithubInstall} />
|
|
199
308
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
309
|
+
{/* Category filter */}
|
|
310
|
+
<div className="flex gap-1.5 mb-4 overflow-x-auto pb-1 scrollbar-thin">
|
|
311
|
+
<button
|
|
312
|
+
onClick={() => setActiveCategory('all')}
|
|
313
|
+
className={`shrink-0 px-3 py-1 rounded-full text-[10px] font-mono font-medium border transition-colors ${
|
|
314
|
+
activeCategory === 'all'
|
|
315
|
+
? 'bg-[--cyan]/10 text-[--cyan] border-[--cyan]/20'
|
|
316
|
+
: 'border-white/[0.06] text-muted-foreground hover:text-foreground hover:border-white/[0.12]'
|
|
317
|
+
}`}
|
|
318
|
+
>
|
|
319
|
+
All ({catalog.tools.length})
|
|
320
|
+
</button>
|
|
321
|
+
{catalog.categories.map(c => {
|
|
322
|
+
const cs = getCatStyle(c.id);
|
|
323
|
+
const isActive = activeCategory === c.id;
|
|
324
|
+
return (
|
|
325
|
+
<button
|
|
326
|
+
key={c.id}
|
|
327
|
+
onClick={() => setActiveCategory(c.id)}
|
|
328
|
+
className={`shrink-0 px-3 py-1 rounded-full text-[10px] font-mono font-medium border transition-colors ${
|
|
329
|
+
isActive
|
|
330
|
+
? `${cs.bg} ${cs.text} ${cs.border}`
|
|
331
|
+
: 'border-white/[0.06] text-muted-foreground hover:text-foreground hover:border-white/[0.12]'
|
|
332
|
+
}`}
|
|
333
|
+
>
|
|
334
|
+
{c.name} ({catalog.tools.filter(t => t.category === c.id).length})
|
|
335
|
+
</button>
|
|
336
|
+
);
|
|
337
|
+
})}
|
|
222
338
|
</div>
|
|
223
|
-
)}
|
|
224
|
-
</>
|
|
225
|
-
)}
|
|
226
339
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
{containers.length === 0 ? (
|
|
230
|
-
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
231
|
-
<div className="rounded-full bg-muted p-4 mb-4"><PackageIcon size={24} /></div>
|
|
232
|
-
<p className="text-sm font-medium mb-1">No containers running</p>
|
|
233
|
-
<p className="text-xs text-muted-foreground">Spawn containers from installed tools to give agents terminal access.</p>
|
|
234
|
-
</div>
|
|
235
|
-
) : (
|
|
340
|
+
<GithubInstaller onInstall={handleGithubInstall} />
|
|
341
|
+
|
|
236
342
|
<div className="flex flex-col gap-2">
|
|
237
|
-
{
|
|
238
|
-
<
|
|
239
|
-
<div className={`shrink-0 w-2 h-2 rounded-full ${c.status === 'running' ? 'bg-green-500 animate-pulse' : c.status === 'stopped' ? 'bg-muted-foreground' : 'bg-red-500'}`} />
|
|
240
|
-
<div className="flex-1 min-w-0">
|
|
241
|
-
<p className="text-sm font-mono truncate">{c.imageName}</p>
|
|
242
|
-
<p className="text-[10px] text-muted-foreground mt-0.5">{c.containerId?.slice(0, 12)} — {c.status}</p>
|
|
243
|
-
</div>
|
|
244
|
-
{c.status === 'running' && (
|
|
245
|
-
<button onClick={() => handleStop(c.id)} className="text-xs text-muted-foreground hover:text-destructive">Stop</button>
|
|
246
|
-
)}
|
|
247
|
-
</div>
|
|
343
|
+
{filteredCatalog.map((t, i) => (
|
|
344
|
+
<CatalogToolCard key={t.id} tool={t} installed={installedSlugs.has(t.id)} onInstall={handleInstall} installing={installing} index={i} />
|
|
248
345
|
))}
|
|
346
|
+
{filteredCatalog.length === 0 && (
|
|
347
|
+
<div className="flex flex-col items-center py-12 text-center">
|
|
348
|
+
<div className="rounded-full bg-white/[0.04] border border-white/[0.06] p-4 mb-4"><SearchIcon size={24} /></div>
|
|
349
|
+
<p className="text-sm font-mono text-muted-foreground">No tools match your search.</p>
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
249
352
|
</div>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
353
|
+
</motion.div>
|
|
354
|
+
)}
|
|
355
|
+
|
|
356
|
+
{tab === 'installed' && (
|
|
357
|
+
<motion.div key="installed" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>
|
|
358
|
+
{installed.length === 0 ? (
|
|
359
|
+
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
360
|
+
<div className="rounded-full bg-white/[0.04] border border-white/[0.06] p-4 mb-4"><PackageIcon size={24} /></div>
|
|
361
|
+
<p className="text-sm font-mono font-medium mb-1">No tools installed</p>
|
|
362
|
+
<p className="text-[11px] text-muted-foreground font-mono">Browse the catalog to install security tools for your agents.</p>
|
|
363
|
+
</div>
|
|
364
|
+
) : (
|
|
365
|
+
<div className="flex flex-col gap-2">
|
|
366
|
+
{installed.map((t, i) => <InstalledToolCard key={t.id} tool={t} onUninstall={handleUninstall} onToggle={handleToggle} onSpawn={handleSpawn} index={i} />)}
|
|
367
|
+
</div>
|
|
368
|
+
)}
|
|
369
|
+
</motion.div>
|
|
370
|
+
)}
|
|
371
|
+
|
|
372
|
+
{tab === 'containers' && (
|
|
373
|
+
<motion.div key="containers" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>
|
|
374
|
+
{containers.length === 0 ? (
|
|
375
|
+
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
376
|
+
<div className="rounded-full bg-white/[0.04] border border-white/[0.06] p-4 mb-4"><PackageIcon size={24} /></div>
|
|
377
|
+
<p className="text-sm font-mono font-medium mb-1">No containers running</p>
|
|
378
|
+
<p className="text-[11px] text-muted-foreground font-mono">Spawn containers from installed tools to give agents terminal access.</p>
|
|
379
|
+
</div>
|
|
380
|
+
) : (
|
|
381
|
+
<div className="flex flex-col gap-2">
|
|
382
|
+
{containers.map((c, i) => (
|
|
383
|
+
<motion.div
|
|
384
|
+
key={c.id}
|
|
385
|
+
initial={{ opacity: 0, y: 8 }}
|
|
386
|
+
animate={{ opacity: 1, y: 0 }}
|
|
387
|
+
transition={{ duration: 0.25, delay: i * 0.03 }}
|
|
388
|
+
className="flex items-center gap-3 p-3 rounded-lg border border-white/[0.06] bg-[--card] hover:border-[--cyan]/20 transition-colors"
|
|
389
|
+
>
|
|
390
|
+
<div className={`shrink-0 w-2.5 h-2.5 rounded-full ${
|
|
391
|
+
c.status === 'running' ? 'bg-green-500 animate-pulse' :
|
|
392
|
+
c.status === 'stopped' ? 'bg-muted-foreground' : 'bg-red-500'
|
|
393
|
+
}`} />
|
|
394
|
+
<div className="flex-1 min-w-0">
|
|
395
|
+
<p className="text-sm font-mono font-medium truncate">{c.imageName}</p>
|
|
396
|
+
<div className="flex items-center gap-2 mt-0.5">
|
|
397
|
+
<span className="text-[10px] font-mono text-muted-foreground">{c.containerId?.slice(0, 12)}</span>
|
|
398
|
+
<span className={`inline-flex rounded-full px-1.5 py-0.5 text-[9px] font-mono font-medium ${
|
|
399
|
+
c.status === 'running' ? 'bg-green-500/10 text-green-500 border border-green-500/20' :
|
|
400
|
+
'bg-white/5 text-muted-foreground border border-white/10'
|
|
401
|
+
}`}>{c.status}</span>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
{c.status === 'running' && (
|
|
405
|
+
<button onClick={() => handleStop(c.id)} className="text-xs font-mono text-muted-foreground hover:text-[--destructive] transition-colors">Stop</button>
|
|
406
|
+
)}
|
|
407
|
+
</motion.div>
|
|
408
|
+
))}
|
|
409
|
+
</div>
|
|
410
|
+
)}
|
|
411
|
+
</motion.div>
|
|
412
|
+
)}
|
|
413
|
+
</AnimatePresence>
|
|
253
414
|
</>
|
|
254
415
|
);
|
|
255
416
|
}
|