@harbinger-ai/harbinger 0.1.0

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 (317) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +406 -0
  3. package/agents/README.md +76 -0
  4. package/agents/_template/CONFIG.yaml +7 -0
  5. package/agents/_template/HEARTBEAT.md +59 -0
  6. package/agents/_template/IDENTITY.md +4 -0
  7. package/agents/_template/SKILLS.md +1 -0
  8. package/agents/_template/SOUL.md +25 -0
  9. package/agents/_template/TOOLS.md +3 -0
  10. package/agents/binary-reverser/CONFIG.yaml +21 -0
  11. package/agents/binary-reverser/HEARTBEAT.md +65 -0
  12. package/agents/binary-reverser/IDENTITY.md +1 -0
  13. package/agents/binary-reverser/SKILLS.md +1 -0
  14. package/agents/binary-reverser/SOUL.md +23 -0
  15. package/agents/binary-reverser/TOOLS.md +99 -0
  16. package/agents/browser-agent/CONFIG.yaml +20 -0
  17. package/agents/browser-agent/HEARTBEAT.md +79 -0
  18. package/agents/browser-agent/IDENTITY.md +5 -0
  19. package/agents/browser-agent/SKILLS.md +86 -0
  20. package/agents/browser-agent/SOUL.md +23 -0
  21. package/agents/browser-agent/TOOLS.md +186 -0
  22. package/agents/cloud-infiltrator/CONFIG.yaml +22 -0
  23. package/agents/cloud-infiltrator/HEARTBEAT.md +78 -0
  24. package/agents/cloud-infiltrator/IDENTITY.md +1 -0
  25. package/agents/cloud-infiltrator/SKILLS.md +1 -0
  26. package/agents/cloud-infiltrator/SOUL.md +23 -0
  27. package/agents/cloud-infiltrator/TOOLS.md +68 -0
  28. package/agents/coding-assistant/CONFIG.yaml +22 -0
  29. package/agents/coding-assistant/HEARTBEAT.md +57 -0
  30. package/agents/coding-assistant/IDENTITY.md +5 -0
  31. package/agents/coding-assistant/SKILLS.md +69 -0
  32. package/agents/coding-assistant/SOUL.md +60 -0
  33. package/agents/coding-assistant/TOOLS.md +168 -0
  34. package/agents/learning-agent/CONFIG.yaml +21 -0
  35. package/agents/learning-agent/HEARTBEAT.md +63 -0
  36. package/agents/learning-agent/IDENTITY.md +5 -0
  37. package/agents/learning-agent/SKILLS.md +86 -0
  38. package/agents/learning-agent/SOUL.md +77 -0
  39. package/agents/learning-agent/TOOLS.md +145 -0
  40. package/agents/maintainer/CONFIG.yaml +31 -0
  41. package/agents/maintainer/HEARTBEAT.md +28 -0
  42. package/agents/maintainer/IDENTITY.md +33 -0
  43. package/agents/maintainer/SKILLS.md +24 -0
  44. package/agents/maintainer/SOUL.md +61 -0
  45. package/agents/maintainer/TOOLS.md +29 -0
  46. package/agents/maintainer/lib/engine.js +279 -0
  47. package/agents/maintainer/lib/safe-fixer.js +183 -0
  48. package/agents/morning-brief/CONFIG.yaml +22 -0
  49. package/agents/morning-brief/HEARTBEAT.md +60 -0
  50. package/agents/morning-brief/IDENTITY.md +5 -0
  51. package/agents/morning-brief/SKILLS.md +56 -0
  52. package/agents/morning-brief/SOUL.md +64 -0
  53. package/agents/morning-brief/TOOLS.md +112 -0
  54. package/agents/osint-detective/CONFIG.yaml +24 -0
  55. package/agents/osint-detective/HEARTBEAT.md +66 -0
  56. package/agents/osint-detective/IDENTITY.md +1 -0
  57. package/agents/osint-detective/SKILLS.md +1 -0
  58. package/agents/osint-detective/SOUL.md +23 -0
  59. package/agents/osint-detective/TOOLS.md +81 -0
  60. package/agents/recon-scout/CONFIG.yaml +22 -0
  61. package/agents/recon-scout/HEARTBEAT.md +79 -0
  62. package/agents/recon-scout/IDENTITY.md +1 -0
  63. package/agents/recon-scout/SKILLS.md +1 -0
  64. package/agents/recon-scout/SOUL.md +23 -0
  65. package/agents/recon-scout/TOOLS.md +93 -0
  66. package/agents/report-writer/CONFIG.yaml +21 -0
  67. package/agents/report-writer/HEARTBEAT.md +63 -0
  68. package/agents/report-writer/IDENTITY.md +1 -0
  69. package/agents/report-writer/SKILLS.md +1 -0
  70. package/agents/report-writer/SOUL.md +23 -0
  71. package/agents/report-writer/TOOLS.md +69 -0
  72. package/agents/shared/README.md +13 -0
  73. package/agents/web-hacker/CONFIG.yaml +24 -0
  74. package/agents/web-hacker/HEARTBEAT.md +78 -0
  75. package/agents/web-hacker/IDENTITY.md +1 -0
  76. package/agents/web-hacker/SKILLS.md +1 -0
  77. package/agents/web-hacker/SOUL.md +23 -0
  78. package/agents/web-hacker/TOOLS.md +86 -0
  79. package/api/CLAUDE.md +19 -0
  80. package/api/index.js +274 -0
  81. package/bin/cli.js +620 -0
  82. package/bin/local.sh +31 -0
  83. package/bin/postinstall.js +63 -0
  84. package/config/index.js +24 -0
  85. package/config/instrumentation.js +93 -0
  86. package/drizzle/0000_initial.sql +52 -0
  87. package/drizzle/0001_bounty_and_registry.sql +82 -0
  88. package/drizzle/0002_sync_columns.sql +7 -0
  89. package/drizzle/0003_graceful_bloodscream.sql +86 -0
  90. package/drizzle/meta/0000_snapshot.json +321 -0
  91. package/drizzle/meta/0003_snapshot.json +878 -0
  92. package/drizzle/meta/_journal.json +34 -0
  93. package/drizzle/relations.ts +3 -0
  94. package/drizzle/schema.ts +145 -0
  95. package/lib/actions.js +47 -0
  96. package/lib/agents.js +166 -0
  97. package/lib/ai/agent.js +96 -0
  98. package/lib/ai/autonomous-engine.js +261 -0
  99. package/lib/ai/index.js +359 -0
  100. package/lib/ai/model-router.js +254 -0
  101. package/lib/ai/model.js +73 -0
  102. package/lib/ai/tools.js +84 -0
  103. package/lib/auth/actions.js +28 -0
  104. package/lib/auth/config.js +27 -0
  105. package/lib/auth/edge-config.js +27 -0
  106. package/lib/auth/index.js +27 -0
  107. package/lib/auth/middleware.js +53 -0
  108. package/lib/bounty/actions.js +119 -0
  109. package/lib/bounty/findings.js +64 -0
  110. package/lib/bounty/programs.js +34 -0
  111. package/lib/bounty/sync-targets.js +267 -0
  112. package/lib/bounty/targets.js +33 -0
  113. package/lib/channels/base.js +56 -0
  114. package/lib/channels/index.js +15 -0
  115. package/lib/channels/telegram.js +148 -0
  116. package/lib/chat/actions.js +288 -0
  117. package/lib/chat/api.js +135 -0
  118. package/lib/chat/components/app-sidebar.js +237 -0
  119. package/lib/chat/components/app-sidebar.jsx +289 -0
  120. package/lib/chat/components/chat-header.js +27 -0
  121. package/lib/chat/components/chat-header.jsx +37 -0
  122. package/lib/chat/components/chat-input.js +230 -0
  123. package/lib/chat/components/chat-input.jsx +228 -0
  124. package/lib/chat/components/chat-nav-context.js +11 -0
  125. package/lib/chat/components/chat-nav-context.jsx +11 -0
  126. package/lib/chat/components/chat-page.js +81 -0
  127. package/lib/chat/components/chat-page.jsx +100 -0
  128. package/lib/chat/components/chat.js +150 -0
  129. package/lib/chat/components/chat.jsx +182 -0
  130. package/lib/chat/components/chats-page.js +302 -0
  131. package/lib/chat/components/chats-page.jsx +330 -0
  132. package/lib/chat/components/crons-page.js +172 -0
  133. package/lib/chat/components/crons-page.jsx +244 -0
  134. package/lib/chat/components/enhanced-tool-call.js +103 -0
  135. package/lib/chat/components/enhanced-tool-call.jsx +139 -0
  136. package/lib/chat/components/findings-page.js +175 -0
  137. package/lib/chat/components/findings-page.jsx +214 -0
  138. package/lib/chat/components/greeting.js +22 -0
  139. package/lib/chat/components/greeting.jsx +26 -0
  140. package/lib/chat/components/icons.js +777 -0
  141. package/lib/chat/components/icons.jsx +741 -0
  142. package/lib/chat/components/index.js +26 -0
  143. package/lib/chat/components/mcp-page.js +260 -0
  144. package/lib/chat/components/mcp-page.jsx +355 -0
  145. package/lib/chat/components/message.js +289 -0
  146. package/lib/chat/components/message.jsx +315 -0
  147. package/lib/chat/components/messages.js +66 -0
  148. package/lib/chat/components/messages.jsx +77 -0
  149. package/lib/chat/components/notifications-page.js +56 -0
  150. package/lib/chat/components/notifications-page.jsx +87 -0
  151. package/lib/chat/components/page-layout.js +21 -0
  152. package/lib/chat/components/page-layout.jsx +28 -0
  153. package/lib/chat/components/registry-page.js +222 -0
  154. package/lib/chat/components/registry-page.jsx +255 -0
  155. package/lib/chat/components/settings-layout.js +40 -0
  156. package/lib/chat/components/settings-layout.jsx +54 -0
  157. package/lib/chat/components/settings-secrets-page.js +216 -0
  158. package/lib/chat/components/settings-secrets-page.jsx +264 -0
  159. package/lib/chat/components/sidebar-history-item.js +132 -0
  160. package/lib/chat/components/sidebar-history-item.jsx +113 -0
  161. package/lib/chat/components/sidebar-history.js +115 -0
  162. package/lib/chat/components/sidebar-history.jsx +157 -0
  163. package/lib/chat/components/sidebar-user-nav.js +63 -0
  164. package/lib/chat/components/sidebar-user-nav.jsx +73 -0
  165. package/lib/chat/components/status-bar.js +39 -0
  166. package/lib/chat/components/status-bar.jsx +51 -0
  167. package/lib/chat/components/swarm-page.js +157 -0
  168. package/lib/chat/components/swarm-page.jsx +210 -0
  169. package/lib/chat/components/targets-page.js +376 -0
  170. package/lib/chat/components/targets-page.jsx +389 -0
  171. package/lib/chat/components/tool-call.js +86 -0
  172. package/lib/chat/components/tool-call.jsx +104 -0
  173. package/lib/chat/components/tool-panel.js +107 -0
  174. package/lib/chat/components/tool-panel.jsx +145 -0
  175. package/lib/chat/components/triggers-page.js +153 -0
  176. package/lib/chat/components/triggers-page.jsx +221 -0
  177. package/lib/chat/components/ui/confirm-dialog.js +53 -0
  178. package/lib/chat/components/ui/confirm-dialog.jsx +57 -0
  179. package/lib/chat/components/ui/dropdown-menu.js +98 -0
  180. package/lib/chat/components/ui/dropdown-menu.jsx +116 -0
  181. package/lib/chat/components/ui/rename-dialog.js +74 -0
  182. package/lib/chat/components/ui/rename-dialog.jsx +72 -0
  183. package/lib/chat/components/ui/scroll-area.js +13 -0
  184. package/lib/chat/components/ui/scroll-area.jsx +17 -0
  185. package/lib/chat/components/ui/separator.js +21 -0
  186. package/lib/chat/components/ui/separator.jsx +18 -0
  187. package/lib/chat/components/ui/sheet.js +75 -0
  188. package/lib/chat/components/ui/sheet.jsx +95 -0
  189. package/lib/chat/components/ui/sidebar.js +227 -0
  190. package/lib/chat/components/ui/sidebar.jsx +245 -0
  191. package/lib/chat/components/ui/tooltip.js +56 -0
  192. package/lib/chat/components/ui/tooltip.jsx +66 -0
  193. package/lib/chat/components/upgrade-dialog.js +151 -0
  194. package/lib/chat/components/upgrade-dialog.jsx +170 -0
  195. package/lib/chat/utils.js +11 -0
  196. package/lib/cron.js +246 -0
  197. package/lib/db/api-keys.js +163 -0
  198. package/lib/db/chats.js +145 -0
  199. package/lib/db/index.js +52 -0
  200. package/lib/db/notifications.js +99 -0
  201. package/lib/db/schema.js +145 -0
  202. package/lib/db/update-check.js +96 -0
  203. package/lib/db/users.js +89 -0
  204. package/lib/mcp/actions.js +104 -0
  205. package/lib/mcp/client.js +79 -0
  206. package/lib/mcp/handler.js +57 -0
  207. package/lib/mcp/server.js +165 -0
  208. package/lib/paths.js +46 -0
  209. package/lib/registry/actions.js +164 -0
  210. package/lib/registry/catalog.js +137 -0
  211. package/lib/registry/tools.js +71 -0
  212. package/lib/tools/create-job.js +99 -0
  213. package/lib/tools/github.js +217 -0
  214. package/lib/tools/openai.js +35 -0
  215. package/lib/tools/telegram.js +292 -0
  216. package/lib/triggers.js +118 -0
  217. package/lib/utils/render-md.js +102 -0
  218. package/package.json +103 -0
  219. package/setup/lib/auth.mjs +81 -0
  220. package/setup/lib/env.mjs +21 -0
  221. package/setup/lib/fs-utils.mjs +20 -0
  222. package/setup/lib/github.mjs +149 -0
  223. package/setup/lib/prerequisites.mjs +155 -0
  224. package/setup/lib/prompts.mjs +267 -0
  225. package/setup/lib/providers.mjs +48 -0
  226. package/setup/lib/sync.mjs +125 -0
  227. package/setup/lib/targets.mjs +45 -0
  228. package/setup/lib/telegram-verify.mjs +63 -0
  229. package/setup/lib/telegram.mjs +76 -0
  230. package/setup/setup-telegram.mjs +264 -0
  231. package/setup/setup.mjs +842 -0
  232. package/templates/.dockerignore +5 -0
  233. package/templates/.env.example +63 -0
  234. package/templates/.github/workflows/auto-merge.yml +117 -0
  235. package/templates/.github/workflows/build-image.yml +36 -0
  236. package/templates/.github/workflows/notify-job-failed.yml +64 -0
  237. package/templates/.github/workflows/notify-pr-complete.yml +119 -0
  238. package/templates/.github/workflows/rebuild-event-handler.yml +121 -0
  239. package/templates/.github/workflows/run-job.yml +89 -0
  240. package/templates/.github/workflows/upgrade-event-handler.yml +62 -0
  241. package/templates/.gitignore.template +45 -0
  242. package/templates/.pi/extensions/env-sanitizer/index.ts +48 -0
  243. package/templates/.pi/extensions/env-sanitizer/package.json +5 -0
  244. package/templates/CLAUDE.md +29 -0
  245. package/templates/CLAUDE.md.template +307 -0
  246. package/templates/app/api/[...thepopebot]/route.js +1 -0
  247. package/templates/app/api/auth/[...nextauth]/route.js +1 -0
  248. package/templates/app/chat/[chatId]/page.js +8 -0
  249. package/templates/app/chats/page.js +7 -0
  250. package/templates/app/components/ascii-logo.jsx +10 -0
  251. package/templates/app/components/login-form.jsx +92 -0
  252. package/templates/app/components/setup-form.jsx +82 -0
  253. package/templates/app/components/theme-provider.jsx +11 -0
  254. package/templates/app/components/theme-toggle.jsx +38 -0
  255. package/templates/app/components/ui/button.jsx +21 -0
  256. package/templates/app/components/ui/card.jsx +23 -0
  257. package/templates/app/components/ui/input.jsx +10 -0
  258. package/templates/app/components/ui/label.jsx +10 -0
  259. package/templates/app/crons/page.js +5 -0
  260. package/templates/app/findings/page.js +7 -0
  261. package/templates/app/globals.css +90 -0
  262. package/templates/app/layout.js +19 -0
  263. package/templates/app/login/page.js +15 -0
  264. package/templates/app/notifications/page.js +7 -0
  265. package/templates/app/page.js +7 -0
  266. package/templates/app/settings/crons/page.js +5 -0
  267. package/templates/app/settings/layout.js +7 -0
  268. package/templates/app/settings/mcp/page.js +5 -0
  269. package/templates/app/settings/page.js +5 -0
  270. package/templates/app/settings/secrets/page.js +5 -0
  271. package/templates/app/settings/triggers/page.js +5 -0
  272. package/templates/app/stream/chat/route.js +1 -0
  273. package/templates/app/swarm/page.js +7 -0
  274. package/templates/app/targets/page.js +7 -0
  275. package/templates/app/toolbox/page.js +7 -0
  276. package/templates/app/triggers/page.js +5 -0
  277. package/templates/config/AGENT.md +34 -0
  278. package/templates/config/CRONS.json +56 -0
  279. package/templates/config/EVENT_HANDLER.md +224 -0
  280. package/templates/config/HEARTBEAT.md +3 -0
  281. package/templates/config/JOB_SUMMARY.md +130 -0
  282. package/templates/config/MCP_SERVERS.json +1 -0
  283. package/templates/config/SKILL_BUILDING_GUIDE.md +90 -0
  284. package/templates/config/SOUL.md +17 -0
  285. package/templates/config/TRIGGERS.json +58 -0
  286. package/templates/docker/event-handler/Dockerfile +20 -0
  287. package/templates/docker/event-handler/ecosystem.config.cjs +8 -0
  288. package/templates/docker/job-claude-code/Dockerfile +34 -0
  289. package/templates/docker/job-claude-code/entrypoint.sh +139 -0
  290. package/templates/docker/job-pi-coding-agent/Dockerfile +44 -0
  291. package/templates/docker/job-pi-coding-agent/entrypoint.sh +163 -0
  292. package/templates/docker-compose.yml +63 -0
  293. package/templates/instrumentation.js +6 -0
  294. package/templates/middleware.js +1 -0
  295. package/templates/next.config.mjs +3 -0
  296. package/templates/postcss.config.mjs +5 -0
  297. package/templates/skills/LICENSE +21 -0
  298. package/templates/skills/README.md +119 -0
  299. package/templates/skills/brave-search/SKILL.md +79 -0
  300. package/templates/skills/brave-search/content.js +86 -0
  301. package/templates/skills/brave-search/package-lock.json +621 -0
  302. package/templates/skills/brave-search/package.json +14 -0
  303. package/templates/skills/brave-search/search.js +199 -0
  304. package/templates/skills/browser-tools/SKILL.md +196 -0
  305. package/templates/skills/browser-tools/browser-content.js +103 -0
  306. package/templates/skills/browser-tools/browser-cookies.js +35 -0
  307. package/templates/skills/browser-tools/browser-eval.js +53 -0
  308. package/templates/skills/browser-tools/browser-hn-scraper.js +108 -0
  309. package/templates/skills/browser-tools/browser-nav.js +44 -0
  310. package/templates/skills/browser-tools/browser-pick.js +162 -0
  311. package/templates/skills/browser-tools/browser-screenshot.js +34 -0
  312. package/templates/skills/browser-tools/browser-start.js +87 -0
  313. package/templates/skills/browser-tools/package-lock.json +2556 -0
  314. package/templates/skills/browser-tools/package.json +19 -0
  315. package/templates/skills/llm-secrets/SKILL.md +34 -0
  316. package/templates/skills/llm-secrets/llm-secrets.js +33 -0
  317. package/templates/skills/modify-self/SKILL.md +12 -0
@@ -0,0 +1,389 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { CrosshairIcon, PlusIcon, TrashIcon, ChevronDownIcon, GlobeIcon, SpinnerIcon, DownloadIcon, CheckIcon } from './icons.js';
5
+ import { getPrograms, createProgram, deleteProgram, getTargets, createTarget, deleteTarget, updateTarget, syncTargetsFromPlatform, syncAllTargets, getSyncStatus } from '../../bounty/actions.js';
6
+
7
+ 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' },
14
+ ];
15
+
16
+ const TARGET_TYPES = ['domain', 'wildcard', 'ip', 'cidr', 'url', 'api', 'mobile'];
17
+ 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',
22
+ };
23
+
24
+ function timeAgo(ts) {
25
+ if (!ts) return 'never';
26
+ const diff = Date.now() - ts;
27
+ const mins = Math.floor(diff / 60000);
28
+ if (mins < 60) return `${mins}m ago`;
29
+ const hrs = Math.floor(mins / 60);
30
+ if (hrs < 24) return `${hrs}h ago`;
31
+ return `${Math.floor(hrs / 24)}d ago`;
32
+ }
33
+
34
+ function SyncPanel({ onSync, syncStatus }) {
35
+ const [syncing, setSyncing] = useState(null); // platform name or 'all'
36
+ const [results, setResults] = useState(null);
37
+ const [maxPrograms, setMaxPrograms] = useState('50');
38
+
39
+ async function handleSync(platform) {
40
+ setSyncing(platform);
41
+ setResults(null);
42
+ try {
43
+ const opts = { maxPrograms: parseInt(maxPrograms) || 0 };
44
+ let res;
45
+ if (platform === 'all') {
46
+ res = await onSync(null, opts);
47
+ } else {
48
+ res = await onSync(platform, opts);
49
+ }
50
+ setResults(res);
51
+ } catch (err) {
52
+ setResults({ error: err.message });
53
+ }
54
+ setSyncing(null);
55
+ }
56
+
57
+ const syncablePlatforms = PLATFORMS.filter(p => p.id !== 'custom');
58
+
59
+ return (
60
+ <div className="rounded-lg border bg-card p-4 mb-6">
61
+ <div className="flex items-center justify-between mb-3">
62
+ <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)
66
+ </p>
67
+ </div>
68
+ {syncStatus?.lastSyncedAt && (
69
+ <span className="text-[10px] text-muted-foreground">Last sync: {timeAgo(syncStatus.lastSyncedAt)}</span>
70
+ )}
71
+ </div>
72
+
73
+ {/* Platform sync buttons */}
74
+ <div className="flex flex-wrap gap-2 mb-3">
75
+ {syncablePlatforms.map(p => {
76
+ const count = syncStatus?.platformCounts?.[p.id] || 0;
77
+ return (
78
+ <button
79
+ key={p.id}
80
+ onClick={() => handleSync(p.id)}
81
+ 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}`}
83
+ >
84
+ {syncing === p.id ? <SpinnerIcon size={12} /> : <DownloadIcon size={12} />}
85
+ {p.label}
86
+ {count > 0 && <span className={`inline-flex rounded-full px-1.5 py-0.5 text-[9px] ${p.color}`}>{count}</span>}
87
+ </button>
88
+ );
89
+ })}
90
+ <button
91
+ onClick={() => handleSync('all')}
92
+ 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"
94
+ >
95
+ {syncing === 'all' ? <SpinnerIcon size={12} /> : <DownloadIcon size={12} />}
96
+ Sync All
97
+ </button>
98
+ </div>
99
+
100
+ {/* Max programs limit */}
101
+ <div className="flex items-center gap-2 mb-3">
102
+ <label className="text-[10px] text-muted-foreground">Max programs per platform:</label>
103
+ <input
104
+ type="number"
105
+ value={maxPrograms}
106
+ onChange={e => setMaxPrograms(e.target.value)}
107
+ className="w-20 text-xs border rounded-md px-2 py-1 bg-background"
108
+ min="0"
109
+ placeholder="0 = all"
110
+ />
111
+ <span className="text-[10px] text-muted-foreground">(0 = unlimited)</span>
112
+ </div>
113
+
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>}
129
+ </div>
130
+ ))}
131
+ </div>
132
+ )}
133
+ </div>
134
+ )}
135
+
136
+ {/* Sync status summary */}
137
+ {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>
140
+ {Object.entries(syncStatus.platformCounts || {}).map(([p, count]) => {
141
+ const plat = PLATFORMS.find(x => x.id === p);
142
+ 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})
145
+ </span>
146
+ );
147
+ })}
148
+ </div>
149
+ )}
150
+ </div>
151
+ );
152
+ }
153
+
154
+ function ProgramCard({ program, onSelect, selected, onDelete }) {
155
+ const platform = PLATFORMS.find(p => p.id === program.platform) || PLATFORMS[5];
156
+ return (
157
+ <button
158
+ 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'}`}
160
+ >
161
+ <div className="shrink-0 rounded-md bg-muted p-2"><GlobeIcon size={14} /></div>
162
+ <div className="flex-1 min-w-0">
163
+ <p className="text-sm font-medium truncate">{program.name}</p>
164
+ <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>}
168
+ </div>
169
+ </div>
170
+ {!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>
172
+ )}
173
+ </button>
174
+ );
175
+ }
176
+
177
+ function TargetRow({ target, onDelete, onStatusChange }) {
178
+ 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>
181
+ <div className="flex-1 min-w-0">
182
+ <p className="text-sm font-mono font-medium truncate">{target.value}</p>
183
+ <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>}
187
+ </div>
188
+ </div>
189
+ <select
190
+ value={target.status}
191
+ 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] || ''}`}
193
+ >
194
+ <option value="in_scope">in scope</option>
195
+ <option value="testing">testing</option>
196
+ <option value="completed">completed</option>
197
+ <option value="out_of_scope">out of scope</option>
198
+ </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>
201
+ );
202
+ }
203
+
204
+ function AddForm({ fields, onSubmit }) {
205
+ const [values, setValues] = useState({});
206
+ const [open, setOpen] = useState(false);
207
+
208
+ 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">
210
+ <PlusIcon size={12} /> Add
211
+ </button>
212
+ );
213
+
214
+ return (
215
+ <div className="flex flex-wrap items-end gap-2 p-3 rounded-lg border bg-card">
216
+ {fields.map(f => (
217
+ <div key={f.name} className="flex flex-col gap-1">
218
+ <label className="text-[10px] text-muted-foreground">{f.label}</label>
219
+ {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">
221
+ {f.options.map(o => <option key={o} value={o}>{o}</option>)}
222
+ </select>
223
+ ) : (
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]" />
225
+ )}
226
+ </div>
227
+ ))}
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>
231
+ );
232
+ }
233
+
234
+ export function TargetsPage() {
235
+ const [programs_, setPrograms] = useState([]);
236
+ const [targets_, setTargets] = useState([]);
237
+ const [selectedProgram, setSelectedProgram] = useState(null);
238
+ const [loading, setLoading] = useState(true);
239
+ const [syncStatus, setSyncStatus] = useState(null);
240
+ const [platformFilter, setPlatformFilter] = useState('all');
241
+
242
+ async function load() {
243
+ const [p, ss] = await Promise.all([getPrograms(), getSyncStatus()]);
244
+ setPrograms(p);
245
+ setSyncStatus(ss);
246
+ if (p.length > 0 && !selectedProgram) setSelectedProgram(p[0].id);
247
+ setLoading(false);
248
+ }
249
+
250
+ async function loadTargets() {
251
+ if (!selectedProgram) { setTargets([]); return; }
252
+ const t = await getTargets(selectedProgram);
253
+ setTargets(t);
254
+ }
255
+
256
+ useEffect(() => { load(); }, []);
257
+ useEffect(() => { if (selectedProgram) loadTargets(); }, [selectedProgram]);
258
+
259
+ async function handleSync(platform, options) {
260
+ let res;
261
+ if (platform) {
262
+ const stats = await syncTargetsFromPlatform(platform, options);
263
+ res = { [platform]: stats };
264
+ } else {
265
+ res = await syncAllTargets(options);
266
+ }
267
+ await load();
268
+ if (selectedProgram) loadTargets();
269
+ return res;
270
+ }
271
+
272
+ async function handleAddProgram(values) {
273
+ await createProgram({ name: values.name || 'Untitled', platform: values.platform || 'custom', url: values.url, maxBounty: values.maxBounty ? Number(values.maxBounty) : null });
274
+ load();
275
+ }
276
+
277
+ async function handleDeleteProgram(id) {
278
+ await deleteProgram(id);
279
+ if (selectedProgram === id) setSelectedProgram(null);
280
+ load();
281
+ }
282
+
283
+ async function handleAddTarget(values) {
284
+ await createTarget({ programId: selectedProgram, type: values.type || 'domain', value: values.value || '' });
285
+ loadTargets();
286
+ }
287
+
288
+ async function handleDeleteTarget(id) {
289
+ await deleteTarget(id);
290
+ loadTargets();
291
+ }
292
+
293
+ async function handleStatusChange(id, status) {
294
+ await updateTarget(id, { status });
295
+ loadTargets();
296
+ }
297
+
298
+ const filteredPrograms = platformFilter === 'all'
299
+ ? programs_
300
+ : programs_.filter(p => p.platform === platformFilter);
301
+
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>;
303
+
304
+ return (
305
+ <>
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>
309
+ </div>
310
+
311
+ {/* Sync Panel */}
312
+ <SyncPanel onSync={handleSync} syncStatus={syncStatus} />
313
+
314
+ {/* 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'}`}>
317
+ All ({programs_.length})
318
+ </button>
319
+ {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'}`}>
321
+ {p.label} ({programs_.filter(prog => prog.platform === p.id).length})
322
+ </button>
323
+ ))}
324
+ </div>
325
+
326
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
327
+ {/* Programs panel */}
328
+ <div className="lg:col-span-1">
329
+ <div className="flex items-center justify-between mb-3">
330
+ <h2 className="text-sm font-medium">Programs</h2>
331
+ <AddForm
332
+ fields={[
333
+ { name: 'name', label: 'Name', placeholder: 'Program name' },
334
+ { name: 'platform', label: 'Platform', type: 'select', options: ['hackerone', 'bugcrowd', 'intigriti', 'yeswehack', 'federacy', 'custom'] },
335
+ { name: 'url', label: 'URL', placeholder: 'https://...' },
336
+ { name: 'maxBounty', label: 'Max Bounty', placeholder: '10000', type: 'number' },
337
+ ]}
338
+ onSubmit={handleAddProgram}
339
+ />
340
+ </div>
341
+ <div className="flex flex-col gap-2 max-h-[600px] overflow-y-auto">
342
+ {filteredPrograms.length === 0 ? (
343
+ <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>
347
+ </div>
348
+ ) : filteredPrograms.map(p => (
349
+ <ProgramCard key={p.id} program={p} selected={selectedProgram === p.id} onSelect={setSelectedProgram} onDelete={handleDeleteProgram} />
350
+ ))}
351
+ </div>
352
+ </div>
353
+
354
+ {/* Targets panel */}
355
+ <div className="lg:col-span-2">
356
+ <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>
358
+ {selectedProgram && (
359
+ <AddForm
360
+ fields={[
361
+ { name: 'value', label: 'Target', placeholder: '*.example.com' },
362
+ { name: 'type', label: 'Type', type: 'select', options: TARGET_TYPES },
363
+ ]}
364
+ onSubmit={handleAddTarget}
365
+ />
366
+ )}
367
+ </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
+ )}
385
+ </div>
386
+ </div>
387
+ </>
388
+ );
389
+ }
@@ -0,0 +1,86 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { WrenchIcon, SpinnerIcon, CheckIcon, XIcon, ChevronDownIcon } from "./icons.js";
5
+ import { cn } from "../utils.js";
6
+ const TOOL_DISPLAY_NAMES = {
7
+ create_job: "Create Job",
8
+ get_job_status: "Check Job Status",
9
+ get_system_technical_specs: "Read Tech Docs",
10
+ get_skill_building_guide: "Read Skill Docs"
11
+ };
12
+ function getToolDisplayName(toolName) {
13
+ return TOOL_DISPLAY_NAMES[toolName] || toolName.replace(/_/g, " ");
14
+ }
15
+ function formatContent(content) {
16
+ if (content == null) return null;
17
+ if (typeof content === "string") {
18
+ try {
19
+ const parsed = JSON.parse(content);
20
+ return JSON.stringify(parsed, null, 2);
21
+ } catch {
22
+ return content;
23
+ }
24
+ }
25
+ return JSON.stringify(content, null, 2);
26
+ }
27
+ function ToolCall({ part }) {
28
+ const [expanded, setExpanded] = useState(false);
29
+ const toolName = part.toolName || (part.type?.startsWith("tool-") ? part.type.slice(5) : "tool");
30
+ const displayName = getToolDisplayName(toolName);
31
+ const state = part.state || "input-available";
32
+ const isRunning = state === "input-streaming" || state === "input-available";
33
+ const isDone = state === "output-available";
34
+ const isError = state === "output-error";
35
+ return /* @__PURE__ */ jsxs("div", { className: "my-1 rounded-lg border border-border bg-background", children: [
36
+ /* @__PURE__ */ jsxs(
37
+ "button",
38
+ {
39
+ onClick: () => setExpanded(!expanded),
40
+ className: "flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-muted/50 rounded-lg",
41
+ children: [
42
+ /* @__PURE__ */ jsx(WrenchIcon, { size: 14, className: "text-muted-foreground shrink-0" }),
43
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: displayName }),
44
+ /* @__PURE__ */ jsxs("span", { className: "ml-auto flex items-center gap-1.5 text-xs text-muted-foreground", children: [
45
+ isRunning && /* @__PURE__ */ jsxs(Fragment, { children: [
46
+ /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }),
47
+ /* @__PURE__ */ jsx("span", { children: "Running..." })
48
+ ] }),
49
+ isDone && /* @__PURE__ */ jsxs(Fragment, { children: [
50
+ /* @__PURE__ */ jsx(CheckIcon, { size: 12, className: "text-green-500" }),
51
+ /* @__PURE__ */ jsx("span", { children: "Done" })
52
+ ] }),
53
+ isError && /* @__PURE__ */ jsxs(Fragment, { children: [
54
+ /* @__PURE__ */ jsx(XIcon, { size: 12, className: "text-red-500" }),
55
+ /* @__PURE__ */ jsx("span", { children: "Error" })
56
+ ] })
57
+ ] }),
58
+ /* @__PURE__ */ jsx(
59
+ ChevronDownIcon,
60
+ {
61
+ size: 14,
62
+ className: cn(
63
+ "text-muted-foreground transition-transform shrink-0",
64
+ expanded && "rotate-180"
65
+ )
66
+ }
67
+ )
68
+ ]
69
+ }
70
+ ),
71
+ expanded && /* @__PURE__ */ jsxs("div", { className: "border-t border-border px-3 py-2 text-xs", children: [
72
+ part.input != null && /* @__PURE__ */ jsxs("div", { className: "mb-2", children: [
73
+ /* @__PURE__ */ jsx("div", { className: "font-medium text-muted-foreground mb-1", children: "Input" }),
74
+ /* @__PURE__ */ jsx("pre", { className: "whitespace-pre-wrap break-all rounded bg-muted p-2 text-foreground overflow-x-auto", children: formatContent(part.input) })
75
+ ] }),
76
+ part.output != null && /* @__PURE__ */ jsxs("div", { children: [
77
+ /* @__PURE__ */ jsx("div", { className: "font-medium text-muted-foreground mb-1", children: "Output" }),
78
+ /* @__PURE__ */ jsx("pre", { className: "whitespace-pre-wrap break-all rounded bg-muted p-2 text-foreground overflow-x-auto max-h-64 overflow-y-auto", children: formatContent(part.output) })
79
+ ] }),
80
+ part.input == null && part.output == null && /* @__PURE__ */ jsx("div", { className: "text-muted-foreground italic", children: "Waiting for data..." })
81
+ ] })
82
+ ] });
83
+ }
84
+ export {
85
+ ToolCall
86
+ };
@@ -0,0 +1,104 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { WrenchIcon, SpinnerIcon, CheckIcon, XIcon, ChevronDownIcon } from './icons.js';
5
+ import { cn } from '../utils.js';
6
+
7
+ const TOOL_DISPLAY_NAMES = {
8
+ create_job: 'Create Job',
9
+ get_job_status: 'Check Job Status',
10
+ get_system_technical_specs: 'Read Tech Docs',
11
+ get_skill_building_guide: 'Read Skill Docs',
12
+ };
13
+
14
+ function getToolDisplayName(toolName) {
15
+ return TOOL_DISPLAY_NAMES[toolName] || toolName.replace(/_/g, ' ');
16
+ }
17
+
18
+ function formatContent(content) {
19
+ if (content == null) return null;
20
+ if (typeof content === 'string') {
21
+ try {
22
+ const parsed = JSON.parse(content);
23
+ return JSON.stringify(parsed, null, 2);
24
+ } catch {
25
+ return content;
26
+ }
27
+ }
28
+ return JSON.stringify(content, null, 2);
29
+ }
30
+
31
+ export function ToolCall({ part }) {
32
+ const [expanded, setExpanded] = useState(false);
33
+
34
+ const toolName = part.toolName || (part.type?.startsWith('tool-') ? part.type.slice(5) : 'tool');
35
+ const displayName = getToolDisplayName(toolName);
36
+ const state = part.state || 'input-available';
37
+
38
+ const isRunning = state === 'input-streaming' || state === 'input-available';
39
+ const isDone = state === 'output-available';
40
+ const isError = state === 'output-error';
41
+
42
+ return (
43
+ <div className="my-1 rounded-lg border border-border bg-background">
44
+ <button
45
+ onClick={() => setExpanded(!expanded)}
46
+ className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-muted/50 rounded-lg"
47
+ >
48
+ <WrenchIcon size={14} className="text-muted-foreground shrink-0" />
49
+ <span className="font-medium text-foreground">{displayName}</span>
50
+ <span className="ml-auto flex items-center gap-1.5 text-xs text-muted-foreground">
51
+ {isRunning && (
52
+ <>
53
+ <SpinnerIcon size={12} />
54
+ <span>Running...</span>
55
+ </>
56
+ )}
57
+ {isDone && (
58
+ <>
59
+ <CheckIcon size={12} className="text-green-500" />
60
+ <span>Done</span>
61
+ </>
62
+ )}
63
+ {isError && (
64
+ <>
65
+ <XIcon size={12} className="text-red-500" />
66
+ <span>Error</span>
67
+ </>
68
+ )}
69
+ </span>
70
+ <ChevronDownIcon
71
+ size={14}
72
+ className={cn(
73
+ 'text-muted-foreground transition-transform shrink-0',
74
+ expanded && 'rotate-180'
75
+ )}
76
+ />
77
+ </button>
78
+
79
+ {expanded && (
80
+ <div className="border-t border-border px-3 py-2 text-xs">
81
+ {part.input != null && (
82
+ <div className="mb-2">
83
+ <div className="font-medium text-muted-foreground mb-1">Input</div>
84
+ <pre className="whitespace-pre-wrap break-all rounded bg-muted p-2 text-foreground overflow-x-auto">
85
+ {formatContent(part.input)}
86
+ </pre>
87
+ </div>
88
+ )}
89
+ {part.output != null && (
90
+ <div>
91
+ <div className="font-medium text-muted-foreground mb-1">Output</div>
92
+ <pre className="whitespace-pre-wrap break-all rounded bg-muted p-2 text-foreground overflow-x-auto max-h-64 overflow-y-auto">
93
+ {formatContent(part.output)}
94
+ </pre>
95
+ </div>
96
+ )}
97
+ {part.input == null && part.output == null && (
98
+ <div className="text-muted-foreground italic">Waiting for data...</div>
99
+ )}
100
+ </div>
101
+ )}
102
+ </div>
103
+ );
104
+ }