@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.
Files changed (65) hide show
  1. package/bin/cli.js +40 -38
  2. package/bin/local.sh +2 -2
  3. package/bin/postinstall.js +8 -8
  4. package/config/index.js +6 -3
  5. package/config/instrumentation.js +11 -11
  6. package/lib/chat/actions.js +19 -0
  7. package/lib/chat/components/app-sidebar.js +17 -1
  8. package/lib/chat/components/app-sidebar.jsx +19 -1
  9. package/lib/chat/components/findings-page.js +164 -103
  10. package/lib/chat/components/findings-page.jsx +156 -101
  11. package/lib/chat/components/icons.js +22 -0
  12. package/lib/chat/components/icons.jsx +20 -0
  13. package/lib/chat/components/index.js +1 -0
  14. package/lib/chat/components/mission-control.js +490 -0
  15. package/lib/chat/components/mission-control.jsx +618 -0
  16. package/lib/chat/components/registry-page.js +267 -133
  17. package/lib/chat/components/registry-page.jsx +299 -138
  18. package/lib/chat/components/sidebar-user-nav.js +1 -1
  19. package/lib/chat/components/sidebar-user-nav.jsx +1 -1
  20. package/lib/chat/components/targets-page.js +269 -200
  21. package/lib/chat/components/targets-page.jsx +181 -111
  22. package/lib/chat/components/upgrade-dialog.js +2 -2
  23. package/lib/chat/components/upgrade-dialog.jsx +2 -2
  24. package/lib/cron.js +11 -7
  25. package/lib/db/index.js +6 -1
  26. package/lib/mcp/actions.js +1 -1
  27. package/lib/mcp/handler.js +2 -2
  28. package/lib/mcp/server.js +1 -1
  29. package/lib/paths.js +1 -1
  30. package/package.json +1 -1
  31. package/templates/.env.example +4 -4
  32. package/templates/.github/workflows/rebuild-event-handler.yml +20 -20
  33. package/templates/.github/workflows/run-job.yml +6 -6
  34. package/templates/.github/workflows/upgrade-event-handler.yml +12 -12
  35. package/templates/CLAUDE.md +3 -3
  36. package/templates/CLAUDE.md.template +9 -9
  37. package/templates/app/api/[...thepopebot]/route.js +1 -1
  38. package/templates/app/api/auth/[...nextauth]/route.js +1 -1
  39. package/templates/app/chat/[chatId]/page.js +2 -2
  40. package/templates/app/chats/page.js +2 -2
  41. package/templates/app/components/setup-form.jsx +1 -1
  42. package/templates/app/findings/page.js +2 -2
  43. package/templates/app/globals.css +1 -1
  44. package/templates/app/layout.js +1 -1
  45. package/templates/app/login/page.js +1 -1
  46. package/templates/app/notifications/page.js +2 -2
  47. package/templates/app/page.js +2 -2
  48. package/templates/app/settings/crons/page.js +1 -1
  49. package/templates/app/settings/layout.js +2 -2
  50. package/templates/app/settings/mcp/page.js +1 -1
  51. package/templates/app/settings/secrets/page.js +1 -1
  52. package/templates/app/settings/triggers/page.js +1 -1
  53. package/templates/app/stream/chat/route.js +1 -1
  54. package/templates/app/swarm/page.js +2 -2
  55. package/templates/app/targets/page.js +2 -2
  56. package/templates/app/toolbox/page.js +2 -2
  57. package/templates/config/AGENT.md +2 -2
  58. package/templates/config/EVENT_HANDLER.md +3 -3
  59. package/templates/config/SKILL_BUILDING_GUIDE.md +1 -1
  60. package/templates/config/SOUL.md +1 -1
  61. package/templates/docker/event-handler/Dockerfile +1 -1
  62. package/templates/docker-compose.yml +2 -2
  63. package/templates/instrumentation.js +1 -1
  64. package/templates/middleware.js +1 -1
  65. 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', color: 'bg-purple-500/10 text-purple-500', border: 'border-purple-500/20' },
9
- { id: 'bugcrowd', label: 'Bugcrowd', color: 'bg-orange-500/10 text-orange-500', border: 'border-orange-500/20' },
10
- { id: 'intigriti', label: 'Intigriti', color: 'bg-blue-500/10 text-blue-500', border: 'border-blue-500/20' },
11
- { id: 'yeswehack', label: 'YesWeHack', color: 'bg-teal-500/10 text-teal-500', border: 'border-teal-500/20' },
12
- { id: 'federacy', label: 'Federacy', color: 'bg-pink-500/10 text-pink-500', border: 'border-pink-500/20' },
13
- { id: 'custom', label: 'Custom', color: 'bg-muted text-muted-foreground', border: 'border-border' },
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); // platform name or 'all'
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 className="rounded-lg border bg-card p-4 mb-6">
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
- <h3 className="text-sm font-medium">Sync from Bounty Platforms</h3>
64
- <p className="text-[11px] text-muted-foreground mt-0.5">
65
- Import programs and targets from bounty-targets-data (arkadiyt/bounty-targets-data)
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-accent/50 disabled:opacity-50 ${p.border}`}
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.color}`}>{count}</span>}
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-foreground text-background hover:opacity-90 disabled:opacity-50"
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 per platform:</label>
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-background"
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
- {/* Sync results */}
115
- {results && (
116
- <div className="rounded-md bg-muted/50 p-3 mt-2">
117
- {results.error ? (
118
- <p className="text-xs text-destructive">{results.error}</p>
119
- ) : (
120
- <div className="flex flex-col gap-1">
121
- {Object.entries(results).map(([platform, stats]) => (
122
- <div key={platform} className="flex items-center gap-3 text-xs">
123
- <span className="font-medium w-24">{platform}</span>
124
- <span className="text-green-500">+{stats.programsAdded} programs</span>
125
- <span className="text-blue-500">{stats.programsUpdated} updated</span>
126
- <span className="text-green-500">+{stats.targetsAdded} targets</span>
127
- <span className="text-muted-foreground">{stats.targetsSkipped} skipped</span>
128
- {stats.errors?.length > 0 && <span className="text-destructive">{stats.errors.length} errors</span>}
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
- </div>
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-2 pt-2 border-t">
139
- <span className="text-[10px] text-muted-foreground">{syncStatus.totalSyncedPrograms} synced programs:</span>
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 = PLATFORMS.find(x => x.id === p);
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?.color || ''}`}>
144
- {plat?.label || p} ({count})
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 platform = PLATFORMS.find(p => p.id === program.platform) || PLATFORMS[5];
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-colors ${selected ? 'border-foreground bg-accent/50' : 'bg-card hover:bg-accent/30'}`}
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="shrink-0 rounded-md bg-muted p-2"><GlobeIcon size={14} /></div>
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 ${platform.color}`}>{platform.label}</span>
166
- {program.maxBounty > 0 && <span className="text-[10px] text-muted-foreground">Up to ${program.maxBounty.toLocaleString()}</span>}
167
- {program.syncHandle && <span className="inline-flex rounded-full bg-cyan-500/10 px-1.5 py-0.5 text-[9px] text-cyan-500">synced</span>}
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 className="flex items-center gap-3 p-3 rounded-lg border bg-card">
180
- <div className="shrink-0 rounded-md bg-muted p-2"><CrosshairIcon size={14} /></div>
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-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground">{target.type}</span>
185
- {target.syncSource && <span className="inline-flex rounded-full bg-cyan-500/10 px-1.5 py-0.5 text-[9px] text-cyan-500">{target.syncSource}</span>}
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-0 cursor-pointer ${STATUS_COLORS[target.status] || ''}`}
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-accent/50 transition-colors text-muted-foreground hover:text-foreground">
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 className="flex flex-wrap items-end gap-2 p-3 rounded-lg border bg-card">
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-background">
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-background min-w-[120px]" />
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-foreground text-background hover:opacity-90">Save</button>
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
- 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-border/50" />)}</div>;
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
- <div className="mb-6">
307
- <h1 className="text-2xl font-semibold">Targets</h1>
308
- <p className="text-sm text-muted-foreground mt-1">{programs_.length} program{programs_.length !== 1 ? 's' : ''}, {targets_.length} target{targets_.length !== 1 ? 's' : ''} in scope</p>
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 onClick={() => setPlatformFilter('all')} className={`shrink-0 px-3 py-1 rounded-full text-xs font-medium transition-colors ${platformFilter === 'all' ? 'bg-foreground text-background' : 'bg-muted text-muted-foreground hover:text-foreground'}`}>
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 key={p.id} onClick={() => setPlatformFilter(p.id)} className={`shrink-0 px-3 py-1 rounded-full text-xs font-medium transition-colors ${platformFilter === p.id ? 'bg-foreground text-background' : 'bg-muted text-muted-foreground hover:text-foreground'}`}>
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-sm font-medium">Programs</h2>
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-muted p-3 mb-3"><GlobeIcon size={20} /></div>
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-sm font-medium">Targets {selectedProgram && `\u2014 ${programs_.find(p => p.id === selectedProgram)?.name || ''}`}</h2>
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
- {!selectedProgram ? (
369
- <div className="flex flex-col items-center py-12 text-center">
370
- <div className="rounded-full bg-muted p-4 mb-4"><CrosshairIcon size={24} /></div>
371
- <p className="text-sm font-medium mb-1">Select a program</p>
372
- <p className="text-xs text-muted-foreground">Choose a program to manage its targets</p>
373
- </div>
374
- ) : targets_.length === 0 ? (
375
- <div className="flex flex-col items-center py-12 text-center">
376
- <div className="rounded-full bg-muted p-4 mb-4"><CrosshairIcon size={24} /></div>
377
- <p className="text-sm font-medium mb-1">No targets yet</p>
378
- <p className="text-xs text-muted-foreground">Add domains, IPs, and URLs to start hunting</p>
379
- </div>
380
- ) : (
381
- <div className="flex flex-col gap-2 max-h-[600px] overflow-y-auto">
382
- {targets_.map(t => <TargetRow key={t.id} target={t} onDelete={handleDeleteTarget} onStatusChange={handleStatusChange} />)}
383
- </div>
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/stephengpope/thepopebot?tab=readme-ov-file#understanding-init",
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/stephengpope/thepopebot?tab=readme-ov-file#manual-updating",
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/stephengpope/thepopebot?tab=readme-ov-file#understanding-init"
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/stephengpope/thepopebot?tab=readme-ov-file#manual-updating"
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 pkgPath = path.join(process.cwd(), 'node_modules', 'thepopebot', 'package.json');
9
- return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
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/stephengpope/thepopebot/releases/tags/v${target}`
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 thepopebot.
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/thepopebot/latest'),
123
- fetch('https://registry.npmjs.org/thepopebot/beta'),
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/thepopebot/latest');
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
- const migrationsFolder = path.join(PROJECT_ROOT, 'node_modules', 'thepopebot', 'drizzle');
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
 
@@ -44,7 +44,7 @@ export async function getMcpStatus() {
44
44
  }
45
45
 
46
46
  /**
47
- * Get metadata about thepopebot's own MCP server capabilities.
47
+ * Get metadata about Harbinger's own MCP server capabilities.
48
48
  */
49
49
  export async function getOwnMcpServerInfo() {
50
50
  await requireAuth();
@@ -40,9 +40,9 @@ export async function handleMcpRequest(request) {
40
40
 
41
41
  if (method === 'GET') {
42
42
  return Response.json({
43
- name: 'thepopebot',
43
+ name: 'harbinger',
44
44
  version,
45
- description: 'thepopebot MCP server — autonomous AI agent platform',
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'],
package/lib/mcp/server.js CHANGED
@@ -12,7 +12,7 @@ const { version } = require('../../package.json');
12
12
  */
13
13
  export function createMcpServer() {
14
14
  const server = new McpServer({
15
- name: 'thepopebot',
15
+ name: 'harbinger',
16
16
  version,
17
17
  });
18
18