@harbinger-ai/harbinger 0.1.0 → 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/bin/cli.js +40 -38
- package/bin/local.sh +2 -2
- package/bin/postinstall.js +8 -8
- package/config/index.js +6 -3
- package/config/instrumentation.js +11 -11
- 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/sidebar-user-nav.js +1 -1
- package/lib/chat/components/sidebar-user-nav.jsx +1 -1
- package/lib/chat/components/targets-page.js +269 -200
- package/lib/chat/components/targets-page.jsx +181 -111
- package/lib/chat/components/upgrade-dialog.js +2 -2
- package/lib/chat/components/upgrade-dialog.jsx +2 -2
- package/lib/cron.js +11 -7
- package/lib/db/index.js +6 -1
- package/lib/mcp/actions.js +1 -1
- package/lib/mcp/handler.js +2 -2
- package/lib/mcp/server.js +1 -1
- package/lib/paths.js +1 -1
- package/package.json +1 -1
- package/templates/.env.example +4 -4
- package/templates/.github/workflows/rebuild-event-handler.yml +20 -20
- package/templates/.github/workflows/run-job.yml +6 -6
- package/templates/.github/workflows/upgrade-event-handler.yml +12 -12
- package/templates/CLAUDE.md +3 -3
- package/templates/CLAUDE.md.template +9 -9
- package/templates/app/api/[...thepopebot]/route.js +1 -1
- package/templates/app/api/auth/[...nextauth]/route.js +1 -1
- package/templates/app/chat/[chatId]/page.js +2 -2
- package/templates/app/chats/page.js +2 -2
- package/templates/app/components/setup-form.jsx +1 -1
- package/templates/app/findings/page.js +2 -2
- package/templates/app/globals.css +1 -1
- package/templates/app/layout.js +1 -1
- package/templates/app/login/page.js +1 -1
- package/templates/app/notifications/page.js +2 -2
- package/templates/app/page.js +2 -2
- package/templates/app/settings/crons/page.js +1 -1
- package/templates/app/settings/layout.js +2 -2
- package/templates/app/settings/mcp/page.js +1 -1
- package/templates/app/settings/secrets/page.js +1 -1
- package/templates/app/settings/triggers/page.js +1 -1
- package/templates/app/stream/chat/route.js +1 -1
- package/templates/app/swarm/page.js +2 -2
- package/templates/app/targets/page.js +2 -2
- package/templates/app/toolbox/page.js +2 -2
- package/templates/config/AGENT.md +2 -2
- package/templates/config/EVENT_HANDLER.md +3 -3
- package/templates/config/SKILL_BUILDING_GUIDE.md +1 -1
- package/templates/config/SOUL.md +1 -1
- package/templates/docker/event-handler/Dockerfile +1 -1
- package/templates/docker-compose.yml +2 -2
- package/templates/instrumentation.js +1 -1
- package/templates/middleware.js +1 -1
- package/templates/next.config.mjs +2 -2
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
4
5
|
import { CrosshairIcon, PlusIcon, TrashIcon, ChevronDownIcon, GlobeIcon, SpinnerIcon, DownloadIcon, CheckIcon } from './icons.js';
|
|
5
6
|
import { getPrograms, createProgram, deleteProgram, getTargets, createTarget, deleteTarget, updateTarget, syncTargetsFromPlatform, syncAllTargets, getSyncStatus } from '../../bounty/actions.js';
|
|
6
7
|
|
|
7
8
|
const PLATFORMS = [
|
|
8
|
-
{ id: 'hackerone', label: 'HackerOne',
|
|
9
|
-
{ id: 'bugcrowd', label: 'Bugcrowd',
|
|
10
|
-
{ id: 'intigriti', label: 'Intigriti',
|
|
11
|
-
{ id: 'yeswehack', label: 'YesWeHack',
|
|
12
|
-
{ id: 'federacy', label: 'Federacy',
|
|
13
|
-
{ id: 'custom', label: 'Custom',
|
|
9
|
+
{ id: 'hackerone', label: 'HackerOne', bg: 'bg-purple-500/10', text: 'text-purple-500', border: 'border-purple-500/20' },
|
|
10
|
+
{ id: 'bugcrowd', label: 'Bugcrowd', bg: 'bg-orange-500/10', text: 'text-orange-500', border: 'border-orange-500/20' },
|
|
11
|
+
{ id: 'intigriti', label: 'Intigriti', bg: 'bg-blue-500/10', text: 'text-blue-500', border: 'border-blue-500/20' },
|
|
12
|
+
{ id: 'yeswehack', label: 'YesWeHack', bg: 'bg-teal-500/10', text: 'text-teal-500', border: 'border-teal-500/20' },
|
|
13
|
+
{ id: 'federacy', label: 'Federacy', bg: 'bg-pink-500/10', text: 'text-pink-500', border: 'border-pink-500/20' },
|
|
14
|
+
{ id: 'custom', label: 'Custom', bg: 'bg-white/5', text: 'text-muted-foreground', border: 'border-white/10' },
|
|
14
15
|
];
|
|
15
16
|
|
|
16
17
|
const TARGET_TYPES = ['domain', 'wildcard', 'ip', 'cidr', 'url', 'api', 'mobile'];
|
|
17
18
|
const STATUS_COLORS = {
|
|
18
|
-
in_scope: 'bg-green-500/10 text-green-500',
|
|
19
|
-
out_of_scope: 'bg-red-500/10 text-red-500',
|
|
20
|
-
testing: 'bg-yellow-500/10 text-yellow-500',
|
|
21
|
-
completed: 'bg-blue-500/10 text-blue-500',
|
|
19
|
+
in_scope: { bg: 'bg-green-500/10', text: 'text-green-500', border: 'border-green-500/20', dot: 'bg-green-500' },
|
|
20
|
+
out_of_scope: { bg: 'bg-red-500/10', text: 'text-red-500', border: 'border-red-500/20', dot: 'bg-red-500' },
|
|
21
|
+
testing: { bg: 'bg-yellow-500/10', text: 'text-yellow-500', border: 'border-yellow-500/20', dot: 'bg-yellow-500 animate-pulse' },
|
|
22
|
+
completed: { bg: 'bg-blue-500/10', text: 'text-blue-500', border: 'border-blue-500/20', dot: 'bg-blue-500' },
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
function timeAgo(ts) {
|
|
@@ -31,8 +32,12 @@ function timeAgo(ts) {
|
|
|
31
32
|
return `${Math.floor(hrs / 24)}d ago`;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
function getPlatStyle(platformId) {
|
|
36
|
+
return PLATFORMS.find(p => p.id === platformId) || PLATFORMS[5];
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
function SyncPanel({ onSync, syncStatus }) {
|
|
35
|
-
const [syncing, setSyncing] = useState(null);
|
|
40
|
+
const [syncing, setSyncing] = useState(null);
|
|
36
41
|
const [results, setResults] = useState(null);
|
|
37
42
|
const [maxPrograms, setMaxPrograms] = useState('50');
|
|
38
43
|
|
|
@@ -57,20 +62,27 @@ function SyncPanel({ onSync, syncStatus }) {
|
|
|
57
62
|
const syncablePlatforms = PLATFORMS.filter(p => p.id !== 'custom');
|
|
58
63
|
|
|
59
64
|
return (
|
|
60
|
-
<div
|
|
65
|
+
<motion.div
|
|
66
|
+
initial={{ opacity: 0, y: 8 }}
|
|
67
|
+
animate={{ opacity: 1, y: 0 }}
|
|
68
|
+
transition={{ duration: 0.3 }}
|
|
69
|
+
className="rounded-lg border border-white/[0.06] bg-[--card] p-4 mb-6"
|
|
70
|
+
>
|
|
61
71
|
<div className="flex items-center justify-between mb-3">
|
|
62
72
|
<div>
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
<div className="flex items-center gap-2 mb-0.5">
|
|
74
|
+
<DownloadIcon size={14} />
|
|
75
|
+
<h3 className="text-sm font-mono font-medium text-[--cyan]">Sync from Bounty Platforms</h3>
|
|
76
|
+
</div>
|
|
77
|
+
<p className="text-[11px] text-muted-foreground font-mono">
|
|
78
|
+
Import programs and targets from bounty-targets-data
|
|
66
79
|
</p>
|
|
67
80
|
</div>
|
|
68
81
|
{syncStatus?.lastSyncedAt && (
|
|
69
|
-
<span className="text-[10px] text-muted-foreground">Last sync: {timeAgo(syncStatus.lastSyncedAt)}</span>
|
|
82
|
+
<span className="text-[10px] text-muted-foreground font-mono">Last sync: {timeAgo(syncStatus.lastSyncedAt)}</span>
|
|
70
83
|
)}
|
|
71
84
|
</div>
|
|
72
85
|
|
|
73
|
-
{/* Platform sync buttons */}
|
|
74
86
|
<div className="flex flex-wrap gap-2 mb-3">
|
|
75
87
|
{syncablePlatforms.map(p => {
|
|
76
88
|
const count = syncStatus?.platformCounts?.[p.id] || 0;
|
|
@@ -79,125 +91,148 @@ function SyncPanel({ onSync, syncStatus }) {
|
|
|
79
91
|
key={p.id}
|
|
80
92
|
onClick={() => handleSync(p.id)}
|
|
81
93
|
disabled={syncing !== null}
|
|
82
|
-
className={`inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium border transition-colors hover:bg-
|
|
94
|
+
className={`inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-mono font-medium border transition-colors hover:bg-white/[0.04] disabled:opacity-50 ${p.border}`}
|
|
83
95
|
>
|
|
84
96
|
{syncing === p.id ? <SpinnerIcon size={12} /> : <DownloadIcon size={12} />}
|
|
85
97
|
{p.label}
|
|
86
|
-
{count > 0 && <span className={`inline-flex rounded-full px-1.5 py-0.5 text-[9px] ${p.
|
|
98
|
+
{count > 0 && <span className={`inline-flex rounded-full px-1.5 py-0.5 text-[9px] font-mono ${p.bg} ${p.text}`}>{count}</span>}
|
|
87
99
|
</button>
|
|
88
100
|
);
|
|
89
101
|
})}
|
|
90
102
|
<button
|
|
91
103
|
onClick={() => handleSync('all')}
|
|
92
104
|
disabled={syncing !== null}
|
|
93
|
-
className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium bg-
|
|
105
|
+
className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-mono font-medium bg-[--cyan] text-[--primary-foreground] hover:opacity-90 disabled:opacity-50 transition-opacity"
|
|
94
106
|
>
|
|
95
107
|
{syncing === 'all' ? <SpinnerIcon size={12} /> : <DownloadIcon size={12} />}
|
|
96
108
|
Sync All
|
|
97
109
|
</button>
|
|
98
110
|
</div>
|
|
99
111
|
|
|
100
|
-
{/* Max programs limit */}
|
|
101
112
|
<div className="flex items-center gap-2 mb-3">
|
|
102
|
-
<label className="text-[10px] text-muted-foreground">Max programs
|
|
113
|
+
<label className="text-[10px] text-muted-foreground font-mono uppercase tracking-wider">Max programs:</label>
|
|
103
114
|
<input
|
|
104
115
|
type="number"
|
|
105
116
|
value={maxPrograms}
|
|
106
117
|
onChange={e => setMaxPrograms(e.target.value)}
|
|
107
|
-
className="w-20 text-xs border rounded-md px-2 py-1 bg-
|
|
118
|
+
className="w-20 text-xs border border-white/[0.06] rounded-md px-2 py-1 bg-black/20 font-mono focus:outline-none focus:border-[--cyan]/40 transition-colors"
|
|
108
119
|
min="0"
|
|
109
120
|
placeholder="0 = all"
|
|
110
121
|
/>
|
|
111
|
-
<span className="text-[10px] text-muted-foreground">(0 = unlimited)</span>
|
|
122
|
+
<span className="text-[10px] text-muted-foreground font-mono">(0 = unlimited)</span>
|
|
112
123
|
</div>
|
|
113
124
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
<AnimatePresence>
|
|
126
|
+
{results && (
|
|
127
|
+
<motion.div
|
|
128
|
+
initial={{ opacity: 0, height: 0 }}
|
|
129
|
+
animate={{ opacity: 1, height: 'auto' }}
|
|
130
|
+
exit={{ opacity: 0, height: 0 }}
|
|
131
|
+
className="overflow-hidden"
|
|
132
|
+
>
|
|
133
|
+
<div className="rounded-md bg-black/30 border border-white/[0.04] p-3 mt-2">
|
|
134
|
+
{results.error ? (
|
|
135
|
+
<p className="text-xs font-mono text-[--destructive]">{results.error}</p>
|
|
136
|
+
) : (
|
|
137
|
+
<div className="flex flex-col gap-1">
|
|
138
|
+
{Object.entries(results).map(([platform, stats]) => (
|
|
139
|
+
<div key={platform} className="flex items-center gap-3 text-xs font-mono">
|
|
140
|
+
<span className="font-medium w-24 text-foreground/80">{platform}</span>
|
|
141
|
+
<span className="text-green-500">+{stats.programsAdded} programs</span>
|
|
142
|
+
<span className="text-blue-500">{stats.programsUpdated} updated</span>
|
|
143
|
+
<span className="text-green-500">+{stats.targetsAdded} targets</span>
|
|
144
|
+
<span className="text-muted-foreground">{stats.targetsSkipped} skipped</span>
|
|
145
|
+
{stats.errors?.length > 0 && <span className="text-[--destructive]">{stats.errors.length} errors</span>}
|
|
146
|
+
</div>
|
|
147
|
+
))}
|
|
129
148
|
</div>
|
|
130
|
-
)
|
|
149
|
+
)}
|
|
131
150
|
</div>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
151
|
+
</motion.div>
|
|
152
|
+
)}
|
|
153
|
+
</AnimatePresence>
|
|
135
154
|
|
|
136
|
-
{/* Sync status summary */}
|
|
137
155
|
{syncStatus && syncStatus.totalSyncedPrograms > 0 && (
|
|
138
|
-
<div className="flex items-center gap-3 mt-
|
|
139
|
-
<span className="text-[10px] text-muted-foreground">{syncStatus.totalSyncedPrograms} synced
|
|
156
|
+
<div className="flex items-center gap-3 mt-3 pt-3 border-t border-white/[0.06]">
|
|
157
|
+
<span className="text-[10px] text-muted-foreground font-mono">{syncStatus.totalSyncedPrograms} synced:</span>
|
|
140
158
|
{Object.entries(syncStatus.platformCounts || {}).map(([p, count]) => {
|
|
141
|
-
const plat =
|
|
159
|
+
const plat = getPlatStyle(p);
|
|
142
160
|
return (
|
|
143
|
-
<span key={p} className={`inline-flex rounded-full px-2 py-0.5 text-[9px] font-medium ${plat
|
|
144
|
-
{plat
|
|
161
|
+
<span key={p} className={`inline-flex rounded-full px-2 py-0.5 text-[9px] font-mono font-medium border ${plat.bg} ${plat.text} ${plat.border}`}>
|
|
162
|
+
{plat.label} ({count})
|
|
145
163
|
</span>
|
|
146
164
|
);
|
|
147
165
|
})}
|
|
148
166
|
</div>
|
|
149
167
|
)}
|
|
150
|
-
</div>
|
|
168
|
+
</motion.div>
|
|
151
169
|
);
|
|
152
170
|
}
|
|
153
171
|
|
|
154
|
-
function ProgramCard({ program, onSelect, selected, onDelete }) {
|
|
155
|
-
const
|
|
172
|
+
function ProgramCard({ program, onSelect, selected, onDelete, index }) {
|
|
173
|
+
const plat = getPlatStyle(program.platform);
|
|
156
174
|
return (
|
|
157
|
-
<button
|
|
175
|
+
<motion.button
|
|
176
|
+
initial={{ opacity: 0, x: -8 }}
|
|
177
|
+
animate={{ opacity: 1, x: 0 }}
|
|
178
|
+
transition={{ duration: 0.25, delay: index * 0.02 }}
|
|
158
179
|
onClick={() => onSelect(program.id)}
|
|
159
|
-
className={`flex items-center gap-3 w-full text-left p-3 rounded-lg border transition-
|
|
180
|
+
className={`flex items-center gap-3 w-full text-left p-3 rounded-lg border transition-all ${
|
|
181
|
+
selected
|
|
182
|
+
? 'border-[--cyan]/40 bg-[--cyan]/5 glow-cyan'
|
|
183
|
+
: 'border-white/[0.06] bg-[--card] hover:border-[--cyan]/20'
|
|
184
|
+
}`}
|
|
160
185
|
>
|
|
161
|
-
<div className="
|
|
186
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
187
|
+
<div className="w-2 h-2 rounded-full bg-[#ff5f57]" />
|
|
188
|
+
<div className="w-2 h-2 rounded-full bg-[#febc2e]" />
|
|
189
|
+
<div className="w-2 h-2 rounded-full bg-[#28c840]" />
|
|
190
|
+
</div>
|
|
162
191
|
<div className="flex-1 min-w-0">
|
|
163
|
-
<p className="text-sm font-medium truncate">{program.name}</p>
|
|
192
|
+
<p className="text-sm font-mono font-medium truncate">{program.name}</p>
|
|
164
193
|
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
|
|
165
|
-
<span className={`inline-flex rounded-full px-2 py-0.5 text-[10px] font-medium ${
|
|
166
|
-
{program.maxBounty > 0 && <span className="text-[10px] text-
|
|
167
|
-
{program.syncHandle && <span className="inline-flex rounded-full bg-cyan
|
|
194
|
+
<span className={`inline-flex rounded-full px-2 py-0.5 text-[10px] font-mono font-medium border ${plat.bg} ${plat.text} ${plat.border}`}>{plat.label}</span>
|
|
195
|
+
{program.maxBounty > 0 && <span className="text-[10px] font-mono text-green-500">Up to ${program.maxBounty.toLocaleString()}</span>}
|
|
196
|
+
{program.syncHandle && <span className="inline-flex rounded-full bg-[--cyan]/10 border border-[--cyan]/20 px-1.5 py-0.5 text-[9px] font-mono text-[--cyan]">synced</span>}
|
|
168
197
|
</div>
|
|
169
198
|
</div>
|
|
170
199
|
{!program.syncHandle && (
|
|
171
|
-
<button onClick={(e) => { e.stopPropagation(); onDelete(program.id); }} className="shrink-0 p-1 text-muted-foreground hover:text-destructive rounded"><TrashIcon size={12} /></button>
|
|
200
|
+
<button onClick={(e) => { e.stopPropagation(); onDelete(program.id); }} className="shrink-0 p-1 text-muted-foreground hover:text-[--destructive] rounded transition-colors"><TrashIcon size={12} /></button>
|
|
172
201
|
)}
|
|
173
|
-
</button>
|
|
202
|
+
</motion.button>
|
|
174
203
|
);
|
|
175
204
|
}
|
|
176
205
|
|
|
177
|
-
function TargetRow({ target, onDelete, onStatusChange }) {
|
|
206
|
+
function TargetRow({ target, onDelete, onStatusChange, index }) {
|
|
207
|
+
const st = STATUS_COLORS[target.status] || STATUS_COLORS.in_scope;
|
|
178
208
|
return (
|
|
179
|
-
<div
|
|
180
|
-
|
|
209
|
+
<motion.div
|
|
210
|
+
initial={{ opacity: 0, y: 6 }}
|
|
211
|
+
animate={{ opacity: 1, y: 0 }}
|
|
212
|
+
transition={{ duration: 0.2, delay: index * 0.02 }}
|
|
213
|
+
className="flex items-center gap-3 p-3 rounded-lg border border-white/[0.06] bg-[--card] hover:border-[--cyan]/20 transition-colors"
|
|
214
|
+
>
|
|
215
|
+
<div className={`shrink-0 w-2 h-2 rounded-full ${st.dot}`} />
|
|
181
216
|
<div className="flex-1 min-w-0">
|
|
182
217
|
<p className="text-sm font-mono font-medium truncate">{target.value}</p>
|
|
183
218
|
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
|
|
184
|
-
<span className="inline-flex rounded-full bg-
|
|
185
|
-
{target.syncSource && <span className="inline-flex rounded-full bg-cyan
|
|
186
|
-
{target.technologies && <span className="text-[10px] text-muted-foreground truncate">{JSON.parse(target.technologies).join(', ')}</span>}
|
|
219
|
+
<span className="inline-flex rounded-full bg-white/5 border border-white/10 px-2 py-0.5 text-[10px] font-mono font-medium text-muted-foreground">{target.type}</span>
|
|
220
|
+
{target.syncSource && <span className="inline-flex rounded-full bg-[--cyan]/10 border border-[--cyan]/20 px-1.5 py-0.5 text-[9px] font-mono text-[--cyan]">{target.syncSource}</span>}
|
|
221
|
+
{target.technologies && <span className="text-[10px] font-mono text-muted-foreground truncate">{JSON.parse(target.technologies).join(', ')}</span>}
|
|
187
222
|
</div>
|
|
188
223
|
</div>
|
|
189
224
|
<select
|
|
190
225
|
value={target.status}
|
|
191
226
|
onChange={(e) => onStatusChange(target.id, e.target.value)}
|
|
192
|
-
className={`text-[10px] font-medium rounded-full px-2 py-0.5 border
|
|
227
|
+
className={`text-[10px] font-mono font-medium rounded-full px-2 py-0.5 border cursor-pointer bg-transparent ${st.bg} ${st.text} ${st.border} focus:outline-none`}
|
|
193
228
|
>
|
|
194
229
|
<option value="in_scope">in scope</option>
|
|
195
230
|
<option value="testing">testing</option>
|
|
196
231
|
<option value="completed">completed</option>
|
|
197
232
|
<option value="out_of_scope">out of scope</option>
|
|
198
233
|
</select>
|
|
199
|
-
<button onClick={() => onDelete(target.id)} className="shrink-0 p-1 text-muted-foreground hover:text-destructive rounded"><TrashIcon size={12} /></button>
|
|
200
|
-
</div>
|
|
234
|
+
<button onClick={() => onDelete(target.id)} className="shrink-0 p-1 text-muted-foreground hover:text-[--destructive] rounded transition-colors"><TrashIcon size={12} /></button>
|
|
235
|
+
</motion.div>
|
|
201
236
|
);
|
|
202
237
|
}
|
|
203
238
|
|
|
@@ -206,28 +241,32 @@ function AddForm({ fields, onSubmit }) {
|
|
|
206
241
|
const [open, setOpen] = useState(false);
|
|
207
242
|
|
|
208
243
|
if (!open) return (
|
|
209
|
-
<button onClick={() => setOpen(true)} className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium border border-dashed hover:bg-
|
|
244
|
+
<button onClick={() => setOpen(true)} 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">
|
|
210
245
|
<PlusIcon size={12} /> Add
|
|
211
246
|
</button>
|
|
212
247
|
);
|
|
213
248
|
|
|
214
249
|
return (
|
|
215
|
-
<div
|
|
250
|
+
<motion.div
|
|
251
|
+
initial={{ opacity: 0, scale: 0.98 }}
|
|
252
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
253
|
+
className="flex flex-wrap items-end gap-2 p-3 rounded-lg border border-white/[0.06] bg-[--card]"
|
|
254
|
+
>
|
|
216
255
|
{fields.map(f => (
|
|
217
256
|
<div key={f.name} className="flex flex-col gap-1">
|
|
218
|
-
<label className="text-[10px] text-muted-foreground">{f.label}</label>
|
|
257
|
+
<label className="text-[10px] text-muted-foreground font-mono uppercase tracking-wider">{f.label}</label>
|
|
219
258
|
{f.type === 'select' ? (
|
|
220
|
-
<select value={values[f.name] || ''} onChange={e => setValues({ ...values, [f.name]: e.target.value })} className="text-xs border rounded-md px-2 py-1.5 bg-
|
|
259
|
+
<select value={values[f.name] || ''} onChange={e => setValues({ ...values, [f.name]: e.target.value })} className="text-xs border border-white/[0.06] rounded-md px-2 py-1.5 bg-black/20 font-mono focus:outline-none focus:border-[--cyan]/40 transition-colors">
|
|
221
260
|
{f.options.map(o => <option key={o} value={o}>{o}</option>)}
|
|
222
261
|
</select>
|
|
223
262
|
) : (
|
|
224
|
-
<input type={f.type || 'text'} placeholder={f.placeholder} value={values[f.name] || ''} onChange={e => setValues({ ...values, [f.name]: e.target.value })} className="text-xs border rounded-md px-2 py-1.5 bg-
|
|
263
|
+
<input type={f.type || 'text'} placeholder={f.placeholder} value={values[f.name] || ''} onChange={e => setValues({ ...values, [f.name]: e.target.value })} className="text-xs border border-white/[0.06] rounded-md px-2 py-1.5 bg-black/20 font-mono min-w-[120px] placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 transition-colors" />
|
|
225
264
|
)}
|
|
226
265
|
</div>
|
|
227
266
|
))}
|
|
228
|
-
<button onClick={() => { onSubmit(values); setValues({}); setOpen(false); }} className="inline-flex items-center gap-1 rounded-md px-3 py-1.5 text-xs font-medium bg-
|
|
229
|
-
<button onClick={() => { setValues({}); setOpen(false); }} className="text-xs text-muted-foreground hover:text-foreground px-2 py-1.5">Cancel</button>
|
|
230
|
-
</div>
|
|
267
|
+
<button onClick={() => { onSubmit(values); setValues({}); setOpen(false); }} className="inline-flex items-center gap-1 rounded-md px-3 py-1.5 text-xs font-mono font-medium bg-[--cyan] text-[--primary-foreground] hover:opacity-90 transition-opacity">Save</button>
|
|
268
|
+
<button onClick={() => { setValues({}); setOpen(false); }} className="text-xs font-mono text-muted-foreground hover:text-foreground px-2 py-1.5 transition-colors">Cancel</button>
|
|
269
|
+
</motion.div>
|
|
231
270
|
);
|
|
232
271
|
}
|
|
233
272
|
|
|
@@ -299,25 +338,52 @@ export function TargetsPage() {
|
|
|
299
338
|
? programs_
|
|
300
339
|
: programs_.filter(p => p.platform === platformFilter);
|
|
301
340
|
|
|
302
|
-
|
|
341
|
+
const inScopeCount = targets_.filter(t => t.status === 'in_scope').length;
|
|
342
|
+
|
|
343
|
+
if (loading) return <div className="flex flex-col gap-3">{[...Array(3)].map((_, i) => <div key={i} className="h-16 animate-pulse rounded-lg bg-white/[0.04] border border-white/[0.06]" />)}</div>;
|
|
303
344
|
|
|
304
345
|
return (
|
|
305
346
|
<>
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
<
|
|
347
|
+
{/* Header */}
|
|
348
|
+
<div className="flex items-center justify-between mb-6">
|
|
349
|
+
<div>
|
|
350
|
+
<h1 className="text-2xl font-mono font-semibold text-[--cyan] text-glow-cyan">Targets</h1>
|
|
351
|
+
<p className="text-[11px] text-muted-foreground mt-1 font-mono">Bug bounty program and target management</p>
|
|
352
|
+
</div>
|
|
353
|
+
<div className="flex items-center gap-2">
|
|
354
|
+
<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">
|
|
355
|
+
{programs_.length} programs
|
|
356
|
+
</span>
|
|
357
|
+
<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">
|
|
358
|
+
{inScopeCount} in scope
|
|
359
|
+
</span>
|
|
360
|
+
</div>
|
|
309
361
|
</div>
|
|
310
362
|
|
|
311
|
-
{/* Sync Panel */}
|
|
312
363
|
<SyncPanel onSync={handleSync} syncStatus={syncStatus} />
|
|
313
364
|
|
|
314
365
|
{/* Platform filter tabs */}
|
|
315
|
-
<div className="flex gap-1 mb-4 overflow-x-auto pb-1">
|
|
316
|
-
<button
|
|
366
|
+
<div className="flex gap-1.5 mb-4 overflow-x-auto pb-1 scrollbar-thin">
|
|
367
|
+
<button
|
|
368
|
+
onClick={() => setPlatformFilter('all')}
|
|
369
|
+
className={`shrink-0 px-3 py-1 rounded-full text-[10px] font-mono font-medium border transition-colors ${
|
|
370
|
+
platformFilter === 'all'
|
|
371
|
+
? 'bg-[--cyan]/10 text-[--cyan] border-[--cyan]/20'
|
|
372
|
+
: 'border-white/[0.06] text-muted-foreground hover:text-foreground hover:border-white/[0.12]'
|
|
373
|
+
}`}
|
|
374
|
+
>
|
|
317
375
|
All ({programs_.length})
|
|
318
376
|
</button>
|
|
319
377
|
{PLATFORMS.filter(p => programs_.some(prog => prog.platform === p.id)).map(p => (
|
|
320
|
-
<button
|
|
378
|
+
<button
|
|
379
|
+
key={p.id}
|
|
380
|
+
onClick={() => setPlatformFilter(p.id)}
|
|
381
|
+
className={`shrink-0 px-3 py-1 rounded-full text-[10px] font-mono font-medium border transition-colors ${
|
|
382
|
+
platformFilter === p.id
|
|
383
|
+
? `${p.bg} ${p.text} ${p.border}`
|
|
384
|
+
: 'border-white/[0.06] text-muted-foreground hover:text-foreground hover:border-white/[0.12]'
|
|
385
|
+
}`}
|
|
386
|
+
>
|
|
321
387
|
{p.label} ({programs_.filter(prog => prog.platform === p.id).length})
|
|
322
388
|
</button>
|
|
323
389
|
))}
|
|
@@ -327,7 +393,7 @@ export function TargetsPage() {
|
|
|
327
393
|
{/* Programs panel */}
|
|
328
394
|
<div className="lg:col-span-1">
|
|
329
395
|
<div className="flex items-center justify-between mb-3">
|
|
330
|
-
<h2 className="text-
|
|
396
|
+
<h2 className="text-[10px] font-mono font-medium uppercase tracking-wider text-muted-foreground">Programs</h2>
|
|
331
397
|
<AddForm
|
|
332
398
|
fields={[
|
|
333
399
|
{ name: 'name', label: 'Name', placeholder: 'Program name' },
|
|
@@ -338,15 +404,15 @@ export function TargetsPage() {
|
|
|
338
404
|
onSubmit={handleAddProgram}
|
|
339
405
|
/>
|
|
340
406
|
</div>
|
|
341
|
-
<div className="flex flex-col gap-2 max-h-[600px] overflow-y-auto">
|
|
407
|
+
<div className="flex flex-col gap-2 max-h-[600px] overflow-y-auto scrollbar-thin">
|
|
342
408
|
{filteredPrograms.length === 0 ? (
|
|
343
409
|
<div className="flex flex-col items-center py-8 text-center">
|
|
344
|
-
<div className="rounded-full bg-
|
|
345
|
-
<p className="text-xs text-muted-foreground">No programs yet</p>
|
|
346
|
-
<p className="text-[10px] text-muted-foreground mt-1">Sync from platforms or add manually</p>
|
|
410
|
+
<div className="rounded-full bg-white/[0.04] border border-white/[0.06] p-3 mb-3"><GlobeIcon size={20} /></div>
|
|
411
|
+
<p className="text-xs font-mono text-muted-foreground">No programs yet</p>
|
|
412
|
+
<p className="text-[10px] font-mono text-muted-foreground mt-1">Sync from platforms or add manually</p>
|
|
347
413
|
</div>
|
|
348
|
-
) : filteredPrograms.map(p => (
|
|
349
|
-
<ProgramCard key={p.id} program={p} selected={selectedProgram === p.id} onSelect={setSelectedProgram} onDelete={handleDeleteProgram} />
|
|
414
|
+
) : filteredPrograms.map((p, i) => (
|
|
415
|
+
<ProgramCard key={p.id} program={p} selected={selectedProgram === p.id} onSelect={setSelectedProgram} onDelete={handleDeleteProgram} index={i} />
|
|
350
416
|
))}
|
|
351
417
|
</div>
|
|
352
418
|
</div>
|
|
@@ -354,7 +420,9 @@ export function TargetsPage() {
|
|
|
354
420
|
{/* Targets panel */}
|
|
355
421
|
<div className="lg:col-span-2">
|
|
356
422
|
<div className="flex items-center justify-between mb-3">
|
|
357
|
-
<h2 className="text-
|
|
423
|
+
<h2 className="text-[10px] font-mono font-medium uppercase tracking-wider text-muted-foreground">
|
|
424
|
+
Targets {selectedProgram && `\u2014 ${programs_.find(p => p.id === selectedProgram)?.name || ''}`}
|
|
425
|
+
</h2>
|
|
358
426
|
{selectedProgram && (
|
|
359
427
|
<AddForm
|
|
360
428
|
fields={[
|
|
@@ -365,23 +433,25 @@ export function TargetsPage() {
|
|
|
365
433
|
/>
|
|
366
434
|
)}
|
|
367
435
|
</div>
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
<div className="
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
<div className="
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
436
|
+
<AnimatePresence mode="wait">
|
|
437
|
+
{!selectedProgram ? (
|
|
438
|
+
<motion.div key="empty" initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="flex flex-col items-center py-12 text-center">
|
|
439
|
+
<div className="rounded-full bg-white/[0.04] border border-white/[0.06] p-4 mb-4"><CrosshairIcon size={24} /></div>
|
|
440
|
+
<p className="text-sm font-mono font-medium mb-1">Select a program</p>
|
|
441
|
+
<p className="text-[11px] font-mono text-muted-foreground">Choose a program to manage its targets</p>
|
|
442
|
+
</motion.div>
|
|
443
|
+
) : targets_.length === 0 ? (
|
|
444
|
+
<motion.div key="no-targets" initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="flex flex-col items-center py-12 text-center">
|
|
445
|
+
<div className="rounded-full bg-white/[0.04] border border-white/[0.06] p-4 mb-4"><CrosshairIcon size={24} /></div>
|
|
446
|
+
<p className="text-sm font-mono font-medium mb-1">No targets yet</p>
|
|
447
|
+
<p className="text-[11px] font-mono text-muted-foreground">Add domains, IPs, and URLs to start hunting</p>
|
|
448
|
+
</motion.div>
|
|
449
|
+
) : (
|
|
450
|
+
<motion.div key="targets" initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="flex flex-col gap-2 max-h-[600px] overflow-y-auto scrollbar-thin">
|
|
451
|
+
{targets_.map((t, i) => <TargetRow key={t.id} target={t} onDelete={handleDeleteTarget} onStatusChange={handleStatusChange} index={i} />)}
|
|
452
|
+
</motion.div>
|
|
453
|
+
)}
|
|
454
|
+
</AnimatePresence>
|
|
385
455
|
</div>
|
|
386
456
|
</div>
|
|
387
457
|
</>
|
|
@@ -71,7 +71,7 @@ function UpgradeDialog({ open, onClose, version, updateAvailable, changelog }) {
|
|
|
71
71
|
/* @__PURE__ */ jsx(
|
|
72
72
|
"a",
|
|
73
73
|
{
|
|
74
|
-
href: "https://github.com/
|
|
74
|
+
href: "https://github.com/Haribinger/bot?tab=readme-ov-file#understanding-init",
|
|
75
75
|
target: "_blank",
|
|
76
76
|
rel: "noopener noreferrer",
|
|
77
77
|
className: "text-emerald-500 hover:underline",
|
|
@@ -87,7 +87,7 @@ function UpgradeDialog({ open, onClose, version, updateAvailable, changelog }) {
|
|
|
87
87
|
/* @__PURE__ */ jsx(
|
|
88
88
|
"a",
|
|
89
89
|
{
|
|
90
|
-
href: "https://github.com/
|
|
90
|
+
href: "https://github.com/Haribinger/bot?tab=readme-ov-file#manual-updating",
|
|
91
91
|
target: "_blank",
|
|
92
92
|
rel: "noopener noreferrer",
|
|
93
93
|
className: "text-emerald-500 hover:underline",
|
|
@@ -78,7 +78,7 @@ export function UpgradeDialog({ open, onClose, version, updateAvailable, changel
|
|
|
78
78
|
<p>
|
|
79
79
|
Some files (prompts, crons, triggers) won't be auto-updated to avoid breaking your bot.{' '}
|
|
80
80
|
<a
|
|
81
|
-
href="https://github.com/
|
|
81
|
+
href="https://github.com/Haribinger/bot?tab=readme-ov-file#understanding-init"
|
|
82
82
|
target="_blank"
|
|
83
83
|
rel="noopener noreferrer"
|
|
84
84
|
className="text-emerald-500 hover:underline"
|
|
@@ -92,7 +92,7 @@ export function UpgradeDialog({ open, onClose, version, updateAvailable, changel
|
|
|
92
92
|
<p className="text-xs text-muted-foreground">
|
|
93
93
|
If you hit unrecoverable errors, see the{' '}
|
|
94
94
|
<a
|
|
95
|
-
href="https://github.com/
|
|
95
|
+
href="https://github.com/Haribinger/bot?tab=readme-ov-file#manual-updating"
|
|
96
96
|
target="_blank"
|
|
97
97
|
rel="noopener noreferrer"
|
|
98
98
|
className="text-emerald-500 hover:underline"
|
package/lib/cron.js
CHANGED
|
@@ -5,8 +5,12 @@ import { cronsFile, cronDir } from './paths.js';
|
|
|
5
5
|
import { executeAction } from './actions.js';
|
|
6
6
|
|
|
7
7
|
function getInstalledVersion() {
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
const names = ['@harbinger-ai/harbinger', 'thepopebot'];
|
|
9
|
+
for (const name of names) {
|
|
10
|
+
const pkgPath = path.join(process.cwd(), 'node_modules', ...name.split('/'), 'package.json');
|
|
11
|
+
try { return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version; } catch {}
|
|
12
|
+
}
|
|
13
|
+
return '0.0.0';
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
// In-memory flag for available update (read by sidebar, written by cron)
|
|
@@ -98,7 +102,7 @@ function compareVersions(a, b) {
|
|
|
98
102
|
async function fetchAndStoreReleaseNotes(target) {
|
|
99
103
|
try {
|
|
100
104
|
const ghRes = await fetch(
|
|
101
|
-
`https://api.github.com/repos/
|
|
105
|
+
`https://api.github.com/repos/Haribinger/bot/releases/tags/v${target}`
|
|
102
106
|
);
|
|
103
107
|
if (!ghRes.ok) return;
|
|
104
108
|
const release = await ghRes.json();
|
|
@@ -110,7 +114,7 @@ async function fetchAndStoreReleaseNotes(target) {
|
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
/**
|
|
113
|
-
* Check npm registry for a newer version of
|
|
117
|
+
* Check npm registry for a newer version of harbinger.
|
|
114
118
|
*/
|
|
115
119
|
async function runVersionCheck() {
|
|
116
120
|
try {
|
|
@@ -119,8 +123,8 @@ async function runVersionCheck() {
|
|
|
119
123
|
if (isPrerelease(installed)) {
|
|
120
124
|
// Beta path: check both stable and beta dist-tags
|
|
121
125
|
const results = await Promise.allSettled([
|
|
122
|
-
fetch('https://registry.npmjs.org/
|
|
123
|
-
fetch('https://registry.npmjs.org/
|
|
126
|
+
fetch('https://registry.npmjs.org/@harbinger-ai%2fharbinger/latest'),
|
|
127
|
+
fetch('https://registry.npmjs.org/@harbinger-ai%2fharbinger/beta'),
|
|
124
128
|
]);
|
|
125
129
|
|
|
126
130
|
const candidates = [];
|
|
@@ -151,7 +155,7 @@ async function runVersionCheck() {
|
|
|
151
155
|
}
|
|
152
156
|
} else {
|
|
153
157
|
// Stable path: existing logic, untouched
|
|
154
|
-
const res = await fetch('https://registry.npmjs.org/
|
|
158
|
+
const res = await fetch('https://registry.npmjs.org/@harbinger-ai%2fharbinger/latest');
|
|
155
159
|
if (!res.ok) {
|
|
156
160
|
console.warn(`[version check] npm registry returned ${res.status}`);
|
|
157
161
|
return;
|
package/lib/db/index.js
CHANGED
|
@@ -41,7 +41,12 @@ export function initDatabase() {
|
|
|
41
41
|
|
|
42
42
|
// Resolve migrations folder from the installed package.
|
|
43
43
|
// import.meta.url doesn't survive webpack bundling, so resolve from PROJECT_ROOT.
|
|
44
|
-
|
|
44
|
+
// Try the new scoped package name first, fall back to the legacy name.
|
|
45
|
+
const packageNames = ['@harbinger-ai/harbinger', 'thepopebot'];
|
|
46
|
+
const migrationsFolder = packageNames
|
|
47
|
+
.map(name => path.join(PROJECT_ROOT, 'node_modules', ...name.split('/'), 'drizzle'))
|
|
48
|
+
.find(p => { try { return fs.existsSync(p); } catch { return false; } })
|
|
49
|
+
|| path.join(PROJECT_ROOT, 'node_modules', '@harbinger-ai', 'harbinger', 'drizzle');
|
|
45
50
|
|
|
46
51
|
migrate(db, { migrationsFolder });
|
|
47
52
|
|
package/lib/mcp/actions.js
CHANGED
|
@@ -44,7 +44,7 @@ export async function getMcpStatus() {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* Get metadata about
|
|
47
|
+
* Get metadata about Harbinger's own MCP server capabilities.
|
|
48
48
|
*/
|
|
49
49
|
export async function getOwnMcpServerInfo() {
|
|
50
50
|
await requireAuth();
|
package/lib/mcp/handler.js
CHANGED
|
@@ -40,9 +40,9 @@ export async function handleMcpRequest(request) {
|
|
|
40
40
|
|
|
41
41
|
if (method === 'GET') {
|
|
42
42
|
return Response.json({
|
|
43
|
-
name: '
|
|
43
|
+
name: 'harbinger',
|
|
44
44
|
version,
|
|
45
|
-
description: '
|
|
45
|
+
description: 'Harbinger MCP server — autonomous AI agent platform',
|
|
46
46
|
tools: ['create_job', 'get_job_status', 'chat', 'list_agents', 'get_agent_profile'],
|
|
47
47
|
resources: ['agent://agents', 'agent://{agentId}/soul', 'config://soul', 'config://crons', 'config://triggers'],
|
|
48
48
|
prompts: ['agent-prompt'],
|