@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.
- package/LICENSE +21 -0
- package/README.md +406 -0
- package/agents/README.md +76 -0
- package/agents/_template/CONFIG.yaml +7 -0
- package/agents/_template/HEARTBEAT.md +59 -0
- package/agents/_template/IDENTITY.md +4 -0
- package/agents/_template/SKILLS.md +1 -0
- package/agents/_template/SOUL.md +25 -0
- package/agents/_template/TOOLS.md +3 -0
- package/agents/binary-reverser/CONFIG.yaml +21 -0
- package/agents/binary-reverser/HEARTBEAT.md +65 -0
- package/agents/binary-reverser/IDENTITY.md +1 -0
- package/agents/binary-reverser/SKILLS.md +1 -0
- package/agents/binary-reverser/SOUL.md +23 -0
- package/agents/binary-reverser/TOOLS.md +99 -0
- package/agents/browser-agent/CONFIG.yaml +20 -0
- package/agents/browser-agent/HEARTBEAT.md +79 -0
- package/agents/browser-agent/IDENTITY.md +5 -0
- package/agents/browser-agent/SKILLS.md +86 -0
- package/agents/browser-agent/SOUL.md +23 -0
- package/agents/browser-agent/TOOLS.md +186 -0
- package/agents/cloud-infiltrator/CONFIG.yaml +22 -0
- package/agents/cloud-infiltrator/HEARTBEAT.md +78 -0
- package/agents/cloud-infiltrator/IDENTITY.md +1 -0
- package/agents/cloud-infiltrator/SKILLS.md +1 -0
- package/agents/cloud-infiltrator/SOUL.md +23 -0
- package/agents/cloud-infiltrator/TOOLS.md +68 -0
- package/agents/coding-assistant/CONFIG.yaml +22 -0
- package/agents/coding-assistant/HEARTBEAT.md +57 -0
- package/agents/coding-assistant/IDENTITY.md +5 -0
- package/agents/coding-assistant/SKILLS.md +69 -0
- package/agents/coding-assistant/SOUL.md +60 -0
- package/agents/coding-assistant/TOOLS.md +168 -0
- package/agents/learning-agent/CONFIG.yaml +21 -0
- package/agents/learning-agent/HEARTBEAT.md +63 -0
- package/agents/learning-agent/IDENTITY.md +5 -0
- package/agents/learning-agent/SKILLS.md +86 -0
- package/agents/learning-agent/SOUL.md +77 -0
- package/agents/learning-agent/TOOLS.md +145 -0
- package/agents/maintainer/CONFIG.yaml +31 -0
- package/agents/maintainer/HEARTBEAT.md +28 -0
- package/agents/maintainer/IDENTITY.md +33 -0
- package/agents/maintainer/SKILLS.md +24 -0
- package/agents/maintainer/SOUL.md +61 -0
- package/agents/maintainer/TOOLS.md +29 -0
- package/agents/maintainer/lib/engine.js +279 -0
- package/agents/maintainer/lib/safe-fixer.js +183 -0
- package/agents/morning-brief/CONFIG.yaml +22 -0
- package/agents/morning-brief/HEARTBEAT.md +60 -0
- package/agents/morning-brief/IDENTITY.md +5 -0
- package/agents/morning-brief/SKILLS.md +56 -0
- package/agents/morning-brief/SOUL.md +64 -0
- package/agents/morning-brief/TOOLS.md +112 -0
- package/agents/osint-detective/CONFIG.yaml +24 -0
- package/agents/osint-detective/HEARTBEAT.md +66 -0
- package/agents/osint-detective/IDENTITY.md +1 -0
- package/agents/osint-detective/SKILLS.md +1 -0
- package/agents/osint-detective/SOUL.md +23 -0
- package/agents/osint-detective/TOOLS.md +81 -0
- package/agents/recon-scout/CONFIG.yaml +22 -0
- package/agents/recon-scout/HEARTBEAT.md +79 -0
- package/agents/recon-scout/IDENTITY.md +1 -0
- package/agents/recon-scout/SKILLS.md +1 -0
- package/agents/recon-scout/SOUL.md +23 -0
- package/agents/recon-scout/TOOLS.md +93 -0
- package/agents/report-writer/CONFIG.yaml +21 -0
- package/agents/report-writer/HEARTBEAT.md +63 -0
- package/agents/report-writer/IDENTITY.md +1 -0
- package/agents/report-writer/SKILLS.md +1 -0
- package/agents/report-writer/SOUL.md +23 -0
- package/agents/report-writer/TOOLS.md +69 -0
- package/agents/shared/README.md +13 -0
- package/agents/web-hacker/CONFIG.yaml +24 -0
- package/agents/web-hacker/HEARTBEAT.md +78 -0
- package/agents/web-hacker/IDENTITY.md +1 -0
- package/agents/web-hacker/SKILLS.md +1 -0
- package/agents/web-hacker/SOUL.md +23 -0
- package/agents/web-hacker/TOOLS.md +86 -0
- package/api/CLAUDE.md +19 -0
- package/api/index.js +274 -0
- package/bin/cli.js +620 -0
- package/bin/local.sh +31 -0
- package/bin/postinstall.js +63 -0
- package/config/index.js +24 -0
- package/config/instrumentation.js +93 -0
- package/drizzle/0000_initial.sql +52 -0
- package/drizzle/0001_bounty_and_registry.sql +82 -0
- package/drizzle/0002_sync_columns.sql +7 -0
- package/drizzle/0003_graceful_bloodscream.sql +86 -0
- package/drizzle/meta/0000_snapshot.json +321 -0
- package/drizzle/meta/0003_snapshot.json +878 -0
- package/drizzle/meta/_journal.json +34 -0
- package/drizzle/relations.ts +3 -0
- package/drizzle/schema.ts +145 -0
- package/lib/actions.js +47 -0
- package/lib/agents.js +166 -0
- package/lib/ai/agent.js +96 -0
- package/lib/ai/autonomous-engine.js +261 -0
- package/lib/ai/index.js +359 -0
- package/lib/ai/model-router.js +254 -0
- package/lib/ai/model.js +73 -0
- package/lib/ai/tools.js +84 -0
- package/lib/auth/actions.js +28 -0
- package/lib/auth/config.js +27 -0
- package/lib/auth/edge-config.js +27 -0
- package/lib/auth/index.js +27 -0
- package/lib/auth/middleware.js +53 -0
- package/lib/bounty/actions.js +119 -0
- package/lib/bounty/findings.js +64 -0
- package/lib/bounty/programs.js +34 -0
- package/lib/bounty/sync-targets.js +267 -0
- package/lib/bounty/targets.js +33 -0
- package/lib/channels/base.js +56 -0
- package/lib/channels/index.js +15 -0
- package/lib/channels/telegram.js +148 -0
- package/lib/chat/actions.js +288 -0
- package/lib/chat/api.js +135 -0
- package/lib/chat/components/app-sidebar.js +237 -0
- package/lib/chat/components/app-sidebar.jsx +289 -0
- package/lib/chat/components/chat-header.js +27 -0
- package/lib/chat/components/chat-header.jsx +37 -0
- package/lib/chat/components/chat-input.js +230 -0
- package/lib/chat/components/chat-input.jsx +228 -0
- package/lib/chat/components/chat-nav-context.js +11 -0
- package/lib/chat/components/chat-nav-context.jsx +11 -0
- package/lib/chat/components/chat-page.js +81 -0
- package/lib/chat/components/chat-page.jsx +100 -0
- package/lib/chat/components/chat.js +150 -0
- package/lib/chat/components/chat.jsx +182 -0
- package/lib/chat/components/chats-page.js +302 -0
- package/lib/chat/components/chats-page.jsx +330 -0
- package/lib/chat/components/crons-page.js +172 -0
- package/lib/chat/components/crons-page.jsx +244 -0
- package/lib/chat/components/enhanced-tool-call.js +103 -0
- package/lib/chat/components/enhanced-tool-call.jsx +139 -0
- package/lib/chat/components/findings-page.js +175 -0
- package/lib/chat/components/findings-page.jsx +214 -0
- package/lib/chat/components/greeting.js +22 -0
- package/lib/chat/components/greeting.jsx +26 -0
- package/lib/chat/components/icons.js +777 -0
- package/lib/chat/components/icons.jsx +741 -0
- package/lib/chat/components/index.js +26 -0
- package/lib/chat/components/mcp-page.js +260 -0
- package/lib/chat/components/mcp-page.jsx +355 -0
- package/lib/chat/components/message.js +289 -0
- package/lib/chat/components/message.jsx +315 -0
- package/lib/chat/components/messages.js +66 -0
- package/lib/chat/components/messages.jsx +77 -0
- package/lib/chat/components/notifications-page.js +56 -0
- package/lib/chat/components/notifications-page.jsx +87 -0
- package/lib/chat/components/page-layout.js +21 -0
- package/lib/chat/components/page-layout.jsx +28 -0
- package/lib/chat/components/registry-page.js +222 -0
- package/lib/chat/components/registry-page.jsx +255 -0
- package/lib/chat/components/settings-layout.js +40 -0
- package/lib/chat/components/settings-layout.jsx +54 -0
- package/lib/chat/components/settings-secrets-page.js +216 -0
- package/lib/chat/components/settings-secrets-page.jsx +264 -0
- package/lib/chat/components/sidebar-history-item.js +132 -0
- package/lib/chat/components/sidebar-history-item.jsx +113 -0
- package/lib/chat/components/sidebar-history.js +115 -0
- package/lib/chat/components/sidebar-history.jsx +157 -0
- package/lib/chat/components/sidebar-user-nav.js +63 -0
- package/lib/chat/components/sidebar-user-nav.jsx +73 -0
- package/lib/chat/components/status-bar.js +39 -0
- package/lib/chat/components/status-bar.jsx +51 -0
- package/lib/chat/components/swarm-page.js +157 -0
- package/lib/chat/components/swarm-page.jsx +210 -0
- package/lib/chat/components/targets-page.js +376 -0
- package/lib/chat/components/targets-page.jsx +389 -0
- package/lib/chat/components/tool-call.js +86 -0
- package/lib/chat/components/tool-call.jsx +104 -0
- package/lib/chat/components/tool-panel.js +107 -0
- package/lib/chat/components/tool-panel.jsx +145 -0
- package/lib/chat/components/triggers-page.js +153 -0
- package/lib/chat/components/triggers-page.jsx +221 -0
- package/lib/chat/components/ui/confirm-dialog.js +53 -0
- package/lib/chat/components/ui/confirm-dialog.jsx +57 -0
- package/lib/chat/components/ui/dropdown-menu.js +98 -0
- package/lib/chat/components/ui/dropdown-menu.jsx +116 -0
- package/lib/chat/components/ui/rename-dialog.js +74 -0
- package/lib/chat/components/ui/rename-dialog.jsx +72 -0
- package/lib/chat/components/ui/scroll-area.js +13 -0
- package/lib/chat/components/ui/scroll-area.jsx +17 -0
- package/lib/chat/components/ui/separator.js +21 -0
- package/lib/chat/components/ui/separator.jsx +18 -0
- package/lib/chat/components/ui/sheet.js +75 -0
- package/lib/chat/components/ui/sheet.jsx +95 -0
- package/lib/chat/components/ui/sidebar.js +227 -0
- package/lib/chat/components/ui/sidebar.jsx +245 -0
- package/lib/chat/components/ui/tooltip.js +56 -0
- package/lib/chat/components/ui/tooltip.jsx +66 -0
- package/lib/chat/components/upgrade-dialog.js +151 -0
- package/lib/chat/components/upgrade-dialog.jsx +170 -0
- package/lib/chat/utils.js +11 -0
- package/lib/cron.js +246 -0
- package/lib/db/api-keys.js +163 -0
- package/lib/db/chats.js +145 -0
- package/lib/db/index.js +52 -0
- package/lib/db/notifications.js +99 -0
- package/lib/db/schema.js +145 -0
- package/lib/db/update-check.js +96 -0
- package/lib/db/users.js +89 -0
- package/lib/mcp/actions.js +104 -0
- package/lib/mcp/client.js +79 -0
- package/lib/mcp/handler.js +57 -0
- package/lib/mcp/server.js +165 -0
- package/lib/paths.js +46 -0
- package/lib/registry/actions.js +164 -0
- package/lib/registry/catalog.js +137 -0
- package/lib/registry/tools.js +71 -0
- package/lib/tools/create-job.js +99 -0
- package/lib/tools/github.js +217 -0
- package/lib/tools/openai.js +35 -0
- package/lib/tools/telegram.js +292 -0
- package/lib/triggers.js +118 -0
- package/lib/utils/render-md.js +102 -0
- package/package.json +103 -0
- package/setup/lib/auth.mjs +81 -0
- package/setup/lib/env.mjs +21 -0
- package/setup/lib/fs-utils.mjs +20 -0
- package/setup/lib/github.mjs +149 -0
- package/setup/lib/prerequisites.mjs +155 -0
- package/setup/lib/prompts.mjs +267 -0
- package/setup/lib/providers.mjs +48 -0
- package/setup/lib/sync.mjs +125 -0
- package/setup/lib/targets.mjs +45 -0
- package/setup/lib/telegram-verify.mjs +63 -0
- package/setup/lib/telegram.mjs +76 -0
- package/setup/setup-telegram.mjs +264 -0
- package/setup/setup.mjs +842 -0
- package/templates/.dockerignore +5 -0
- package/templates/.env.example +63 -0
- package/templates/.github/workflows/auto-merge.yml +117 -0
- package/templates/.github/workflows/build-image.yml +36 -0
- package/templates/.github/workflows/notify-job-failed.yml +64 -0
- package/templates/.github/workflows/notify-pr-complete.yml +119 -0
- package/templates/.github/workflows/rebuild-event-handler.yml +121 -0
- package/templates/.github/workflows/run-job.yml +89 -0
- package/templates/.github/workflows/upgrade-event-handler.yml +62 -0
- package/templates/.gitignore.template +45 -0
- package/templates/.pi/extensions/env-sanitizer/index.ts +48 -0
- package/templates/.pi/extensions/env-sanitizer/package.json +5 -0
- package/templates/CLAUDE.md +29 -0
- package/templates/CLAUDE.md.template +307 -0
- package/templates/app/api/[...thepopebot]/route.js +1 -0
- package/templates/app/api/auth/[...nextauth]/route.js +1 -0
- package/templates/app/chat/[chatId]/page.js +8 -0
- package/templates/app/chats/page.js +7 -0
- package/templates/app/components/ascii-logo.jsx +10 -0
- package/templates/app/components/login-form.jsx +92 -0
- package/templates/app/components/setup-form.jsx +82 -0
- package/templates/app/components/theme-provider.jsx +11 -0
- package/templates/app/components/theme-toggle.jsx +38 -0
- package/templates/app/components/ui/button.jsx +21 -0
- package/templates/app/components/ui/card.jsx +23 -0
- package/templates/app/components/ui/input.jsx +10 -0
- package/templates/app/components/ui/label.jsx +10 -0
- package/templates/app/crons/page.js +5 -0
- package/templates/app/findings/page.js +7 -0
- package/templates/app/globals.css +90 -0
- package/templates/app/layout.js +19 -0
- package/templates/app/login/page.js +15 -0
- package/templates/app/notifications/page.js +7 -0
- package/templates/app/page.js +7 -0
- package/templates/app/settings/crons/page.js +5 -0
- package/templates/app/settings/layout.js +7 -0
- package/templates/app/settings/mcp/page.js +5 -0
- package/templates/app/settings/page.js +5 -0
- package/templates/app/settings/secrets/page.js +5 -0
- package/templates/app/settings/triggers/page.js +5 -0
- package/templates/app/stream/chat/route.js +1 -0
- package/templates/app/swarm/page.js +7 -0
- package/templates/app/targets/page.js +7 -0
- package/templates/app/toolbox/page.js +7 -0
- package/templates/app/triggers/page.js +5 -0
- package/templates/config/AGENT.md +34 -0
- package/templates/config/CRONS.json +56 -0
- package/templates/config/EVENT_HANDLER.md +224 -0
- package/templates/config/HEARTBEAT.md +3 -0
- package/templates/config/JOB_SUMMARY.md +130 -0
- package/templates/config/MCP_SERVERS.json +1 -0
- package/templates/config/SKILL_BUILDING_GUIDE.md +90 -0
- package/templates/config/SOUL.md +17 -0
- package/templates/config/TRIGGERS.json +58 -0
- package/templates/docker/event-handler/Dockerfile +20 -0
- package/templates/docker/event-handler/ecosystem.config.cjs +8 -0
- package/templates/docker/job-claude-code/Dockerfile +34 -0
- package/templates/docker/job-claude-code/entrypoint.sh +139 -0
- package/templates/docker/job-pi-coding-agent/Dockerfile +44 -0
- package/templates/docker/job-pi-coding-agent/entrypoint.sh +163 -0
- package/templates/docker-compose.yml +63 -0
- package/templates/instrumentation.js +6 -0
- package/templates/middleware.js +1 -0
- package/templates/next.config.mjs +3 -0
- package/templates/postcss.config.mjs +5 -0
- package/templates/skills/LICENSE +21 -0
- package/templates/skills/README.md +119 -0
- package/templates/skills/brave-search/SKILL.md +79 -0
- package/templates/skills/brave-search/content.js +86 -0
- package/templates/skills/brave-search/package-lock.json +621 -0
- package/templates/skills/brave-search/package.json +14 -0
- package/templates/skills/brave-search/search.js +199 -0
- package/templates/skills/browser-tools/SKILL.md +196 -0
- package/templates/skills/browser-tools/browser-content.js +103 -0
- package/templates/skills/browser-tools/browser-cookies.js +35 -0
- package/templates/skills/browser-tools/browser-eval.js +53 -0
- package/templates/skills/browser-tools/browser-hn-scraper.js +108 -0
- package/templates/skills/browser-tools/browser-nav.js +44 -0
- package/templates/skills/browser-tools/browser-pick.js +162 -0
- package/templates/skills/browser-tools/browser-screenshot.js +34 -0
- package/templates/skills/browser-tools/browser-start.js +87 -0
- package/templates/skills/browser-tools/package-lock.json +2556 -0
- package/templates/skills/browser-tools/package.json +19 -0
- package/templates/skills/llm-secrets/SKILL.md +34 -0
- package/templates/skills/llm-secrets/llm-secrets.js +33 -0
- 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
|
+
|
package/lib/chat/api.js
ADDED
|
@@ -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
|
+
}
|