@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,148 @@
1
+ import { ChannelAdapter } from './base.js';
2
+ import {
3
+ sendMessage,
4
+ downloadFile,
5
+ reactToMessage,
6
+ startTypingIndicator,
7
+ } from '../tools/telegram.js';
8
+ import { isWhisperEnabled, transcribeAudio } from '../tools/openai.js';
9
+
10
+ class TelegramAdapter extends ChannelAdapter {
11
+ constructor(botToken) {
12
+ super();
13
+ this.botToken = botToken;
14
+ }
15
+
16
+ /**
17
+ * Parse a Telegram webhook update into normalized message data.
18
+ * Handles: text, voice/audio (transcribed), photos, documents.
19
+ * Returns null if the update should be ignored.
20
+ */
21
+ async receive(request) {
22
+ const { TELEGRAM_WEBHOOK_SECRET, TELEGRAM_CHAT_ID, TELEGRAM_VERIFICATION } = process.env;
23
+
24
+ // Validate secret token (required)
25
+ if (!TELEGRAM_WEBHOOK_SECRET) {
26
+ console.error('[telegram] TELEGRAM_WEBHOOK_SECRET not configured — rejecting webhook');
27
+ return null;
28
+ }
29
+ const headerSecret = request.headers.get('x-telegram-bot-api-secret-token');
30
+ if (headerSecret !== TELEGRAM_WEBHOOK_SECRET) {
31
+ return null;
32
+ }
33
+
34
+ const update = await request.json();
35
+ const message = update.message || update.edited_message;
36
+
37
+ if (!message || !message.chat || !this.botToken) return null;
38
+
39
+ const chatId = String(message.chat.id);
40
+ let text = message.text || null;
41
+ const attachments = [];
42
+
43
+ // Check for verification code — works even before TELEGRAM_CHAT_ID is set
44
+ if (TELEGRAM_VERIFICATION && text === TELEGRAM_VERIFICATION) {
45
+ await sendMessage(this.botToken, chatId, `Your chat ID:\n<code>${chatId}</code>`);
46
+ return null;
47
+ }
48
+
49
+ // Security: if no TELEGRAM_CHAT_ID configured, ignore all messages
50
+ if (!TELEGRAM_CHAT_ID) return null;
51
+
52
+ // Security: only accept messages from configured chat
53
+ if (chatId !== TELEGRAM_CHAT_ID) return null;
54
+
55
+ // Voice messages → transcribe to text
56
+ if (message.voice) {
57
+ if (!isWhisperEnabled()) {
58
+ await sendMessage(
59
+ this.botToken,
60
+ chatId,
61
+ 'Voice messages are not supported. Please set OPENAI_API_KEY to enable transcription.'
62
+ );
63
+ return null;
64
+ }
65
+ try {
66
+ const { buffer, filename } = await downloadFile(this.botToken, message.voice.file_id);
67
+ text = await transcribeAudio(buffer, filename);
68
+ } catch (err) {
69
+ console.error('Failed to transcribe voice:', err);
70
+ await sendMessage(this.botToken, chatId, 'Sorry, I could not transcribe your voice message.');
71
+ return null;
72
+ }
73
+ }
74
+
75
+ // Audio messages → transcribe to text
76
+ if (message.audio && !text) {
77
+ if (!isWhisperEnabled()) {
78
+ await sendMessage(
79
+ this.botToken,
80
+ chatId,
81
+ 'Audio messages are not supported. Please set OPENAI_API_KEY to enable transcription.'
82
+ );
83
+ return null;
84
+ }
85
+ try {
86
+ const { buffer, filename } = await downloadFile(this.botToken, message.audio.file_id);
87
+ text = await transcribeAudio(buffer, filename);
88
+ } catch (err) {
89
+ console.error('Failed to transcribe audio:', err);
90
+ await sendMessage(this.botToken, chatId, 'Sorry, I could not transcribe your audio message.');
91
+ return null;
92
+ }
93
+ }
94
+
95
+ // Photo → download largest size, add as image attachment
96
+ if (message.photo && message.photo.length > 0) {
97
+ try {
98
+ const largest = message.photo[message.photo.length - 1];
99
+ const { buffer } = await downloadFile(this.botToken, largest.file_id);
100
+ attachments.push({ category: 'image', mimeType: 'image/jpeg', data: buffer });
101
+ // Use caption as text if no text yet
102
+ if (!text && message.caption) text = message.caption;
103
+ } catch (err) {
104
+ console.error('Failed to download photo:', err);
105
+ }
106
+ }
107
+
108
+ // Document → download, add as document attachment
109
+ if (message.document) {
110
+ try {
111
+ const { buffer, filename } = await downloadFile(this.botToken, message.document.file_id);
112
+ const mimeType = message.document.mime_type || 'application/octet-stream';
113
+ attachments.push({ category: 'document', mimeType, data: buffer });
114
+ if (!text && message.caption) text = message.caption;
115
+ } catch (err) {
116
+ console.error('Failed to download document:', err);
117
+ }
118
+ }
119
+
120
+ // Nothing actionable
121
+ if (!text && attachments.length === 0) return null;
122
+
123
+ return {
124
+ threadId: chatId,
125
+ text: text || '',
126
+ attachments,
127
+ metadata: { messageId: message.message_id, chatId },
128
+ };
129
+ }
130
+
131
+ async acknowledge(metadata) {
132
+ await reactToMessage(this.botToken, metadata.chatId, metadata.messageId).catch(() => {});
133
+ }
134
+
135
+ startProcessingIndicator(metadata) {
136
+ return startTypingIndicator(this.botToken, metadata.chatId);
137
+ }
138
+
139
+ async sendResponse(threadId, text, metadata) {
140
+ await sendMessage(this.botToken, threadId, text);
141
+ }
142
+
143
+ get supportsStreaming() {
144
+ return false;
145
+ }
146
+ }
147
+
148
+ export { TelegramAdapter };
@@ -0,0 +1,288 @@
1
+ 'use server';
2
+
3
+ import { auth } from '../auth/index.js';
4
+ import {
5
+ createChat as dbCreateChat,
6
+ getChatById,
7
+ getMessagesByChatId,
8
+ deleteChat as dbDeleteChat,
9
+ deleteAllChatsByUser,
10
+ updateChatTitle,
11
+ toggleChatStarred,
12
+ } from '../db/chats.js';
13
+ import {
14
+ getNotifications as dbGetNotifications,
15
+ getUnreadCount as dbGetUnreadCount,
16
+ markAllRead as dbMarkAllRead,
17
+ } from '../db/notifications.js';
18
+
19
+ /**
20
+ * Get the authenticated user or throw.
21
+ */
22
+ async function requireAuth() {
23
+ const session = await auth();
24
+ if (!session?.user?.id) {
25
+ throw new Error('Unauthorized');
26
+ }
27
+ return session.user;
28
+ }
29
+
30
+ /**
31
+ * Get all chats for the authenticated user (includes Telegram chats).
32
+ * @returns {Promise<object[]>}
33
+ */
34
+ export async function getChats() {
35
+ const user = await requireAuth();
36
+ const { or, eq, desc } = await import('drizzle-orm');
37
+ const { getDb } = await import('../db/index.js');
38
+ const { chats } = await import('../db/schema.js');
39
+ const db = getDb();
40
+ return db
41
+ .select()
42
+ .from(chats)
43
+ .where(or(eq(chats.userId, user.id), eq(chats.userId, 'telegram')))
44
+ .orderBy(desc(chats.updatedAt))
45
+ .all();
46
+ }
47
+
48
+ /**
49
+ * Get messages for a specific chat (with ownership check).
50
+ * @param {string} chatId
51
+ * @returns {Promise<object[]>}
52
+ */
53
+ export async function getChatMessages(chatId) {
54
+ const user = await requireAuth();
55
+ const chat = getChatById(chatId);
56
+ if (!chat || (chat.userId !== user.id && chat.userId !== 'telegram')) {
57
+ return [];
58
+ }
59
+ return getMessagesByChatId(chatId);
60
+ }
61
+
62
+ /**
63
+ * Create a new chat.
64
+ * @param {string} [id] - Optional chat ID
65
+ * @param {string} [title='New Chat']
66
+ * @returns {Promise<object>}
67
+ */
68
+ export async function createChat(id, title = 'New Chat') {
69
+ const user = await requireAuth();
70
+ return dbCreateChat(user.id, title, id);
71
+ }
72
+
73
+ /**
74
+ * Delete a chat (with ownership check).
75
+ * @param {string} chatId
76
+ * @returns {Promise<{success: boolean}>}
77
+ */
78
+ export async function deleteChat(chatId) {
79
+ const user = await requireAuth();
80
+ const chat = getChatById(chatId);
81
+ if (!chat || chat.userId !== user.id) {
82
+ return { success: false };
83
+ }
84
+ dbDeleteChat(chatId);
85
+ return { success: true };
86
+ }
87
+
88
+ /**
89
+ * Get the title of a specific chat (with ownership check).
90
+ * @param {string} chatId
91
+ * @returns {Promise<string|null>}
92
+ */
93
+ export async function getChatTitle(chatId) {
94
+ const user = await requireAuth();
95
+ const chat = getChatById(chatId);
96
+ if (!chat || (chat.userId !== user.id && chat.userId !== 'telegram')) {
97
+ return null;
98
+ }
99
+ return chat.title;
100
+ }
101
+
102
+ /**
103
+ * Rename a chat (with ownership check).
104
+ * @param {string} chatId
105
+ * @param {string} title
106
+ * @returns {Promise<{success: boolean}>}
107
+ */
108
+ export async function renameChat(chatId, title) {
109
+ const user = await requireAuth();
110
+ const chat = getChatById(chatId);
111
+ if (!chat || chat.userId !== user.id) {
112
+ return { success: false };
113
+ }
114
+ updateChatTitle(chatId, title);
115
+ return { success: true };
116
+ }
117
+
118
+ /**
119
+ * Toggle a chat's starred status (with ownership check).
120
+ * @param {string} chatId
121
+ * @returns {Promise<{success: boolean, starred?: number}>}
122
+ */
123
+ export async function starChat(chatId) {
124
+ const user = await requireAuth();
125
+ const chat = getChatById(chatId);
126
+ if (!chat || chat.userId !== user.id) {
127
+ return { success: false };
128
+ }
129
+ const starred = toggleChatStarred(chatId);
130
+ return { success: true, starred };
131
+ }
132
+
133
+ /**
134
+ * Delete all chats for the authenticated user.
135
+ * @returns {Promise<{success: boolean}>}
136
+ */
137
+ export async function deleteAllChats() {
138
+ const user = await requireAuth();
139
+ deleteAllChatsByUser(user.id);
140
+ return { success: true };
141
+ }
142
+
143
+ /**
144
+ * Get all notifications, newest first.
145
+ * @returns {Promise<object[]>}
146
+ */
147
+ export async function getNotifications() {
148
+ await requireAuth();
149
+ return dbGetNotifications();
150
+ }
151
+
152
+ /**
153
+ * Get count of unread notifications.
154
+ * @returns {Promise<number>}
155
+ */
156
+ export async function getUnreadNotificationCount() {
157
+ await requireAuth();
158
+ return dbGetUnreadCount();
159
+ }
160
+
161
+ /**
162
+ * Mark all notifications as read.
163
+ * @returns {Promise<{success: boolean}>}
164
+ */
165
+ export async function markNotificationsRead() {
166
+ await requireAuth();
167
+ dbMarkAllRead();
168
+ return { success: true };
169
+ }
170
+
171
+ // ─────────────────────────────────────────────────────────────────────────────
172
+ // App info actions
173
+ // ─────────────────────────────────────────────────────────────────────────────
174
+
175
+ /**
176
+ * Get the installed package version and update status (auth-gated, never in client bundle).
177
+ * @returns {Promise<{ version: string, updateAvailable: string|null }>}
178
+ */
179
+ export async function getAppVersion() {
180
+ await requireAuth();
181
+ const { getInstalledVersion } = await import('../cron.js');
182
+ const { getAvailableVersion, getReleaseNotes } = await import('../db/update-check.js');
183
+ return {
184
+ version: getInstalledVersion(),
185
+ updateAvailable: getAvailableVersion(),
186
+ changelog: getReleaseNotes(),
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Trigger the upgrade-event-handler workflow via GitHub Actions.
192
+ * @returns {Promise<{ success: boolean }>}
193
+ */
194
+ export async function triggerUpgrade() {
195
+ await requireAuth();
196
+ const { triggerWorkflowDispatch } = await import('../tools/github.js');
197
+ const { getAvailableVersion } = await import('../db/update-check.js');
198
+ const targetVersion = getAvailableVersion();
199
+ await triggerWorkflowDispatch('upgrade-event-handler.yml', 'main', {
200
+ target_version: targetVersion || '',
201
+ });
202
+ return { success: true };
203
+ }
204
+
205
+ // ─────────────────────────────────────────────────────────────────────────────
206
+ // API Key actions
207
+ // ─────────────────────────────────────────────────────────────────────────────
208
+
209
+ /**
210
+ * Create (or replace) the API key.
211
+ * @returns {Promise<{ key: string, record: object } | { error: string }>}
212
+ */
213
+ export async function createNewApiKey() {
214
+ const user = await requireAuth();
215
+ try {
216
+ const { createApiKeyRecord } = await import('../db/api-keys.js');
217
+ return createApiKeyRecord(user.id);
218
+ } catch (err) {
219
+ console.error('Failed to create API key:', err);
220
+ return { error: 'Failed to create API key' };
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Get the current API key metadata (no hash).
226
+ * @returns {Promise<object|null>}
227
+ */
228
+ export async function getApiKeys() {
229
+ await requireAuth();
230
+ try {
231
+ const { getApiKey } = await import('../db/api-keys.js');
232
+ return getApiKey();
233
+ } catch (err) {
234
+ console.error('Failed to get API key:', err);
235
+ return null;
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Delete the API key.
241
+ * @returns {Promise<{ success: boolean } | { error: string }>}
242
+ */
243
+ export async function deleteApiKey() {
244
+ await requireAuth();
245
+ try {
246
+ const mod = await import('../db/api-keys.js');
247
+ mod.deleteApiKey();
248
+ return { success: true };
249
+ } catch (err) {
250
+ console.error('Failed to delete API key:', err);
251
+ return { error: 'Failed to delete API key' };
252
+ }
253
+ }
254
+
255
+ // ─────────────────────────────────────────────────────────────────────────────
256
+ // Swarm actions
257
+ // ─────────────────────────────────────────────────────────────────────────────
258
+
259
+ /**
260
+ * Get swarm status (active + completed jobs with counts).
261
+ * @returns {Promise<object>}
262
+ */
263
+ export async function getSwarmStatus(page = 1) {
264
+ await requireAuth();
265
+ try {
266
+ const { getSwarmStatus: fetchStatus } = await import('../tools/github.js');
267
+ return await fetchStatus(page);
268
+ } catch (err) {
269
+ console.error('Failed to get swarm status:', err);
270
+ return { error: 'Failed to get swarm status', runs: [], hasMore: false };
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Get swarm config (crons + triggers).
276
+ * @returns {Promise<{ crons: object[], triggers: object[] }>}
277
+ */
278
+ export async function getSwarmConfig() {
279
+ await requireAuth();
280
+ const { cronsFile, triggersFile } = await import('../paths.js');
281
+ const fs = await import('fs');
282
+ let crons = [];
283
+ let triggers = [];
284
+ try { crons = JSON.parse(fs.readFileSync(cronsFile, 'utf8')); } catch {}
285
+ try { triggers = JSON.parse(fs.readFileSync(triggersFile, 'utf8')); } catch {}
286
+ return { crons, triggers };
287
+ }
288
+
@@ -0,0 +1,135 @@
1
+ import { auth } from '../auth/index.js';
2
+ import { chatStream } from '../ai/index.js';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+
5
+ /**
6
+ * POST handler for /stream/chat — streaming chat with session auth.
7
+ * Dedicated route handler separate from the catch-all api/index.js.
8
+ */
9
+ export async function POST(request) {
10
+ const session = await auth();
11
+ if (!session?.user?.id) {
12
+ return Response.json({ error: 'Unauthorized' }, { status: 401 });
13
+ }
14
+
15
+ const body = await request.json();
16
+ const { messages, chatId: rawChatId, trigger } = body;
17
+
18
+ if (!messages?.length) {
19
+ return Response.json({ error: 'No messages' }, { status: 400 });
20
+ }
21
+
22
+ // Get the last user message — AI SDK v5 sends UIMessage[] with parts
23
+ const lastUserMessage = [...messages].reverse().find((m) => m.role === 'user');
24
+ if (!lastUserMessage) {
25
+ return Response.json({ error: 'No user message' }, { status: 400 });
26
+ }
27
+
28
+ // Extract text from message parts (AI SDK v5+) or fall back to content
29
+ let userText =
30
+ lastUserMessage.parts
31
+ ?.filter((p) => p.type === 'text')
32
+ .map((p) => p.text)
33
+ .join('\n') ||
34
+ lastUserMessage.content ||
35
+ '';
36
+
37
+ // Extract file parts from message
38
+ const fileParts = lastUserMessage.parts?.filter((p) => p.type === 'file') || [];
39
+ const attachments = [];
40
+
41
+ for (const part of fileParts) {
42
+ const { mediaType, url } = part;
43
+ if (!mediaType || !url) continue;
44
+
45
+ if (mediaType.startsWith('image/') || mediaType === 'application/pdf') {
46
+ // Images and PDFs → pass as visual attachments for the LLM
47
+ attachments.push({ category: 'image', mimeType: mediaType, dataUrl: url });
48
+ } else if (mediaType.startsWith('text/') || mediaType === 'application/json') {
49
+ // Text files → decode base64 data URL and inline into message text
50
+ try {
51
+ const base64Data = url.split(',')[1];
52
+ const textContent = Buffer.from(base64Data, 'base64').toString('utf-8');
53
+ const fileName = part.name || 'file';
54
+ userText += `\n\nFile: ${fileName}\n\`\`\`\n${textContent}\n\`\`\``;
55
+ } catch (e) {
56
+ console.error('Failed to decode text file:', e);
57
+ }
58
+ }
59
+ }
60
+
61
+ if (!userText.trim() && attachments.length === 0) {
62
+ return Response.json({ error: 'Empty message' }, { status: 400 });
63
+ }
64
+
65
+ // Map web channel to thread_id — AI layer handles DB persistence
66
+ const threadId = rawChatId || uuidv4();
67
+ const { createUIMessageStream, createUIMessageStreamResponse } = await import('ai');
68
+
69
+ const stream = createUIMessageStream({
70
+ onError: (error) => {
71
+ console.error('Chat stream error:', error);
72
+ return error?.message || 'An error occurred while processing your message.';
73
+ },
74
+ execute: async ({ writer }) => {
75
+ // chatStream handles: save user msg, invoke agent, save assistant msg, auto-title
76
+ const skipUserPersist = trigger === 'regenerate-message';
77
+ const chunks = chatStream(threadId, userText, attachments, {
78
+ userId: session.user.id,
79
+ skipUserPersist,
80
+ });
81
+
82
+ // Signal start of assistant message
83
+ writer.write({ type: 'start' });
84
+
85
+ let textStarted = false;
86
+ let textId = uuidv4();
87
+
88
+ for await (const chunk of chunks) {
89
+ if (chunk.type === 'text') {
90
+ if (!textStarted) {
91
+ textId = uuidv4();
92
+ writer.write({ type: 'text-start', id: textId });
93
+ textStarted = true;
94
+ }
95
+ writer.write({ type: 'text-delta', id: textId, delta: chunk.text });
96
+
97
+ } else if (chunk.type === 'tool-call') {
98
+ // Close any open text block before tool events
99
+ if (textStarted) {
100
+ writer.write({ type: 'text-end', id: textId });
101
+ textStarted = false;
102
+ }
103
+ writer.write({
104
+ type: 'tool-input-start',
105
+ toolCallId: chunk.toolCallId,
106
+ toolName: chunk.toolName,
107
+ });
108
+ writer.write({
109
+ type: 'tool-input-available',
110
+ toolCallId: chunk.toolCallId,
111
+ toolName: chunk.toolName,
112
+ input: chunk.args,
113
+ });
114
+
115
+ } else if (chunk.type === 'tool-result') {
116
+ writer.write({
117
+ type: 'tool-output-available',
118
+ toolCallId: chunk.toolCallId,
119
+ output: chunk.result,
120
+ });
121
+ }
122
+ }
123
+
124
+ // Close final text block if still open
125
+ if (textStarted) {
126
+ writer.write({ type: 'text-end', id: textId });
127
+ }
128
+
129
+ // Signal end of assistant message
130
+ writer.write({ type: 'finish' });
131
+ },
132
+ });
133
+
134
+ return createUIMessageStreamResponse({ stream });
135
+ }