@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,77 @@
1
+ 'use client';
2
+
3
+ import { useRef, useEffect, useState } from 'react';
4
+ import { AnimatePresence } from 'framer-motion';
5
+ import { PreviewMessage, ThinkingMessage } from './message.js';
6
+ import { Greeting } from './greeting.js';
7
+ import { ArrowDown } from 'lucide-react';
8
+
9
+ export function Messages({ messages, status, onRetry, onEdit }) {
10
+ const containerRef = useRef(null);
11
+ const endRef = useRef(null);
12
+ const [isAtBottom, setIsAtBottom] = useState(true);
13
+
14
+ // Auto-scroll to bottom when new messages arrive
15
+ useEffect(() => {
16
+ if (isAtBottom && endRef.current) {
17
+ endRef.current.scrollIntoView({ behavior: 'smooth' });
18
+ }
19
+ }, [messages, status, isAtBottom]);
20
+
21
+ // Track scroll position
22
+ useEffect(() => {
23
+ const container = containerRef.current;
24
+ if (!container) return;
25
+
26
+ const handleScroll = () => {
27
+ const { scrollTop, scrollHeight, clientHeight } = container;
28
+ setIsAtBottom(scrollHeight - scrollTop - clientHeight < 40);
29
+ };
30
+
31
+ container.addEventListener('scroll', handleScroll, { passive: true });
32
+ return () => container.removeEventListener('scroll', handleScroll);
33
+ }, []);
34
+
35
+ const scrollToBottom = () => {
36
+ endRef.current?.scrollIntoView({ behavior: 'smooth' });
37
+ };
38
+
39
+ return (
40
+ <div className="relative flex-1">
41
+ <div
42
+ className="absolute inset-0 touch-pan-y overflow-y-auto scrollbar-thin"
43
+ ref={containerRef}
44
+ >
45
+ <div className="mx-auto flex min-w-0 max-w-4xl flex-col gap-4 px-4 py-4 md:gap-6 md:px-6">
46
+ {messages.length === 0 && <Greeting />}
47
+
48
+ <AnimatePresence mode="popLayout">
49
+ {messages.map((message, index) => (
50
+ <PreviewMessage
51
+ key={message.id}
52
+ message={message}
53
+ isLoading={status === 'streaming' && index === messages.length - 1}
54
+ onRetry={onRetry}
55
+ onEdit={onEdit}
56
+ />
57
+ ))}
58
+ </AnimatePresence>
59
+
60
+ {status === 'submitted' && <ThinkingMessage />}
61
+
62
+ <div className="min-h-[24px] shrink-0" ref={endRef} />
63
+ </div>
64
+ </div>
65
+
66
+ {!isAtBottom && (
67
+ <button
68
+ className="absolute bottom-4 left-1/2 z-10 -translate-x-1/2 rounded-full border border-white/[0.06] bg-[--card] p-2 shadow-lg hover:bg-white/[0.04]"
69
+ onClick={scrollToBottom}
70
+ aria-label="Scroll to bottom"
71
+ >
72
+ <ArrowDown className="size-4" />
73
+ </button>
74
+ )}
75
+ </div>
76
+ );
77
+ }
@@ -0,0 +1,56 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState, useEffect } from "react";
4
+ import { Streamdown } from "streamdown";
5
+ import { PageLayout } from "./page-layout.js";
6
+ import { BellIcon } from "./icons.js";
7
+ import { linkSafety } from "./message.js";
8
+ import { getNotifications, markNotificationsRead } from "../actions.js";
9
+ function timeAgo(ts) {
10
+ const seconds = Math.floor((Date.now() - ts) / 1e3);
11
+ if (seconds < 60) return "just now";
12
+ const minutes = Math.floor(seconds / 60);
13
+ if (minutes < 60) return `${minutes}m ago`;
14
+ const hours = Math.floor(minutes / 60);
15
+ if (hours < 24) return `${hours}h ago`;
16
+ const days = Math.floor(hours / 24);
17
+ if (days < 30) return `${days}d ago`;
18
+ const months = Math.floor(days / 30);
19
+ return `${months}mo ago`;
20
+ }
21
+ function NotificationsPage({ session }) {
22
+ const [notifications, setNotifications] = useState([]);
23
+ const [loading, setLoading] = useState(true);
24
+ useEffect(() => {
25
+ async function load() {
26
+ try {
27
+ const result = await getNotifications();
28
+ setNotifications(result);
29
+ await markNotificationsRead();
30
+ } catch (err) {
31
+ console.error("Failed to load notifications:", err);
32
+ } finally {
33
+ setLoading(false);
34
+ }
35
+ }
36
+ load();
37
+ }, []);
38
+ return /* @__PURE__ */ jsxs(PageLayout, { session, children: [
39
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between mb-6", children: /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: "Notifications" }) }),
40
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mb-4", children: [
41
+ notifications.length,
42
+ " ",
43
+ notifications.length === 1 ? "notification" : "notifications"
44
+ ] }),
45
+ loading ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: [...Array(5)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-14 animate-pulse rounded-md bg-border/50" }, i)) }) : notifications.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground py-8 text-center", children: "No notifications yet." }) : /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: notifications.map((n) => /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 p-4 border border-border rounded-lg", children: [
46
+ /* @__PURE__ */ jsx("div", { className: "mt-0.5 shrink-0 text-muted-foreground", children: /* @__PURE__ */ jsx(BellIcon, { size: 16 }) }),
47
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
48
+ /* @__PURE__ */ jsx("div", { className: "text-sm prose-sm", children: /* @__PURE__ */ jsx(Streamdown, { mode: "static", linkSafety, children: n.notification }) }),
49
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: timeAgo(n.createdAt) })
50
+ ] })
51
+ ] }, n.id)) })
52
+ ] });
53
+ }
54
+ export {
55
+ NotificationsPage
56
+ };
@@ -0,0 +1,87 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { Streamdown } from 'streamdown';
5
+ import { PageLayout } from './page-layout.js';
6
+ import { BellIcon } from './icons.js';
7
+ import { linkSafety } from './message.js';
8
+ import { getNotifications, markNotificationsRead } from '../actions.js';
9
+
10
+ function timeAgo(ts) {
11
+ const seconds = Math.floor((Date.now() - ts) / 1000);
12
+ if (seconds < 60) return 'just now';
13
+ const minutes = Math.floor(seconds / 60);
14
+ if (minutes < 60) return `${minutes}m ago`;
15
+ const hours = Math.floor(minutes / 60);
16
+ if (hours < 24) return `${hours}h ago`;
17
+ const days = Math.floor(hours / 24);
18
+ if (days < 30) return `${days}d ago`;
19
+ const months = Math.floor(days / 30);
20
+ return `${months}mo ago`;
21
+ }
22
+
23
+ export function NotificationsPage({ session }) {
24
+ const [notifications, setNotifications] = useState([]);
25
+ const [loading, setLoading] = useState(true);
26
+
27
+ useEffect(() => {
28
+ async function load() {
29
+ try {
30
+ const result = await getNotifications();
31
+ setNotifications(result);
32
+ // Mark all as read on view
33
+ await markNotificationsRead();
34
+ } catch (err) {
35
+ console.error('Failed to load notifications:', err);
36
+ } finally {
37
+ setLoading(false);
38
+ }
39
+ }
40
+ load();
41
+ }, []);
42
+
43
+ return (
44
+ <PageLayout session={session}>
45
+ {/* Header */}
46
+ <div className="flex items-center justify-between mb-6">
47
+ <h1 className="text-2xl font-semibold">Notifications</h1>
48
+ </div>
49
+
50
+ {/* Count */}
51
+ <p className="text-sm text-muted-foreground mb-4">
52
+ {notifications.length} {notifications.length === 1 ? 'notification' : 'notifications'}
53
+ </p>
54
+
55
+ {/* Notification list */}
56
+ {loading ? (
57
+ <div className="flex flex-col gap-3">
58
+ {[...Array(5)].map((_, i) => (
59
+ <div key={i} className="h-14 animate-pulse rounded-md bg-border/50" />
60
+ ))}
61
+ </div>
62
+ ) : notifications.length === 0 ? (
63
+ <p className="text-sm text-muted-foreground py-8 text-center">
64
+ No notifications yet.
65
+ </p>
66
+ ) : (
67
+ <div className="flex flex-col gap-3">
68
+ {notifications.map((n) => (
69
+ <div key={n.id} className="flex items-start gap-3 p-4 border border-border rounded-lg">
70
+ <div className="mt-0.5 shrink-0 text-muted-foreground">
71
+ <BellIcon size={16} />
72
+ </div>
73
+ <div className="flex-1 min-w-0">
74
+ <div className="text-sm prose-sm">
75
+ <Streamdown mode="static" linkSafety={linkSafety}>{n.notification}</Streamdown>
76
+ </div>
77
+ <span className="text-xs text-muted-foreground">
78
+ {timeAgo(n.createdAt)}
79
+ </span>
80
+ </div>
81
+ </div>
82
+ ))}
83
+ </div>
84
+ )}
85
+ </PageLayout>
86
+ );
87
+ }
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { AppSidebar } from "./app-sidebar.js";
4
+ import { SidebarProvider, SidebarInset } from "./ui/sidebar.js";
5
+ import { ChatNavProvider } from "./chat-nav-context.js";
6
+ function defaultNavigateToChat(id) {
7
+ if (id) {
8
+ window.location.href = `/chat/${id}`;
9
+ } else {
10
+ window.location.href = "/";
11
+ }
12
+ }
13
+ function PageLayout({ session, children }) {
14
+ return /* @__PURE__ */ jsx(ChatNavProvider, { value: { activeChatId: null, navigateToChat: defaultNavigateToChat }, children: /* @__PURE__ */ jsxs(SidebarProvider, { children: [
15
+ /* @__PURE__ */ jsx(AppSidebar, { user: session.user }),
16
+ /* @__PURE__ */ jsx(SidebarInset, { children: /* @__PURE__ */ jsx("div", { className: "flex flex-col h-full max-w-4xl mx-auto w-full px-4 py-6", children }) })
17
+ ] }) });
18
+ }
19
+ export {
20
+ PageLayout
21
+ };
@@ -0,0 +1,28 @@
1
+ 'use client';
2
+
3
+ import { AppSidebar } from './app-sidebar.js';
4
+ import { SidebarProvider, SidebarInset } from './ui/sidebar.js';
5
+ import { ChatNavProvider } from './chat-nav-context.js';
6
+
7
+ function defaultNavigateToChat(id) {
8
+ if (id) {
9
+ window.location.href = `/chat/${id}`;
10
+ } else {
11
+ window.location.href = '/';
12
+ }
13
+ }
14
+
15
+ export function PageLayout({ session, children }) {
16
+ return (
17
+ <ChatNavProvider value={{ activeChatId: null, navigateToChat: defaultNavigateToChat }}>
18
+ <SidebarProvider>
19
+ <AppSidebar user={session.user} />
20
+ <SidebarInset>
21
+ <div className="flex flex-col h-full max-w-4xl mx-auto w-full px-4 py-6">
22
+ {children}
23
+ </div>
24
+ </SidebarInset>
25
+ </SidebarProvider>
26
+ </ChatNavProvider>
27
+ );
28
+ }
@@ -0,0 +1,222 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState, useEffect } from "react";
4
+ import { PackageIcon, PlusIcon, SearchIcon, SpinnerIcon, CheckIcon, TrashIcon, ChevronDownIcon, DownloadIcon, GlobeIcon } from "./icons.js";
5
+ import { getCatalog, getInstalledTools, installTool, installCustomTool, uninstallTool, toggleTool, installFromGithub, getContainers, spawnContainer, stopContainer } from "../../registry/actions.js";
6
+ const CATEGORY_COLORS = {
7
+ recon: "bg-blue-500/10 text-blue-500",
8
+ scanning: "bg-cyan-500/10 text-cyan-500",
9
+ web: "bg-orange-500/10 text-orange-500",
10
+ osint: "bg-green-500/10 text-green-500",
11
+ cloud: "bg-purple-500/10 text-purple-500",
12
+ credential: "bg-red-500/10 text-red-500",
13
+ exploitation: "bg-red-600/10 text-red-600",
14
+ binary: "bg-gray-500/10 text-gray-500",
15
+ forensics: "bg-yellow-500/10 text-yellow-500",
16
+ automation: "bg-indigo-500/10 text-indigo-500",
17
+ custom: "bg-muted text-muted-foreground"
18
+ };
19
+ function CatalogToolCard({ tool, installed, onInstall, installing }) {
20
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 p-3 rounded-lg border bg-card hover:bg-accent/30 transition-colors", children: [
21
+ /* @__PURE__ */ jsx("div", { className: "shrink-0 rounded-md bg-muted p-2", children: /* @__PURE__ */ jsx(PackageIcon, { size: 14 }) }),
22
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
23
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium truncate", children: tool.name }),
24
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground mt-0.5 line-clamp-1", children: tool.description })
25
+ ] }),
26
+ /* @__PURE__ */ jsx("span", { className: `shrink-0 inline-flex rounded-full px-2 py-0.5 text-[10px] font-medium ${CATEGORY_COLORS[tool.category] || CATEGORY_COLORS.custom}`, children: tool.category }),
27
+ installed ? /* @__PURE__ */ jsxs("span", { className: "shrink-0 inline-flex items-center gap-1 rounded-full bg-green-500/10 text-green-500 px-2 py-0.5 text-[10px] font-medium", children: [
28
+ /* @__PURE__ */ jsx(CheckIcon, { size: 10 }),
29
+ " installed"
30
+ ] }) : /* @__PURE__ */ jsxs(
31
+ "button",
32
+ {
33
+ onClick: () => onInstall(tool.id),
34
+ disabled: installing === tool.id,
35
+ className: "shrink-0 inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium border hover:bg-accent/50 transition-colors disabled:opacity-50",
36
+ children: [
37
+ installing === tool.id ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(DownloadIcon, { size: 12 }),
38
+ "Install"
39
+ ]
40
+ }
41
+ )
42
+ ] });
43
+ }
44
+ function InstalledToolCard({ tool, onUninstall, onToggle, onSpawn }) {
45
+ const [expanded, setExpanded] = useState(false);
46
+ const disabled = !tool.enabled;
47
+ return /* @__PURE__ */ jsxs("div", { className: `rounded-lg border bg-card transition-opacity ${disabled ? "opacity-60" : ""}`, children: [
48
+ /* @__PURE__ */ jsxs("button", { onClick: () => setExpanded(!expanded), className: "flex items-center gap-3 w-full text-left p-3 hover:bg-accent/30 rounded-lg", children: [
49
+ /* @__PURE__ */ jsx("div", { className: "shrink-0 rounded-md bg-muted p-2", children: /* @__PURE__ */ jsx(PackageIcon, { size: 14 }) }),
50
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
51
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium truncate", children: tool.name }),
52
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground mt-0.5 truncate", children: tool.description })
53
+ ] }),
54
+ /* @__PURE__ */ jsx("span", { className: `shrink-0 inline-flex rounded-full px-2 py-0.5 text-[10px] font-medium ${CATEGORY_COLORS[tool.category] || CATEGORY_COLORS.custom}`, children: tool.category }),
55
+ /* @__PURE__ */ jsx("span", { className: `shrink-0 transition-transform ${expanded ? "rotate-180" : ""}`, children: /* @__PURE__ */ jsx(ChevronDownIcon, { size: 14 }) })
56
+ ] }),
57
+ expanded && /* @__PURE__ */ jsxs("div", { className: "border-t px-4 py-3 flex flex-col gap-2", children: [
58
+ tool.dockerImage && /* @__PURE__ */ jsxs("div", { className: "flex gap-2 text-xs", children: [
59
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Docker:" }),
60
+ /* @__PURE__ */ jsx("span", { className: "font-mono", children: tool.dockerImage })
61
+ ] }),
62
+ tool.installCmd && /* @__PURE__ */ jsxs("div", { className: "flex gap-2 text-xs", children: [
63
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Install:" }),
64
+ /* @__PURE__ */ jsx("code", { className: "font-mono bg-muted px-1.5 py-0.5 rounded text-[11px] break-all", children: tool.installCmd })
65
+ ] }),
66
+ tool.sourceUrl && /* @__PURE__ */ jsxs("div", { className: "flex gap-2 text-xs", children: [
67
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Source:" }),
68
+ /* @__PURE__ */ jsx("a", { href: tool.sourceUrl, target: "_blank", rel: "noopener", className: "text-blue-500 hover:underline truncate", children: tool.sourceUrl })
69
+ ] }),
70
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-1", children: [
71
+ tool.dockerImage && /* @__PURE__ */ jsx("button", { onClick: () => onSpawn(tool.id), className: "inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium border hover:bg-accent/50", children: "Spawn Container" }),
72
+ /* @__PURE__ */ jsx("button", { onClick: () => onToggle(tool.id), className: "inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium border hover:bg-accent/50", children: tool.enabled ? "Disable" : "Enable" }),
73
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
74
+ /* @__PURE__ */ jsx("button", { onClick: () => onUninstall(tool.id), className: "text-xs text-muted-foreground hover:text-destructive", children: "Uninstall" })
75
+ ] })
76
+ ] })
77
+ ] });
78
+ }
79
+ function GithubInstaller({ onInstall }) {
80
+ const [url, setUrl] = useState("");
81
+ const [loading, setLoading] = useState(false);
82
+ const [result, setResult] = useState(null);
83
+ async function handleInstall() {
84
+ if (!url) return;
85
+ setLoading(true);
86
+ setResult(null);
87
+ const res = await onInstall(url);
88
+ setResult(res);
89
+ setLoading(false);
90
+ if (!res.error) setUrl("");
91
+ }
92
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card p-4 mb-4", children: [
93
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium mb-2", children: "Install from GitHub" }),
94
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-3", children: "Paste a GitHub repository URL to add any tool to your registry." }),
95
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
96
+ /* @__PURE__ */ jsx("input", { placeholder: "https://github.com/owner/repo", value: url, onChange: (e) => setUrl(e.target.value), className: "flex-1 text-sm border rounded-md px-3 py-2 bg-background font-mono" }),
97
+ /* @__PURE__ */ jsxs("button", { onClick: handleInstall, disabled: loading || !url, className: "inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-medium bg-foreground text-background hover:opacity-90 disabled:opacity-50", children: [
98
+ loading ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(DownloadIcon, { size: 12 }),
99
+ " Install"
100
+ ] })
101
+ ] }),
102
+ result && /* @__PURE__ */ jsx("p", { className: `text-xs mt-2 ${result.error ? "text-destructive" : "text-green-500"}`, children: result.error || `Installed ${result.name}` })
103
+ ] });
104
+ }
105
+ function RegistryPage() {
106
+ const [catalog, setCatalog] = useState({ tools: [], categories: [] });
107
+ const [installed, setInstalled] = useState([]);
108
+ const [containers, setContainers] = useState([]);
109
+ const [loading, setLoading] = useState(true);
110
+ const [search, setSearch] = useState("");
111
+ const [activeCategory, setActiveCategory] = useState("all");
112
+ const [installing, setInstalling] = useState(null);
113
+ const [tab, setTab] = useState("catalog");
114
+ async function load() {
115
+ const [c, i, cont] = await Promise.all([getCatalog(), getInstalledTools(), getContainers()]);
116
+ setCatalog(c);
117
+ setInstalled(i);
118
+ setContainers(cont);
119
+ setLoading(false);
120
+ }
121
+ useEffect(() => {
122
+ load();
123
+ }, []);
124
+ const installedSlugs = new Set(installed.map((t) => t.slug));
125
+ async function handleInstall(catalogId) {
126
+ setInstalling(catalogId);
127
+ await installTool(catalogId);
128
+ await load();
129
+ setInstalling(null);
130
+ }
131
+ async function handleGithubInstall(url) {
132
+ const result = await installFromGithub(url);
133
+ if (!result.error) await load();
134
+ return result;
135
+ }
136
+ async function handleUninstall(id) {
137
+ await uninstallTool(id);
138
+ load();
139
+ }
140
+ async function handleToggle(id) {
141
+ await toggleTool(id);
142
+ load();
143
+ }
144
+ async function handleSpawn(toolId) {
145
+ await spawnContainer(toolId);
146
+ load();
147
+ }
148
+ async function handleStop(id) {
149
+ await stopContainer(id);
150
+ load();
151
+ }
152
+ const filteredCatalog = catalog.tools.filter((t) => {
153
+ if (activeCategory !== "all" && t.category !== activeCategory) return false;
154
+ if (search && !t.name.toLowerCase().includes(search.toLowerCase()) && !t.description.toLowerCase().includes(search.toLowerCase())) return false;
155
+ return true;
156
+ });
157
+ if (loading) return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: [...Array(5)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-14 animate-pulse rounded-lg bg-border/50" }, i)) });
158
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
159
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between mb-4", children: /* @__PURE__ */ jsxs("div", { children: [
160
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: "Toolbox" }),
161
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mt-1", children: [
162
+ installed.length,
163
+ " installed, ",
164
+ catalog.tools.length,
165
+ " available in catalog"
166
+ ] })
167
+ ] }) }),
168
+ /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b border-border mb-4", children: [
169
+ { id: "catalog", label: `Catalog (${catalog.tools.length})` },
170
+ { id: "installed", label: `Installed (${installed.length})` },
171
+ { id: "containers", label: `Containers (${containers.filter((c) => c.status === "running").length})` }
172
+ ].map((t) => /* @__PURE__ */ jsx("button", { onClick: () => setTab(t.id), className: `px-3 py-2 text-sm font-medium border-b-2 transition-colors ${tab === t.id ? "border-foreground text-foreground" : "border-transparent text-muted-foreground hover:text-foreground"}`, children: t.label }, t.id)) }),
173
+ tab === "catalog" && /* @__PURE__ */ jsxs(Fragment, { children: [
174
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col sm:flex-row gap-3 mb-4", children: /* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
175
+ /* @__PURE__ */ jsx(SearchIcon, { size: 14 }),
176
+ /* @__PURE__ */ jsx("input", { placeholder: "Search tools...", value: search, onChange: (e) => setSearch(e.target.value), className: "w-full text-sm border rounded-md pl-3 pr-3 py-2 bg-background" })
177
+ ] }) }),
178
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1 mb-4 overflow-x-auto pb-1", children: [
179
+ /* @__PURE__ */ jsxs("button", { onClick: () => setActiveCategory("all"), className: `shrink-0 px-3 py-1 rounded-full text-xs font-medium transition-colors ${activeCategory === "all" ? "bg-foreground text-background" : "bg-muted text-muted-foreground hover:text-foreground"}`, children: [
180
+ "All (",
181
+ catalog.tools.length,
182
+ ")"
183
+ ] }),
184
+ catalog.categories.map((c) => /* @__PURE__ */ jsxs("button", { onClick: () => setActiveCategory(c.id), className: `shrink-0 px-3 py-1 rounded-full text-xs font-medium transition-colors ${activeCategory === c.id ? "bg-foreground text-background" : "bg-muted text-muted-foreground hover:text-foreground"}`, children: [
185
+ c.name,
186
+ " (",
187
+ catalog.tools.filter((t) => t.category === c.id).length,
188
+ ")"
189
+ ] }, c.id))
190
+ ] }),
191
+ /* @__PURE__ */ jsx(GithubInstaller, { onInstall: handleGithubInstall }),
192
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
193
+ filteredCatalog.map((t) => /* @__PURE__ */ jsx(CatalogToolCard, { tool: t, installed: installedSlugs.has(t.id), onInstall: handleInstall, installing }, t.id)),
194
+ filteredCatalog.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-center text-sm text-muted-foreground py-8", children: "No tools match your search." })
195
+ ] })
196
+ ] }),
197
+ tab === "installed" && /* @__PURE__ */ jsx(Fragment, { children: installed.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-16 text-center", children: [
198
+ /* @__PURE__ */ jsx("div", { className: "rounded-full bg-muted p-4 mb-4", children: /* @__PURE__ */ jsx(PackageIcon, { size: 24 }) }),
199
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium mb-1", children: "No tools installed" }),
200
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Browse the catalog to install security tools for your agents." })
201
+ ] }) : /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: installed.map((t) => /* @__PURE__ */ jsx(InstalledToolCard, { tool: t, onUninstall: handleUninstall, onToggle: handleToggle, onSpawn: handleSpawn }, t.id)) }) }),
202
+ tab === "containers" && /* @__PURE__ */ jsx(Fragment, { children: containers.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-16 text-center", children: [
203
+ /* @__PURE__ */ jsx("div", { className: "rounded-full bg-muted p-4 mb-4", children: /* @__PURE__ */ jsx(PackageIcon, { size: 24 }) }),
204
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium mb-1", children: "No containers running" }),
205
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Spawn containers from installed tools to give agents terminal access." })
206
+ ] }) : /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: containers.map((c) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 p-3 rounded-lg border bg-card", children: [
207
+ /* @__PURE__ */ jsx("div", { className: `shrink-0 w-2 h-2 rounded-full ${c.status === "running" ? "bg-green-500 animate-pulse" : c.status === "stopped" ? "bg-muted-foreground" : "bg-red-500"}` }),
208
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
209
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-mono truncate", children: c.imageName }),
210
+ /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: [
211
+ c.containerId?.slice(0, 12),
212
+ " \u2014 ",
213
+ c.status
214
+ ] })
215
+ ] }),
216
+ c.status === "running" && /* @__PURE__ */ jsx("button", { onClick: () => handleStop(c.id), className: "text-xs text-muted-foreground hover:text-destructive", children: "Stop" })
217
+ ] }, c.id)) }) })
218
+ ] });
219
+ }
220
+ export {
221
+ RegistryPage
222
+ };