zubo 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/.github/workflows/ci.yml +35 -0
- package/README.md +149 -0
- package/bun.lock +216 -0
- package/desktop/README.md +57 -0
- package/desktop/package.json +12 -0
- package/desktop/src-tauri/Cargo.toml +25 -0
- package/desktop/src-tauri/build.rs +3 -0
- package/desktop/src-tauri/icons/README.md +17 -0
- package/desktop/src-tauri/icons/icon.png +0 -0
- package/desktop/src-tauri/src/main.rs +189 -0
- package/desktop/src-tauri/tauri.conf.json +68 -0
- package/docs/ROADMAP.md +490 -0
- package/migrations/001_init.sql +9 -0
- package/migrations/002_memory.sql +33 -0
- package/migrations/003_cron.sql +24 -0
- package/migrations/004_usage.sql +12 -0
- package/migrations/005_secrets.sql +8 -0
- package/migrations/006_agents.sql +1 -0
- package/migrations/007_workflows.sql +22 -0
- package/migrations/008_proactive.sql +24 -0
- package/migrations/009_uploads.sql +9 -0
- package/migrations/010_observability.sql +22 -0
- package/migrations/011_api_keys.sql +7 -0
- package/migrations/012_indexes.sql +5 -0
- package/migrations/013_budget.sql +11 -0
- package/migrations/014_usage_session_idx.sql +2 -0
- package/package.json +39 -0
- package/site/404.html +156 -0
- package/site/CNAME +1 -0
- package/site/docs/agents.html +294 -0
- package/site/docs/api.html +446 -0
- package/site/docs/channels.html +345 -0
- package/site/docs/cli.html +238 -0
- package/site/docs/config.html +1034 -0
- package/site/docs/index.html +433 -0
- package/site/docs/integrations.html +381 -0
- package/site/docs/memory.html +254 -0
- package/site/docs/security.html +375 -0
- package/site/docs/skills.html +322 -0
- package/site/docs.css +412 -0
- package/site/index.html +638 -0
- package/site/install.sh +98 -0
- package/site/logo.svg +1 -0
- package/site/og-image.png +0 -0
- package/site/robots.txt +4 -0
- package/site/script.js +361 -0
- package/site/sitemap.xml +63 -0
- package/site/skills.html +532 -0
- package/site/style.css +1686 -0
- package/src/agent/agents.ts +159 -0
- package/src/agent/compaction.ts +53 -0
- package/src/agent/context.ts +18 -0
- package/src/agent/delegate.ts +118 -0
- package/src/agent/loop.ts +318 -0
- package/src/agent/prompts.ts +111 -0
- package/src/agent/session.ts +87 -0
- package/src/agent/teams.ts +116 -0
- package/src/agent/workflow-executor.ts +192 -0
- package/src/agent/workflow.ts +175 -0
- package/src/channels/adapter.ts +21 -0
- package/src/channels/dashboard.html.ts +2969 -0
- package/src/channels/discord.ts +137 -0
- package/src/channels/optional-deps.d.ts +17 -0
- package/src/channels/router.ts +199 -0
- package/src/channels/signal.ts +133 -0
- package/src/channels/slack.ts +101 -0
- package/src/channels/telegram.ts +102 -0
- package/src/channels/utils.ts +18 -0
- package/src/channels/webchat.ts +1797 -0
- package/src/channels/whatsapp.ts +119 -0
- package/src/config/loader.ts +22 -0
- package/src/config/paths.ts +43 -0
- package/src/config/schema.ts +121 -0
- package/src/db/connection.ts +20 -0
- package/src/db/export.ts +148 -0
- package/src/db/migrations.ts +42 -0
- package/src/index.ts +261 -0
- package/src/llm/claude.ts +193 -0
- package/src/llm/factory.ts +115 -0
- package/src/llm/failover.ts +101 -0
- package/src/llm/openai-compat.ts +409 -0
- package/src/llm/provider.ts +83 -0
- package/src/llm/smart-router.ts +241 -0
- package/src/logs.ts +53 -0
- package/src/memory/chunker.ts +58 -0
- package/src/memory/document-parser.ts +115 -0
- package/src/memory/embedder.ts +235 -0
- package/src/memory/engine.ts +170 -0
- package/src/memory/fts-index.ts +55 -0
- package/src/memory/hybrid-search.ts +72 -0
- package/src/memory/store.ts +56 -0
- package/src/memory/vector-index.ts +72 -0
- package/src/model.ts +118 -0
- package/src/registry/cli.ts +43 -0
- package/src/registry/client.ts +54 -0
- package/src/registry/installer.ts +67 -0
- package/src/scheduler/briefing.ts +71 -0
- package/src/scheduler/cron.ts +258 -0
- package/src/scheduler/heartbeat.ts +58 -0
- package/src/scheduler/memory-triggers.ts +100 -0
- package/src/scheduler/natural-cron.ts +163 -0
- package/src/scheduler/proactive.ts +25 -0
- package/src/scheduler/recipes.ts +110 -0
- package/src/secrets/store.ts +64 -0
- package/src/setup.ts +413 -0
- package/src/skills.ts +293 -0
- package/src/start.ts +373 -0
- package/src/status.ts +165 -0
- package/src/tools/builtin/connect-service.ts +205 -0
- package/src/tools/builtin/cron.ts +126 -0
- package/src/tools/builtin/datetime.ts +36 -0
- package/src/tools/builtin/delegate-task.ts +81 -0
- package/src/tools/builtin/delegate.ts +42 -0
- package/src/tools/builtin/diagnose.ts +41 -0
- package/src/tools/builtin/google-oauth.ts +379 -0
- package/src/tools/builtin/manage-agents.ts +149 -0
- package/src/tools/builtin/manage-skills.ts +294 -0
- package/src/tools/builtin/manage-teams.ts +89 -0
- package/src/tools/builtin/manage-triggers.ts +94 -0
- package/src/tools/builtin/manage-workflows.ts +119 -0
- package/src/tools/builtin/memory-search.ts +38 -0
- package/src/tools/builtin/memory-write.ts +30 -0
- package/src/tools/builtin/run-workflow.ts +36 -0
- package/src/tools/builtin/secrets.ts +122 -0
- package/src/tools/builtin/skill-registry.ts +75 -0
- package/src/tools/builtin-integrations/api-helpers.ts +26 -0
- package/src/tools/builtin-integrations/github/github_issues/SKILL.md +56 -0
- package/src/tools/builtin-integrations/github/github_issues/handler.ts +108 -0
- package/src/tools/builtin-integrations/github/github_prs/SKILL.md +57 -0
- package/src/tools/builtin-integrations/github/github_prs/handler.ts +113 -0
- package/src/tools/builtin-integrations/github/github_repos/SKILL.md +37 -0
- package/src/tools/builtin-integrations/github/github_repos/handler.ts +88 -0
- package/src/tools/builtin-integrations/google/gmail/SKILL.md +51 -0
- package/src/tools/builtin-integrations/google/gmail/handler.ts +125 -0
- package/src/tools/builtin-integrations/google/google_calendar/SKILL.md +35 -0
- package/src/tools/builtin-integrations/google/google_calendar/handler.ts +105 -0
- package/src/tools/builtin-integrations/google/google_docs/SKILL.md +35 -0
- package/src/tools/builtin-integrations/google/google_docs/handler.ts +108 -0
- package/src/tools/builtin-integrations/google/google_drive/SKILL.md +39 -0
- package/src/tools/builtin-integrations/google/google_drive/handler.ts +106 -0
- package/src/tools/builtin-integrations/google/google_sheets/SKILL.md +36 -0
- package/src/tools/builtin-integrations/google/google_sheets/handler.ts +116 -0
- package/src/tools/builtin-integrations/jira/jira_boards/SKILL.md +21 -0
- package/src/tools/builtin-integrations/jira/jira_boards/handler.ts +74 -0
- package/src/tools/builtin-integrations/jira/jira_issues/SKILL.md +28 -0
- package/src/tools/builtin-integrations/jira/jira_issues/handler.ts +140 -0
- package/src/tools/builtin-integrations/linear/linear_issues/SKILL.md +30 -0
- package/src/tools/builtin-integrations/linear/linear_issues/handler.ts +75 -0
- package/src/tools/builtin-integrations/linear/linear_projects/SKILL.md +21 -0
- package/src/tools/builtin-integrations/linear/linear_projects/handler.ts +43 -0
- package/src/tools/builtin-integrations/notion/notion_databases/SKILL.md +39 -0
- package/src/tools/builtin-integrations/notion/notion_databases/handler.ts +83 -0
- package/src/tools/builtin-integrations/notion/notion_pages/SKILL.md +43 -0
- package/src/tools/builtin-integrations/notion/notion_pages/handler.ts +130 -0
- package/src/tools/builtin-integrations/notion/notion_search/SKILL.md +27 -0
- package/src/tools/builtin-integrations/notion/notion_search/handler.ts +69 -0
- package/src/tools/builtin-integrations/slack/slack_messages/SKILL.md +42 -0
- package/src/tools/builtin-integrations/slack/slack_messages/handler.ts +72 -0
- package/src/tools/builtin-integrations/twitter/twitter_posts/SKILL.md +24 -0
- package/src/tools/builtin-integrations/twitter/twitter_posts/handler.ts +133 -0
- package/src/tools/builtin-skills/file-read/SKILL.md +26 -0
- package/src/tools/builtin-skills/file-read/handler.ts +66 -0
- package/src/tools/builtin-skills/file-write/SKILL.md +30 -0
- package/src/tools/builtin-skills/file-write/handler.ts +64 -0
- package/src/tools/builtin-skills/http-request/SKILL.md +34 -0
- package/src/tools/builtin-skills/http-request/handler.ts +87 -0
- package/src/tools/builtin-skills/shell/SKILL.md +26 -0
- package/src/tools/builtin-skills/shell/handler.ts +96 -0
- package/src/tools/builtin-skills/url-fetch/SKILL.md +26 -0
- package/src/tools/builtin-skills/url-fetch/handler.ts +37 -0
- package/src/tools/builtin-skills/web-search/SKILL.md +26 -0
- package/src/tools/builtin-skills/web-search/handler.ts +50 -0
- package/src/tools/executor.ts +205 -0
- package/src/tools/integration-installer.ts +106 -0
- package/src/tools/permissions.ts +45 -0
- package/src/tools/registry.ts +39 -0
- package/src/tools/sandbox-runner.ts +56 -0
- package/src/tools/sandbox.ts +82 -0
- package/src/tools/skill-installer.ts +52 -0
- package/src/tools/skill-loader.ts +259 -0
- package/src/types/optional-deps.d.ts +23 -0
- package/src/util/auth.ts +121 -0
- package/src/util/costs.ts +59 -0
- package/src/util/error-buffer.ts +32 -0
- package/src/util/google-tokens.ts +180 -0
- package/src/util/logger.ts +73 -0
- package/src/util/perf-collector.ts +35 -0
- package/src/util/rate-limiter.ts +70 -0
- package/src/util/tokens.ts +17 -0
- package/src/voice/stt.ts +57 -0
- package/src/voice/tts.ts +103 -0
- package/tests/agent/session.test.ts +109 -0
- package/tests/agent-loop.test.ts +54 -0
- package/tests/auth.test.ts +89 -0
- package/tests/channels.test.ts +67 -0
- package/tests/compaction.test.ts +44 -0
- package/tests/config.test.ts +51 -0
- package/tests/costs.test.ts +19 -0
- package/tests/cron.test.ts +55 -0
- package/tests/db/export.test.ts +219 -0
- package/tests/executor.test.ts +144 -0
- package/tests/export.test.ts +137 -0
- package/tests/helpers/mock-llm.ts +34 -0
- package/tests/helpers/test-db.ts +74 -0
- package/tests/integration/chat-flow.test.ts +48 -0
- package/tests/integrations.test.ts +97 -0
- package/tests/memory/engine.test.ts +114 -0
- package/tests/memory-engine.test.ts +57 -0
- package/tests/permissions.test.ts +21 -0
- package/tests/rate-limiter.test.ts +70 -0
- package/tests/registry.test.ts +67 -0
- package/tests/router.test.ts +36 -0
- package/tests/session.test.ts +58 -0
- package/tests/skill-loader.test.ts +44 -0
- package/tests/tokens.test.ts +30 -0
- package/tests/tools/executor.test.ts +130 -0
- package/tests/util/auth.test.ts +75 -0
- package/tests/util/rate-limiter.test.ts +73 -0
- package/tests/voice.test.ts +60 -0
- package/tests/webchat.test.ts +88 -0
- package/tests/workflow.test.ts +38 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Client,
|
|
3
|
+
GatewayIntentBits,
|
|
4
|
+
type Message as DiscordMessage,
|
|
5
|
+
} from "discord.js";
|
|
6
|
+
import type { ChannelAdapter, InboundMessage } from "./adapter";
|
|
7
|
+
import type { MessageRouter } from "./router";
|
|
8
|
+
import { splitMessage } from "./utils";
|
|
9
|
+
import { logger } from "../util/logger";
|
|
10
|
+
|
|
11
|
+
export function createDiscordAdapter(
|
|
12
|
+
token: string,
|
|
13
|
+
allowedUsers: string[],
|
|
14
|
+
router: MessageRouter
|
|
15
|
+
): ChannelAdapter {
|
|
16
|
+
const client = new Client({
|
|
17
|
+
intents: [
|
|
18
|
+
GatewayIntentBits.Guilds,
|
|
19
|
+
GatewayIntentBits.GuildMessages,
|
|
20
|
+
GatewayIntentBits.DirectMessages,
|
|
21
|
+
GatewayIntentBits.MessageContent,
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Map session keys to channel IDs for proactive messaging
|
|
26
|
+
const channelIds = new Map<string, string>();
|
|
27
|
+
|
|
28
|
+
client.on("messageCreate", async (msg: DiscordMessage) => {
|
|
29
|
+
// Ignore bots
|
|
30
|
+
if (msg.author.bot) return;
|
|
31
|
+
|
|
32
|
+
const isDM = !msg.guild;
|
|
33
|
+
const isMentioned =
|
|
34
|
+
msg.mentions.users.has(client.user?.id ?? "") ||
|
|
35
|
+
msg.content.startsWith(`<@${client.user?.id}>`);
|
|
36
|
+
|
|
37
|
+
// In guilds, only respond when mentioned
|
|
38
|
+
if (!isDM && !isMentioned) return;
|
|
39
|
+
|
|
40
|
+
const userId = msg.author.id;
|
|
41
|
+
|
|
42
|
+
// Auto-pair: first user to DM gets registered
|
|
43
|
+
if (allowedUsers.length === 0 && isDM) {
|
|
44
|
+
allowedUsers.push(userId);
|
|
45
|
+
logger.info(`Auto-registered Discord user: ${userId}`);
|
|
46
|
+
await msg.reply(
|
|
47
|
+
"Welcome! You've been registered as the owner of this Zubo agent."
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Access control (skip for guild mentions if no allowlist)
|
|
52
|
+
if (allowedUsers.length > 0 && !allowedUsers.includes(userId)) {
|
|
53
|
+
if (isDM) {
|
|
54
|
+
await msg.reply("Sorry, you're not authorized to use this bot.");
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const sessionKey = `discord:${userId}`;
|
|
60
|
+
channelIds.set(sessionKey, msg.channel.id);
|
|
61
|
+
|
|
62
|
+
// Strip the mention from the message text
|
|
63
|
+
let text = msg.content;
|
|
64
|
+
if (isMentioned && !isDM) {
|
|
65
|
+
text = text
|
|
66
|
+
.replace(new RegExp(`<@!?${client.user?.id}>`, "g"), "")
|
|
67
|
+
.trim();
|
|
68
|
+
}
|
|
69
|
+
if (!text) return;
|
|
70
|
+
|
|
71
|
+
const message: InboundMessage = {
|
|
72
|
+
channel: "discord",
|
|
73
|
+
userId,
|
|
74
|
+
sessionKey,
|
|
75
|
+
text,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Show typing
|
|
79
|
+
if ("sendTyping" in msg.channel) {
|
|
80
|
+
await msg.channel.sendTyping().catch((err) => {
|
|
81
|
+
logger.warn("Discord sendTyping failed", { error: err.message });
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await router.handleMessage(message, async (replyText: string) => {
|
|
86
|
+
const chunks = splitMessage(replyText, 1900);
|
|
87
|
+
for (const chunk of chunks) {
|
|
88
|
+
await msg.reply(chunk).catch(async () => {
|
|
89
|
+
// If reply fails, try channel.send
|
|
90
|
+
if ("send" in msg.channel) {
|
|
91
|
+
await (msg.channel as any).send(chunk).catch((err: any) => {
|
|
92
|
+
logger.error("Discord send failed", { error: err.message });
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
client.on("error", (err) => {
|
|
101
|
+
logger.error("Discord client error", { error: err.message });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
channelName: "discord",
|
|
106
|
+
|
|
107
|
+
start() {
|
|
108
|
+
client.login(token).then(() => {
|
|
109
|
+
logger.info(`Discord bot ${client.user?.tag} started`);
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
stop() {
|
|
114
|
+
client.destroy();
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async sendMessage(sessionKey: string, text: string) {
|
|
118
|
+
const channelId = channelIds.get(sessionKey);
|
|
119
|
+
if (!channelId) {
|
|
120
|
+
logger.warn("No Discord channel ID for session", { sessionKey });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const channel = await client.channels.fetch(channelId);
|
|
125
|
+
if (channel && "send" in channel) {
|
|
126
|
+
const chunks = splitMessage(text, 1900);
|
|
127
|
+
for (const chunk of chunks) {
|
|
128
|
+
await (channel as any).send(chunk);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch (err: any) {
|
|
132
|
+
logger.error("Discord proactive send failed", { error: err.message });
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Type declarations for optional channel dependencies.
|
|
2
|
+
// These modules are dynamically imported and gracefully handle missing packages.
|
|
3
|
+
declare module "@slack/bolt" {
|
|
4
|
+
export class App {
|
|
5
|
+
constructor(options: any);
|
|
6
|
+
message(handler: (args: any) => Promise<void>): void;
|
|
7
|
+
event(name: string, handler: (args: any) => Promise<void>): void;
|
|
8
|
+
start(): Promise<void>;
|
|
9
|
+
stop(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
declare module "@whiskeysockets/baileys" {
|
|
14
|
+
export default function makeWASocket(options: any): any;
|
|
15
|
+
export function useMultiFileAuthState(path: string): Promise<{ state: any; saveCreds: () => Promise<void> }>;
|
|
16
|
+
export const DisconnectReason: { loggedOut: number };
|
|
17
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { LlmProvider } from "../llm/provider";
|
|
2
|
+
import type { InboundMessage, ChannelAdapter } from "./adapter";
|
|
3
|
+
import { agentLoop, agentLoopStream, type StreamCallbacks } from "../agent/loop";
|
|
4
|
+
import { searchMemory } from "../memory/engine";
|
|
5
|
+
import { logger } from "../util/logger";
|
|
6
|
+
import { recordError } from "../util/error-buffer";
|
|
7
|
+
import { Database } from "bun:sqlite";
|
|
8
|
+
|
|
9
|
+
/** Check if budget has been exceeded. Returns an error message if paused, null if OK. */
|
|
10
|
+
function checkBudget(db: Database): string | null {
|
|
11
|
+
try {
|
|
12
|
+
const config = db.query("SELECT daily_limit_usd, monthly_limit_usd, paused FROM budget_config WHERE id = 1").get() as {
|
|
13
|
+
daily_limit_usd: number | null;
|
|
14
|
+
monthly_limit_usd: number | null;
|
|
15
|
+
paused: number;
|
|
16
|
+
} | null;
|
|
17
|
+
if (!config) return null;
|
|
18
|
+
if (config.paused) return "Budget exceeded — agent is paused. Adjust your budget limits in the dashboard to resume.";
|
|
19
|
+
|
|
20
|
+
if (config.daily_limit_usd) {
|
|
21
|
+
const daily = db.query(
|
|
22
|
+
"SELECT COALESCE(SUM(cost_usd), 0) as total FROM usage WHERE date(created_at) = date('now') AND cost_usd IS NOT NULL"
|
|
23
|
+
).get() as { total: number };
|
|
24
|
+
if (daily.total >= config.daily_limit_usd) {
|
|
25
|
+
db.run("UPDATE budget_config SET paused = 1 WHERE id = 1");
|
|
26
|
+
return `Daily budget limit ($${config.daily_limit_usd.toFixed(2)}) reached. Agent paused.`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (config.monthly_limit_usd) {
|
|
31
|
+
const monthly = db.query(
|
|
32
|
+
"SELECT COALESCE(SUM(cost_usd), 0) as total FROM usage WHERE created_at >= datetime('now', 'start of month') AND cost_usd IS NOT NULL"
|
|
33
|
+
).get() as { total: number };
|
|
34
|
+
if (monthly.total >= config.monthly_limit_usd) {
|
|
35
|
+
db.run("UPDATE budget_config SET paused = 1 WHERE id = 1");
|
|
36
|
+
return `Monthly budget limit ($${config.monthly_limit_usd.toFixed(2)}) reached. Agent paused.`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// Budget table may not exist yet — allow through
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** All channels share one session file since Zubo is a single-owner agent. */
|
|
46
|
+
const UNIFIED_SESSION = "owner";
|
|
47
|
+
|
|
48
|
+
export interface MessageRouter {
|
|
49
|
+
handleMessage(
|
|
50
|
+
message: InboundMessage,
|
|
51
|
+
reply: (text: string) => Promise<void>
|
|
52
|
+
): Promise<void>;
|
|
53
|
+
handleMessageStream?(
|
|
54
|
+
message: InboundMessage,
|
|
55
|
+
onDelta: (text: string) => void,
|
|
56
|
+
onToolStart?: (name: string) => void,
|
|
57
|
+
onToolEnd?: (name: string) => void,
|
|
58
|
+
): Promise<string>;
|
|
59
|
+
sendProactive(sessionKey: string, task: string): Promise<string>;
|
|
60
|
+
broadcastProactive?(message: string): Promise<void>;
|
|
61
|
+
addAdapter(adapter: ChannelAdapter): void;
|
|
62
|
+
/** @deprecated Use addAdapter instead */
|
|
63
|
+
setAdapter(adapter: ChannelAdapter): void;
|
|
64
|
+
stopAll(): void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createRouter(
|
|
68
|
+
llm: LlmProvider,
|
|
69
|
+
db: Database
|
|
70
|
+
): MessageRouter {
|
|
71
|
+
const adapters = new Map<string, ChannelAdapter>();
|
|
72
|
+
|
|
73
|
+
function getAdapterForSession(sessionKey: string): ChannelAdapter | null {
|
|
74
|
+
const channel = sessionKey.split(":")[0];
|
|
75
|
+
return adapters.get(channel) ?? null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
addAdapter(adapter: ChannelAdapter) {
|
|
80
|
+
adapters.set(adapter.channelName, adapter);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// Backward compat
|
|
84
|
+
setAdapter(adapter: ChannelAdapter) {
|
|
85
|
+
adapters.set(adapter.channelName, adapter);
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
stopAll() {
|
|
89
|
+
for (const adapter of adapters.values()) {
|
|
90
|
+
adapter.stop();
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
async handleMessage(message, reply) {
|
|
95
|
+
const { sessionKey, text } = message;
|
|
96
|
+
|
|
97
|
+
logger.info(`Message from ${message.channel}:${message.userId}`, {
|
|
98
|
+
sessionKey,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Budget enforcement
|
|
102
|
+
const budgetError = checkBudget(db);
|
|
103
|
+
if (budgetError) {
|
|
104
|
+
await reply(budgetError);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// Search memory for relevant context
|
|
110
|
+
let memories = "";
|
|
111
|
+
try {
|
|
112
|
+
const results = searchMemory(db, text, 3);
|
|
113
|
+
if (results.length > 0) {
|
|
114
|
+
memories = results.map((r) => r.content).join("\n\n");
|
|
115
|
+
}
|
|
116
|
+
} catch (err: any) {
|
|
117
|
+
if (!err.message?.includes("no such table")) {
|
|
118
|
+
logger.warn("Memory search failed", { error: err.message });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result = await agentLoop(llm, UNIFIED_SESSION, text, memories);
|
|
123
|
+
if (result.reply) {
|
|
124
|
+
await reply(result.reply);
|
|
125
|
+
}
|
|
126
|
+
} catch (err: any) {
|
|
127
|
+
logger.error("Agent loop error", { error: err.message });
|
|
128
|
+
recordError("agent-loop", err.message);
|
|
129
|
+
await reply("Sorry, I encountered an error. Please try again.");
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
async handleMessageStream(message, onDelta, onToolStart?, onToolEnd?) {
|
|
134
|
+
const { text } = message;
|
|
135
|
+
|
|
136
|
+
logger.info(`Stream message from ${message.channel}:${message.userId}`);
|
|
137
|
+
|
|
138
|
+
// Budget enforcement
|
|
139
|
+
const budgetError = checkBudget(db);
|
|
140
|
+
if (budgetError) {
|
|
141
|
+
onDelta(budgetError);
|
|
142
|
+
return budgetError;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let memories = "";
|
|
146
|
+
try {
|
|
147
|
+
const results = searchMemory(db, text, 3);
|
|
148
|
+
if (results.length > 0) {
|
|
149
|
+
memories = results.map((r) => r.content).join("\n\n");
|
|
150
|
+
}
|
|
151
|
+
} catch (err: any) {
|
|
152
|
+
if (!err.message?.includes("no such table")) {
|
|
153
|
+
logger.warn("Memory search failed", { error: err.message });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return new Promise<string>((resolve, reject) => {
|
|
158
|
+
agentLoopStream(
|
|
159
|
+
llm,
|
|
160
|
+
UNIFIED_SESSION,
|
|
161
|
+
text,
|
|
162
|
+
{
|
|
163
|
+
onTextDelta: onDelta,
|
|
164
|
+
onToolStart: onToolStart ? (name) => onToolStart(name) : undefined,
|
|
165
|
+
onToolEnd: onToolEnd ? (name) => onToolEnd(name) : undefined,
|
|
166
|
+
onDone: (result) => resolve(result.reply),
|
|
167
|
+
onError: (err) => reject(err),
|
|
168
|
+
},
|
|
169
|
+
memories
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
async sendProactive(sessionKey, task) {
|
|
175
|
+
try {
|
|
176
|
+
const result = await agentLoop(llm, UNIFIED_SESSION, task);
|
|
177
|
+
const adapter = getAdapterForSession(sessionKey);
|
|
178
|
+
if (adapter && result.reply) {
|
|
179
|
+
await adapter.sendMessage(sessionKey, result.reply);
|
|
180
|
+
}
|
|
181
|
+
return result.reply;
|
|
182
|
+
} catch (err: any) {
|
|
183
|
+
logger.error("Proactive message error", { error: err.message });
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
async broadcastProactive(message) {
|
|
189
|
+
for (const [name, adapter] of adapters) {
|
|
190
|
+
try {
|
|
191
|
+
// Use a generic session key for each channel
|
|
192
|
+
await adapter.sendMessage(`${name}:owner`, message);
|
|
193
|
+
} catch (err: any) {
|
|
194
|
+
logger.warn(`Failed to broadcast to ${name}`, { error: err.message });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ChannelAdapter, InboundMessage } from "./adapter";
|
|
2
|
+
import type { MessageRouter } from "./router";
|
|
3
|
+
import { splitMessage } from "./utils";
|
|
4
|
+
import { logger } from "../util/logger";
|
|
5
|
+
|
|
6
|
+
export function createSignalAdapter(
|
|
7
|
+
phoneNumber: string,
|
|
8
|
+
allowedNumbers: string[],
|
|
9
|
+
signalCliPath: string | undefined,
|
|
10
|
+
router: MessageRouter
|
|
11
|
+
): ChannelAdapter {
|
|
12
|
+
let proc: any = null;
|
|
13
|
+
// Validate CLI path — reject shell metacharacters to prevent command injection
|
|
14
|
+
const rawCliPath = signalCliPath ?? "signal-cli";
|
|
15
|
+
if (/[;&|`$]/.test(rawCliPath)) {
|
|
16
|
+
throw new Error("Invalid signal-cli path: shell metacharacters not allowed");
|
|
17
|
+
}
|
|
18
|
+
const cliPath = rawCliPath;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
channelName: "signal",
|
|
22
|
+
|
|
23
|
+
start() {
|
|
24
|
+
try {
|
|
25
|
+
const child = Bun.spawn(
|
|
26
|
+
[cliPath, "-u", phoneNumber, "jsonRpc"],
|
|
27
|
+
{
|
|
28
|
+
stdin: "pipe",
|
|
29
|
+
stdout: "pipe",
|
|
30
|
+
stderr: "pipe",
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
proc = child;
|
|
34
|
+
|
|
35
|
+
const reader = child.stdout.getReader();
|
|
36
|
+
const decoder = new TextDecoder();
|
|
37
|
+
let buffer = "";
|
|
38
|
+
|
|
39
|
+
(async () => {
|
|
40
|
+
try {
|
|
41
|
+
while (true) {
|
|
42
|
+
const { done, value } = await reader.read();
|
|
43
|
+
if (done) break;
|
|
44
|
+
buffer += decoder.decode(value, { stream: true });
|
|
45
|
+
const lines = buffer.split("\n");
|
|
46
|
+
buffer = lines.pop() ?? "";
|
|
47
|
+
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
if (!line.trim()) continue;
|
|
50
|
+
try {
|
|
51
|
+
const msg = JSON.parse(line);
|
|
52
|
+
if (msg.method === "receive") {
|
|
53
|
+
const envelope = msg.params?.envelope;
|
|
54
|
+
if (!envelope?.dataMessage?.message) continue;
|
|
55
|
+
|
|
56
|
+
const sender = envelope.source;
|
|
57
|
+
const text = envelope.dataMessage.message;
|
|
58
|
+
|
|
59
|
+
// Auto-pair
|
|
60
|
+
if (allowedNumbers.length === 0) {
|
|
61
|
+
allowedNumbers.push(sender);
|
|
62
|
+
logger.info(`Auto-registered Signal number: ${sender}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (allowedNumbers.length > 0 && !allowedNumbers.includes(sender)) continue;
|
|
66
|
+
|
|
67
|
+
const sessionKey = `signal:${sender}`;
|
|
68
|
+
const inbound: InboundMessage = {
|
|
69
|
+
channel: "signal",
|
|
70
|
+
userId: sender,
|
|
71
|
+
sessionKey,
|
|
72
|
+
text,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
await router.handleMessage(inbound, async (reply) => {
|
|
76
|
+
const chunks = splitMessage(reply, 4000);
|
|
77
|
+
for (const chunk of chunks) {
|
|
78
|
+
sendSignalMessage(child, phoneNumber, sender, chunk);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
} catch (err: any) {
|
|
83
|
+
logger.warn("Failed to parse Signal message", { error: (err as Error).message });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch (err: any) {
|
|
88
|
+
logger.error("Signal read error", { error: err.message });
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
|
|
92
|
+
logger.info("Signal adapter started via signal-cli JSON-RPC");
|
|
93
|
+
} catch (err: any) {
|
|
94
|
+
logger.error("Failed to start Signal adapter — ensure signal-cli is installed", { error: err.message });
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
stop() {
|
|
99
|
+
if (proc) {
|
|
100
|
+
proc.kill();
|
|
101
|
+
proc = null;
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
async sendMessage(sessionKey: string, text: string) {
|
|
106
|
+
if (!proc) return;
|
|
107
|
+
const number = sessionKey.split(":")[1];
|
|
108
|
+
if (!number) return;
|
|
109
|
+
const chunks = splitMessage(text, 4000);
|
|
110
|
+
for (const chunk of chunks) {
|
|
111
|
+
sendSignalMessage(proc, phoneNumber, number, chunk);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sendSignalMessage(proc: any, from: string, to: string, text: string) {
|
|
118
|
+
const rpcMsg = JSON.stringify({
|
|
119
|
+
jsonrpc: "2.0",
|
|
120
|
+
method: "send",
|
|
121
|
+
id: Date.now().toString(),
|
|
122
|
+
params: {
|
|
123
|
+
recipient: [to],
|
|
124
|
+
message: text,
|
|
125
|
+
},
|
|
126
|
+
}) + "\n";
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
proc.stdin.write(rpcMsg);
|
|
130
|
+
} catch (err: any) {
|
|
131
|
+
logger.error("Signal send failed", { error: err.message });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { ChannelAdapter, InboundMessage } from "./adapter";
|
|
2
|
+
import type { MessageRouter } from "./router";
|
|
3
|
+
import { splitMessage } from "./utils";
|
|
4
|
+
import { logger } from "../util/logger";
|
|
5
|
+
|
|
6
|
+
export function createSlackAdapter(
|
|
7
|
+
botToken: string,
|
|
8
|
+
appToken: string,
|
|
9
|
+
allowedUsers: string[],
|
|
10
|
+
router: MessageRouter
|
|
11
|
+
): ChannelAdapter {
|
|
12
|
+
let app: any = null;
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
channelName: "slack",
|
|
16
|
+
|
|
17
|
+
start() {
|
|
18
|
+
import("@slack/bolt").then(({ App }) => {
|
|
19
|
+
app = new App({
|
|
20
|
+
token: botToken,
|
|
21
|
+
appToken: appToken,
|
|
22
|
+
socketMode: true,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
app.message(async ({ message, say }: any) => {
|
|
26
|
+
if (message.subtype) return;
|
|
27
|
+
const userId = message.user;
|
|
28
|
+
|
|
29
|
+
// Auto-pair first user
|
|
30
|
+
if (allowedUsers.length === 0) {
|
|
31
|
+
allowedUsers.push(userId);
|
|
32
|
+
logger.info(`Auto-registered Slack user: ${userId}`);
|
|
33
|
+
await say("Welcome! You've been registered as the owner of this Zubo agent.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (allowedUsers.length > 0 && !allowedUsers.includes(userId)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sessionKey = `slack:${userId}`;
|
|
41
|
+
const inbound: InboundMessage = {
|
|
42
|
+
channel: "slack",
|
|
43
|
+
userId,
|
|
44
|
+
sessionKey,
|
|
45
|
+
text: message.text || "",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
await router.handleMessage(inbound, async (reply) => {
|
|
49
|
+
const chunks = splitMessage(reply, 3000);
|
|
50
|
+
for (const chunk of chunks) {
|
|
51
|
+
await say(chunk);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
app.event("app_mention", async ({ event, say }: any) => {
|
|
57
|
+
const userId = event.user;
|
|
58
|
+
if (allowedUsers.length > 0 && !allowedUsers.includes(userId)) return;
|
|
59
|
+
|
|
60
|
+
const text = event.text.replace(/<@[^>]+>/g, "").trim();
|
|
61
|
+
if (!text) return;
|
|
62
|
+
|
|
63
|
+
const sessionKey = `slack:${userId}`;
|
|
64
|
+
const inbound: InboundMessage = {
|
|
65
|
+
channel: "slack",
|
|
66
|
+
userId,
|
|
67
|
+
sessionKey,
|
|
68
|
+
text,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
await router.handleMessage(inbound, async (reply) => {
|
|
72
|
+
const chunks = splitMessage(reply, 3000);
|
|
73
|
+
for (const chunk of chunks) {
|
|
74
|
+
await say(chunk);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
app.start().then(() => {
|
|
80
|
+
logger.info("Slack adapter started (Socket Mode)");
|
|
81
|
+
});
|
|
82
|
+
}).catch((err: any) => {
|
|
83
|
+
logger.error("Failed to start Slack adapter — install @slack/bolt", { error: err.message });
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
stop() {
|
|
88
|
+
if (app) {
|
|
89
|
+
app.stop().catch((err: any) => {
|
|
90
|
+
logger.warn("Slack app stop failed", { error: err.message });
|
|
91
|
+
});
|
|
92
|
+
app = null;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
async sendMessage(sessionKey: string, text: string) {
|
|
97
|
+
// For proactive messages we'd need channel IDs; skip for now
|
|
98
|
+
logger.debug("Slack proactive message (not delivered via DM)", { text: text.slice(0, 100) });
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Bot } from "grammy";
|
|
2
|
+
import type { ChannelAdapter, InboundMessage } from "./adapter";
|
|
3
|
+
import type { MessageRouter } from "./router";
|
|
4
|
+
import type { ZuboConfig } from "../config/schema";
|
|
5
|
+
import { saveConfig } from "../config/loader";
|
|
6
|
+
import { splitMessage } from "./utils";
|
|
7
|
+
import { logger } from "../util/logger";
|
|
8
|
+
|
|
9
|
+
export function createTelegramAdapter(
|
|
10
|
+
token: string,
|
|
11
|
+
config: ZuboConfig,
|
|
12
|
+
router: MessageRouter
|
|
13
|
+
): ChannelAdapter {
|
|
14
|
+
const bot = new Bot(token);
|
|
15
|
+
|
|
16
|
+
// Map session keys to chat IDs for proactive messaging
|
|
17
|
+
const chatIds = new Map<string, number>();
|
|
18
|
+
|
|
19
|
+
// Pre-populate chat IDs from config (for proactive messaging after restart).
|
|
20
|
+
// For DMs, chat ID equals user ID.
|
|
21
|
+
for (const userId of config.telegramAllowedUsers) {
|
|
22
|
+
chatIds.set(`telegram:${userId}`, userId);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
bot.on("message:text", async (ctx) => {
|
|
26
|
+
const userId = ctx.from.id;
|
|
27
|
+
const chatId = ctx.chat.id;
|
|
28
|
+
const text = ctx.message.text;
|
|
29
|
+
|
|
30
|
+
// Auto-pair: first user to message gets registered
|
|
31
|
+
if (config.telegramAllowedUsers.length === 0) {
|
|
32
|
+
config.telegramAllowedUsers.push(userId);
|
|
33
|
+
await saveConfig(config);
|
|
34
|
+
logger.info(`Auto-registered Telegram user: ${userId}`);
|
|
35
|
+
await ctx.reply(
|
|
36
|
+
"Welcome! You've been registered as the owner of this Zubo agent."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Access control
|
|
41
|
+
if (!config.telegramAllowedUsers.includes(userId)) {
|
|
42
|
+
await ctx.reply("Sorry, you're not authorized to use this bot.");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const sessionKey = `telegram:${userId}`;
|
|
47
|
+
chatIds.set(sessionKey, chatId);
|
|
48
|
+
|
|
49
|
+
const message: InboundMessage = {
|
|
50
|
+
channel: "telegram",
|
|
51
|
+
userId: String(userId),
|
|
52
|
+
sessionKey,
|
|
53
|
+
text,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Show typing indicator
|
|
57
|
+
await ctx.replyWithChatAction("typing");
|
|
58
|
+
|
|
59
|
+
await router.handleMessage(message, async (replyText: string) => {
|
|
60
|
+
// Split long messages (Telegram 4096 char limit)
|
|
61
|
+
const chunks = splitMessage(replyText, 4000);
|
|
62
|
+
for (const chunk of chunks) {
|
|
63
|
+
await ctx.reply(chunk, { parse_mode: "Markdown" }).catch(async () => {
|
|
64
|
+
// Fallback without markdown if parsing fails
|
|
65
|
+
await ctx.reply(chunk);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
bot.catch((err) => {
|
|
72
|
+
logger.error("Telegram bot error", { error: err.message });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
channelName: "telegram",
|
|
77
|
+
start() {
|
|
78
|
+
bot.start({
|
|
79
|
+
onStart: (info) => {
|
|
80
|
+
logger.info(`Telegram bot @${info.username} started`);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
stop() {
|
|
85
|
+
bot.stop();
|
|
86
|
+
},
|
|
87
|
+
async sendMessage(sessionKey: string, text: string) {
|
|
88
|
+
const chatId = chatIds.get(sessionKey);
|
|
89
|
+
if (chatId) {
|
|
90
|
+
const chunks = splitMessage(text, 4000);
|
|
91
|
+
for (const chunk of chunks) {
|
|
92
|
+
await bot.api.sendMessage(chatId, chunk).catch((err) => {
|
|
93
|
+
logger.error("Failed to send Telegram message", { error: err.message });
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
logger.warn("No chat ID for session key", { sessionKey });
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|