@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,89 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { hashSync, genSaltSync, compare } from 'bcrypt-ts';
3
+ import { eq, sql } from 'drizzle-orm';
4
+ import { getDb } from './index.js';
5
+ import { users } from './schema.js';
6
+
7
+ /**
8
+ * Get the total number of users.
9
+ * Used to detect first-time setup (no users = needs setup).
10
+ * @returns {number}
11
+ */
12
+ export function getUserCount() {
13
+ const db = getDb();
14
+ const result = db.select({ count: sql`count(*)` }).from(users).get();
15
+ return result?.count ?? 0;
16
+ }
17
+
18
+ /**
19
+ * Find a user by email address.
20
+ * @param {string} email
21
+ * @returns {object|undefined}
22
+ */
23
+ export function getUserByEmail(email) {
24
+ const db = getDb();
25
+ return db.select().from(users).where(eq(users.email, email.toLowerCase())).get();
26
+ }
27
+
28
+ /**
29
+ * Create a new user with a hashed password.
30
+ * @param {string} email
31
+ * @param {string} password - Plain text password (will be hashed)
32
+ * @returns {object} The created user (without password_hash)
33
+ */
34
+ export async function createUser(email, password) {
35
+ const db = getDb();
36
+ const now = Date.now();
37
+ const passwordHash = hashSync(password, genSaltSync(10));
38
+
39
+ const user = {
40
+ id: randomUUID(),
41
+ email: email.toLowerCase(),
42
+ passwordHash: passwordHash,
43
+ role: 'admin',
44
+ createdAt: now,
45
+ updatedAt: now,
46
+ };
47
+
48
+ db.insert(users).values(user).run();
49
+
50
+ return { id: user.id, email: user.email, role: user.role };
51
+ }
52
+
53
+ /**
54
+ * Atomically create the first user (admin) if no users exist.
55
+ * Uses a transaction to prevent race conditions — only one caller wins.
56
+ * @param {string} email
57
+ * @param {string} password - Plain text password (will be hashed)
58
+ * @returns {object|null} The created user, or null if users already exist
59
+ */
60
+ export function createFirstUser(email, password) {
61
+ const db = getDb();
62
+ return db.transaction((tx) => {
63
+ const count = tx.select({ count: sql`count(*)` }).from(users).get();
64
+ if (count?.count > 0) return null;
65
+
66
+ const now = Date.now();
67
+ const passwordHash = hashSync(password, genSaltSync(10));
68
+ const user = {
69
+ id: randomUUID(),
70
+ email: email.toLowerCase(),
71
+ passwordHash: passwordHash,
72
+ role: 'admin',
73
+ createdAt: now,
74
+ updatedAt: now,
75
+ };
76
+ tx.insert(users).values(user).run();
77
+ return { id: user.id, email: user.email, role: user.role };
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Verify a password against a user's stored hash.
83
+ * @param {object} user - User object with password_hash field
84
+ * @param {string} password - Plain text password to verify
85
+ * @returns {Promise<boolean>}
86
+ */
87
+ export async function verifyPassword(user, password) {
88
+ return compare(password, user.passwordHash);
89
+ }
@@ -0,0 +1,104 @@
1
+ 'use server';
2
+
3
+ import fs from 'fs';
4
+ import { auth } from '../auth/index.js';
5
+ import { mcpServersFile } from '../paths.js';
6
+
7
+ async function requireAuth() {
8
+ const session = await auth();
9
+ if (!session?.user?.id) {
10
+ throw new Error('Unauthorized');
11
+ }
12
+ return session.user;
13
+ }
14
+
15
+ /**
16
+ * Read MCP_SERVERS.json config.
17
+ */
18
+ export async function getMcpServers() {
19
+ await requireAuth();
20
+ if (!fs.existsSync(mcpServersFile)) return [];
21
+ try {
22
+ return JSON.parse(fs.readFileSync(mcpServersFile, 'utf8'));
23
+ } catch {
24
+ return [];
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Get status of loaded MCP tools from the client singleton.
30
+ */
31
+ export async function getMcpStatus() {
32
+ await requireAuth();
33
+ try {
34
+ const { loadMcpTools } = await import('./client.js');
35
+ const tools = await loadMcpTools();
36
+ return {
37
+ loaded: true,
38
+ toolCount: tools.length,
39
+ tools: tools.map(t => ({ name: t.name, description: t.description })),
40
+ };
41
+ } catch (err) {
42
+ return { loaded: false, toolCount: 0, tools: [], error: err.message };
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Get metadata about thepopebot's own MCP server capabilities.
48
+ */
49
+ export async function getOwnMcpServerInfo() {
50
+ await requireAuth();
51
+ return {
52
+ tools: [
53
+ { name: 'create_job', description: 'Create a new autonomous agent job' },
54
+ { name: 'get_job_status', description: 'Get the status of running or recent agent jobs' },
55
+ { name: 'chat', description: 'Send a message to the AI chat agent' },
56
+ { name: 'list_agents', description: 'List all discovered agent profiles' },
57
+ { name: 'get_agent_profile', description: 'Get the full profile for a specific agent' },
58
+ ],
59
+ resources: [
60
+ { uri: 'agent://agents', description: 'List of all agent profiles' },
61
+ { uri: 'agent://{agentId}/soul', description: 'SOUL.md for a specific agent' },
62
+ { uri: 'config://soul', description: 'Main SOUL.md configuration' },
63
+ { uri: 'config://crons', description: 'Cron jobs configuration' },
64
+ { uri: 'config://triggers', description: 'Webhook triggers configuration' },
65
+ ],
66
+ prompts: [
67
+ { name: 'agent-prompt', description: 'System prompt for an agent' },
68
+ ],
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Test an external MCP tool by invoking it.
74
+ */
75
+ export async function testMcpTool(toolName, args = {}) {
76
+ await requireAuth();
77
+ try {
78
+ const { loadMcpTools } = await import('./client.js');
79
+ const tools = await loadMcpTools();
80
+ const tool = tools.find(t => t.name === toolName);
81
+ if (!tool) return { error: `Tool "${toolName}" not found` };
82
+ const result = await tool.invoke(args);
83
+ return { success: true, result };
84
+ } catch (err) {
85
+ return { error: err.message };
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Reset the MCP client and agent singletons, then reload tools.
91
+ */
92
+ export async function reloadMcpClient() {
93
+ await requireAuth();
94
+ try {
95
+ const { resetMcpClient, loadMcpTools } = await import('./client.js');
96
+ const { resetAgent } = await import('../ai/agent.js');
97
+ await resetMcpClient();
98
+ resetAgent();
99
+ const tools = await loadMcpTools();
100
+ return { success: true, toolCount: tools.length };
101
+ } catch (err) {
102
+ return { error: err.message };
103
+ }
104
+ }
@@ -0,0 +1,79 @@
1
+ import fs from 'fs';
2
+ import { mcpServersFile } from '../paths.js';
3
+
4
+ let _client = null;
5
+ let _promise = null;
6
+
7
+ /**
8
+ * Load MCP tools from configured external servers in MCP_SERVERS.json.
9
+ * Uses @langchain/mcp-adapters MultiServerMCPClient to connect and
10
+ * convert external MCP tools into LangChain-compatible tools.
11
+ *
12
+ * Returns an empty array if no servers are configured or the config file doesn't exist.
13
+ * Uses promise-based singleton to prevent concurrent initialization races.
14
+ */
15
+ export function loadMcpTools() {
16
+ if (!_promise) {
17
+ _promise = _loadMcpToolsImpl().catch(err => {
18
+ _promise = null; // allow retry on failure
19
+ throw err;
20
+ });
21
+ }
22
+ return _promise;
23
+ }
24
+
25
+ async function _loadMcpToolsImpl() {
26
+ // Read config
27
+ if (!fs.existsSync(mcpServersFile)) return [];
28
+
29
+ let servers;
30
+ try {
31
+ servers = JSON.parse(fs.readFileSync(mcpServersFile, 'utf8'));
32
+ } catch {
33
+ return [];
34
+ }
35
+
36
+ if (!Array.isArray(servers) || servers.length === 0) return [];
37
+
38
+ // Filter to enabled servers only
39
+ const enabled = servers.filter(s => s.enabled !== false);
40
+ if (enabled.length === 0) return [];
41
+
42
+ // Build config for MultiServerMCPClient
43
+ const mcpConfig = {};
44
+ for (const server of enabled) {
45
+ if (!server.name || !server.url) continue;
46
+ mcpConfig[server.name] = {
47
+ transport: server.transport || 'http',
48
+ url: server.url,
49
+ headers: server.headers || {},
50
+ };
51
+ }
52
+
53
+ if (Object.keys(mcpConfig).length === 0) return [];
54
+
55
+ try {
56
+ const { MultiServerMCPClient } = await import('@langchain/mcp-adapters');
57
+ _client = new MultiServerMCPClient(mcpConfig);
58
+ // Race against a timeout to prevent hanging on unresponsive MCP servers
59
+ const tools = await Promise.race([
60
+ _client.getTools(),
61
+ new Promise((_, reject) => setTimeout(() => reject(new Error('MCP tool loading timed out after 15s')), 15000)),
62
+ ]);
63
+ return tools;
64
+ } catch (err) {
65
+ console.error('Failed to load MCP tools:', err.message);
66
+ return [];
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Reset the MCP client singleton so tools are reloaded on next call.
72
+ */
73
+ export async function resetMcpClient() {
74
+ if (_client) {
75
+ try { await _client.close(); } catch {}
76
+ }
77
+ _client = null;
78
+ _promise = null;
79
+ }
@@ -0,0 +1,57 @@
1
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
2
+ import { createMcpServer } from './server.js';
3
+ import { createRequire } from 'module';
4
+
5
+ const require = createRequire(import.meta.url);
6
+ const { version } = require('../../package.json');
7
+
8
+ // Cache the MCP server instance — it's stateless, only the transport is per-request
9
+ let _mcpServer = null;
10
+ function getMcpServer() {
11
+ if (!_mcpServer) _mcpServer = createMcpServer();
12
+ return _mcpServer;
13
+ }
14
+
15
+ /**
16
+ * Handle MCP HTTP requests at /api/mcp.
17
+ * Auth is handled by the main API router (api/index.js checkAuth).
18
+ *
19
+ * - POST: JSON-RPC messages (tool calls, resource reads, etc.)
20
+ * - GET: Server capability info
21
+ * - DELETE: Not supported (stateless, no sessions)
22
+ */
23
+ export async function handleMcpRequest(request) {
24
+ const method = request.method;
25
+
26
+ if (method === 'DELETE') {
27
+ return Response.json({ error: 'Method not allowed' }, { status: 405 });
28
+ }
29
+
30
+ try {
31
+ if (method === 'POST') {
32
+ const server = getMcpServer();
33
+ const transport = new StreamableHTTPServerTransport({ enableJsonResponse: true });
34
+ await server.connect(transport);
35
+ const body = await request.json();
36
+ const result = await transport.handleRequest(request, body);
37
+ if (result instanceof Response) return result;
38
+ return Response.json(result);
39
+ }
40
+
41
+ if (method === 'GET') {
42
+ return Response.json({
43
+ name: 'thepopebot',
44
+ version,
45
+ description: 'thepopebot MCP server — autonomous AI agent platform',
46
+ tools: ['create_job', 'get_job_status', 'chat', 'list_agents', 'get_agent_profile'],
47
+ resources: ['agent://agents', 'agent://{agentId}/soul', 'config://soul', 'config://crons', 'config://triggers'],
48
+ prompts: ['agent-prompt'],
49
+ });
50
+ }
51
+
52
+ return Response.json({ error: 'Method not allowed' }, { status: 405 });
53
+ } catch (err) {
54
+ console.error('MCP handler error:', err);
55
+ return Response.json({ error: 'Internal server error' }, { status: 500 });
56
+ }
57
+ }
@@ -0,0 +1,165 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import fs from 'fs';
4
+ import { createRequire } from 'module';
5
+ import { soulMd, cronsFile, triggersFile } from '../paths.js';
6
+
7
+ const require = createRequire(import.meta.url);
8
+ const { version } = require('../../package.json');
9
+
10
+ /**
11
+ * Create a fresh McpServer instance with all tools, resources, and prompts registered.
12
+ */
13
+ export function createMcpServer() {
14
+ const server = new McpServer({
15
+ name: 'thepopebot',
16
+ version,
17
+ });
18
+
19
+ // ─────────────────────────────────────────────────────────────────────────
20
+ // Tools
21
+ // ─────────────────────────────────────────────────────────────────────────
22
+
23
+ server.tool(
24
+ 'create_job',
25
+ 'Create a new autonomous agent job. The agent runs in a Docker container and creates a PR with results.',
26
+ { job_description: z.string().describe('Description of the task for the agent to perform') },
27
+ async ({ job_description }) => {
28
+ const { createJob } = await import('../tools/create-job.js');
29
+ const result = await createJob(job_description);
30
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
31
+ }
32
+ );
33
+
34
+ server.tool(
35
+ 'get_job_status',
36
+ 'Get the status of running or recent agent jobs.',
37
+ { job_id: z.string().optional().describe('Optional specific job ID to check') },
38
+ async ({ job_id }) => {
39
+ const { getJobStatus } = await import('../tools/github.js');
40
+ const result = await getJobStatus(job_id);
41
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
42
+ }
43
+ );
44
+
45
+ server.tool(
46
+ 'chat',
47
+ 'Send a message to the AI chat agent and get a response. Use agent_id to route to a specific Harbinger agent profile.',
48
+ {
49
+ thread_id: z.string().describe('Conversation thread ID'),
50
+ message: z.string().describe('Message to send'),
51
+ agent_id: z.string().optional().describe('Optional agent profile ID to route to (e.g. recon-scout, web-hacker)'),
52
+ },
53
+ async ({ thread_id, message, agent_id }) => {
54
+ const { chat } = await import('../ai/index.js');
55
+ const response = await chat(thread_id, message, [], agent_id ? { agentId: agent_id } : {});
56
+ return { content: [{ type: 'text', text: response }] };
57
+ }
58
+ );
59
+
60
+ server.tool(
61
+ 'list_agents',
62
+ 'List all discovered agent profiles with their identities and roles.',
63
+ {},
64
+ async () => {
65
+ const { discoverAgents } = await import('../agents.js');
66
+ const agents = discoverAgents();
67
+ return { content: [{ type: 'text', text: JSON.stringify(agents.map(a => ({ id: a.id, codename: a.codename, name: a.name, role: a.role }))) }] };
68
+ }
69
+ );
70
+
71
+ server.tool(
72
+ 'get_agent_profile',
73
+ 'Get the full profile for a specific agent including SOUL, skills, and tools.',
74
+ { agent_id: z.string().describe('Directory name of the agent profile') },
75
+ async ({ agent_id }) => {
76
+ const { loadAgentProfile } = await import('../agents.js');
77
+ const profile = loadAgentProfile(agent_id);
78
+ if (!profile) return { content: [{ type: 'text', text: JSON.stringify({ error: 'Agent not found' }) }] };
79
+ return { content: [{ type: 'text', text: JSON.stringify(profile) }] };
80
+ }
81
+ );
82
+
83
+ // ─────────────────────────────────────────────────────────────────────────
84
+ // Resources
85
+ // ─────────────────────────────────────────────────────────────────────────
86
+
87
+ server.resource(
88
+ 'agents-list',
89
+ 'agent://agents',
90
+ 'List of all discovered agent profiles',
91
+ async () => {
92
+ const { discoverAgents } = await import('../agents.js');
93
+ const agents = discoverAgents();
94
+ return { contents: [{ uri: 'agent://agents', text: JSON.stringify(agents.map(a => ({ id: a.id, codename: a.codename, name: a.name, role: a.role }))) }] };
95
+ }
96
+ );
97
+
98
+ server.resource(
99
+ 'agent-soul',
100
+ 'agent://{agentId}/soul',
101
+ 'SOUL.md system prompt for a specific agent',
102
+ async (uri) => {
103
+ const agentId = uri.pathname.split('/')[0] || uri.host;
104
+ const { loadAgentProfile } = await import('../agents.js');
105
+ const profile = loadAgentProfile(agentId);
106
+ const text = profile?.soul || 'Agent not found or has no SOUL.md';
107
+ return { contents: [{ uri: uri.href, text }] };
108
+ }
109
+ );
110
+
111
+ server.resource(
112
+ 'config-soul',
113
+ 'config://soul',
114
+ 'The main SOUL.md configuration file',
115
+ async () => {
116
+ const text = fs.existsSync(soulMd) ? fs.readFileSync(soulMd, 'utf8') : 'SOUL.md not found';
117
+ return { contents: [{ uri: 'config://soul', text }] };
118
+ }
119
+ );
120
+
121
+ server.resource(
122
+ 'config-crons',
123
+ 'config://crons',
124
+ 'Configured cron jobs from CRONS.json',
125
+ async () => {
126
+ const text = fs.existsSync(cronsFile) ? fs.readFileSync(cronsFile, 'utf8') : '[]';
127
+ return { contents: [{ uri: 'config://crons', text }] };
128
+ }
129
+ );
130
+
131
+ server.resource(
132
+ 'config-triggers',
133
+ 'config://triggers',
134
+ 'Configured webhook triggers from TRIGGERS.json',
135
+ async () => {
136
+ const text = fs.existsSync(triggersFile) ? fs.readFileSync(triggersFile, 'utf8') : '[]';
137
+ return { contents: [{ uri: 'config://triggers', text }] };
138
+ }
139
+ );
140
+
141
+ // ─────────────────────────────────────────────────────────────────────────
142
+ // Prompts
143
+ // ─────────────────────────────────────────────────────────────────────────
144
+
145
+ server.prompt(
146
+ 'agent-prompt',
147
+ 'Get the system prompt for an agent (default: main event handler)',
148
+ { agent_id: z.string().optional().describe('Optional agent profile ID') },
149
+ async ({ agent_id }) => {
150
+ if (agent_id) {
151
+ const { loadAgentProfile } = await import('../agents.js');
152
+ const profile = loadAgentProfile(agent_id);
153
+ if (profile?.soul) {
154
+ return { messages: [{ role: 'user', content: { type: 'text', text: profile.soul } }] };
155
+ }
156
+ }
157
+ const { eventHandlerMd } = await import('../paths.js');
158
+ const { render_md } = await import('../utils/render-md.js');
159
+ const text = render_md(eventHandlerMd);
160
+ return { messages: [{ role: 'user', content: { type: 'text', text } }] };
161
+ }
162
+ );
163
+
164
+ return server;
165
+ }
package/lib/paths.js ADDED
@@ -0,0 +1,46 @@
1
+ import path from 'path';
2
+
3
+ /**
4
+ * Central path resolver for thepopebot.
5
+ * All paths resolve from process.cwd() (the user's project root).
6
+ */
7
+
8
+ const PROJECT_ROOT = process.cwd();
9
+
10
+ export {
11
+ PROJECT_ROOT,
12
+ };
13
+
14
+ // config/ files
15
+ export const configDir = path.join(PROJECT_ROOT, 'config');
16
+ export const cronsFile = path.join(PROJECT_ROOT, 'config', 'CRONS.json');
17
+ export const triggersFile = path.join(PROJECT_ROOT, 'config', 'TRIGGERS.json');
18
+ export const mcpServersFile = path.join(PROJECT_ROOT, 'config', 'MCP_SERVERS.json');
19
+ export const eventHandlerMd = path.join(PROJECT_ROOT, 'config', 'EVENT_HANDLER.md');
20
+ export const jobSummaryMd = path.join(PROJECT_ROOT, 'config', 'JOB_SUMMARY.md');
21
+ export const soulMd = path.join(PROJECT_ROOT, 'config', 'SOUL.md');
22
+ export const claudeMd = path.join(PROJECT_ROOT, 'CLAUDE.md');
23
+ export const skillGuidePath = path.join(PROJECT_ROOT, 'config', 'SKILL_BUILDING_GUIDE.md');
24
+
25
+ // Active skills (resolves through .pi/skills → skills/active symlink)
26
+ export const piSkillsDir = path.join(PROJECT_ROOT, '.pi', 'skills');
27
+
28
+ // Working directories for command-type actions
29
+ export const cronDir = path.join(PROJECT_ROOT, 'cron');
30
+ export const triggersDir = path.join(PROJECT_ROOT, 'triggers');
31
+
32
+ // Logs
33
+ export const logsDir = path.join(PROJECT_ROOT, 'logs');
34
+
35
+ // Data (SQLite memory, etc.)
36
+ export const dataDir = path.join(PROJECT_ROOT, 'data');
37
+
38
+ // Database
39
+ export const thepopebotDb = process.env.DATABASE_PATH || path.join(PROJECT_ROOT, 'data', 'thepopebot.sqlite');
40
+
41
+ // Agents
42
+ export const agentsDir = path.join(PROJECT_ROOT, 'agents');
43
+ export const agentSharedDir = path.join(PROJECT_ROOT, 'agents', 'shared');
44
+
45
+ // .env
46
+ export const envFile = path.join(PROJECT_ROOT, '.env');