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.
Files changed (222) hide show
  1. package/.github/workflows/ci.yml +35 -0
  2. package/README.md +149 -0
  3. package/bun.lock +216 -0
  4. package/desktop/README.md +57 -0
  5. package/desktop/package.json +12 -0
  6. package/desktop/src-tauri/Cargo.toml +25 -0
  7. package/desktop/src-tauri/build.rs +3 -0
  8. package/desktop/src-tauri/icons/README.md +17 -0
  9. package/desktop/src-tauri/icons/icon.png +0 -0
  10. package/desktop/src-tauri/src/main.rs +189 -0
  11. package/desktop/src-tauri/tauri.conf.json +68 -0
  12. package/docs/ROADMAP.md +490 -0
  13. package/migrations/001_init.sql +9 -0
  14. package/migrations/002_memory.sql +33 -0
  15. package/migrations/003_cron.sql +24 -0
  16. package/migrations/004_usage.sql +12 -0
  17. package/migrations/005_secrets.sql +8 -0
  18. package/migrations/006_agents.sql +1 -0
  19. package/migrations/007_workflows.sql +22 -0
  20. package/migrations/008_proactive.sql +24 -0
  21. package/migrations/009_uploads.sql +9 -0
  22. package/migrations/010_observability.sql +22 -0
  23. package/migrations/011_api_keys.sql +7 -0
  24. package/migrations/012_indexes.sql +5 -0
  25. package/migrations/013_budget.sql +11 -0
  26. package/migrations/014_usage_session_idx.sql +2 -0
  27. package/package.json +39 -0
  28. package/site/404.html +156 -0
  29. package/site/CNAME +1 -0
  30. package/site/docs/agents.html +294 -0
  31. package/site/docs/api.html +446 -0
  32. package/site/docs/channels.html +345 -0
  33. package/site/docs/cli.html +238 -0
  34. package/site/docs/config.html +1034 -0
  35. package/site/docs/index.html +433 -0
  36. package/site/docs/integrations.html +381 -0
  37. package/site/docs/memory.html +254 -0
  38. package/site/docs/security.html +375 -0
  39. package/site/docs/skills.html +322 -0
  40. package/site/docs.css +412 -0
  41. package/site/index.html +638 -0
  42. package/site/install.sh +98 -0
  43. package/site/logo.svg +1 -0
  44. package/site/og-image.png +0 -0
  45. package/site/robots.txt +4 -0
  46. package/site/script.js +361 -0
  47. package/site/sitemap.xml +63 -0
  48. package/site/skills.html +532 -0
  49. package/site/style.css +1686 -0
  50. package/src/agent/agents.ts +159 -0
  51. package/src/agent/compaction.ts +53 -0
  52. package/src/agent/context.ts +18 -0
  53. package/src/agent/delegate.ts +118 -0
  54. package/src/agent/loop.ts +318 -0
  55. package/src/agent/prompts.ts +111 -0
  56. package/src/agent/session.ts +87 -0
  57. package/src/agent/teams.ts +116 -0
  58. package/src/agent/workflow-executor.ts +192 -0
  59. package/src/agent/workflow.ts +175 -0
  60. package/src/channels/adapter.ts +21 -0
  61. package/src/channels/dashboard.html.ts +2969 -0
  62. package/src/channels/discord.ts +137 -0
  63. package/src/channels/optional-deps.d.ts +17 -0
  64. package/src/channels/router.ts +199 -0
  65. package/src/channels/signal.ts +133 -0
  66. package/src/channels/slack.ts +101 -0
  67. package/src/channels/telegram.ts +102 -0
  68. package/src/channels/utils.ts +18 -0
  69. package/src/channels/webchat.ts +1797 -0
  70. package/src/channels/whatsapp.ts +119 -0
  71. package/src/config/loader.ts +22 -0
  72. package/src/config/paths.ts +43 -0
  73. package/src/config/schema.ts +121 -0
  74. package/src/db/connection.ts +20 -0
  75. package/src/db/export.ts +148 -0
  76. package/src/db/migrations.ts +42 -0
  77. package/src/index.ts +261 -0
  78. package/src/llm/claude.ts +193 -0
  79. package/src/llm/factory.ts +115 -0
  80. package/src/llm/failover.ts +101 -0
  81. package/src/llm/openai-compat.ts +409 -0
  82. package/src/llm/provider.ts +83 -0
  83. package/src/llm/smart-router.ts +241 -0
  84. package/src/logs.ts +53 -0
  85. package/src/memory/chunker.ts +58 -0
  86. package/src/memory/document-parser.ts +115 -0
  87. package/src/memory/embedder.ts +235 -0
  88. package/src/memory/engine.ts +170 -0
  89. package/src/memory/fts-index.ts +55 -0
  90. package/src/memory/hybrid-search.ts +72 -0
  91. package/src/memory/store.ts +56 -0
  92. package/src/memory/vector-index.ts +72 -0
  93. package/src/model.ts +118 -0
  94. package/src/registry/cli.ts +43 -0
  95. package/src/registry/client.ts +54 -0
  96. package/src/registry/installer.ts +67 -0
  97. package/src/scheduler/briefing.ts +71 -0
  98. package/src/scheduler/cron.ts +258 -0
  99. package/src/scheduler/heartbeat.ts +58 -0
  100. package/src/scheduler/memory-triggers.ts +100 -0
  101. package/src/scheduler/natural-cron.ts +163 -0
  102. package/src/scheduler/proactive.ts +25 -0
  103. package/src/scheduler/recipes.ts +110 -0
  104. package/src/secrets/store.ts +64 -0
  105. package/src/setup.ts +413 -0
  106. package/src/skills.ts +293 -0
  107. package/src/start.ts +373 -0
  108. package/src/status.ts +165 -0
  109. package/src/tools/builtin/connect-service.ts +205 -0
  110. package/src/tools/builtin/cron.ts +126 -0
  111. package/src/tools/builtin/datetime.ts +36 -0
  112. package/src/tools/builtin/delegate-task.ts +81 -0
  113. package/src/tools/builtin/delegate.ts +42 -0
  114. package/src/tools/builtin/diagnose.ts +41 -0
  115. package/src/tools/builtin/google-oauth.ts +379 -0
  116. package/src/tools/builtin/manage-agents.ts +149 -0
  117. package/src/tools/builtin/manage-skills.ts +294 -0
  118. package/src/tools/builtin/manage-teams.ts +89 -0
  119. package/src/tools/builtin/manage-triggers.ts +94 -0
  120. package/src/tools/builtin/manage-workflows.ts +119 -0
  121. package/src/tools/builtin/memory-search.ts +38 -0
  122. package/src/tools/builtin/memory-write.ts +30 -0
  123. package/src/tools/builtin/run-workflow.ts +36 -0
  124. package/src/tools/builtin/secrets.ts +122 -0
  125. package/src/tools/builtin/skill-registry.ts +75 -0
  126. package/src/tools/builtin-integrations/api-helpers.ts +26 -0
  127. package/src/tools/builtin-integrations/github/github_issues/SKILL.md +56 -0
  128. package/src/tools/builtin-integrations/github/github_issues/handler.ts +108 -0
  129. package/src/tools/builtin-integrations/github/github_prs/SKILL.md +57 -0
  130. package/src/tools/builtin-integrations/github/github_prs/handler.ts +113 -0
  131. package/src/tools/builtin-integrations/github/github_repos/SKILL.md +37 -0
  132. package/src/tools/builtin-integrations/github/github_repos/handler.ts +88 -0
  133. package/src/tools/builtin-integrations/google/gmail/SKILL.md +51 -0
  134. package/src/tools/builtin-integrations/google/gmail/handler.ts +125 -0
  135. package/src/tools/builtin-integrations/google/google_calendar/SKILL.md +35 -0
  136. package/src/tools/builtin-integrations/google/google_calendar/handler.ts +105 -0
  137. package/src/tools/builtin-integrations/google/google_docs/SKILL.md +35 -0
  138. package/src/tools/builtin-integrations/google/google_docs/handler.ts +108 -0
  139. package/src/tools/builtin-integrations/google/google_drive/SKILL.md +39 -0
  140. package/src/tools/builtin-integrations/google/google_drive/handler.ts +106 -0
  141. package/src/tools/builtin-integrations/google/google_sheets/SKILL.md +36 -0
  142. package/src/tools/builtin-integrations/google/google_sheets/handler.ts +116 -0
  143. package/src/tools/builtin-integrations/jira/jira_boards/SKILL.md +21 -0
  144. package/src/tools/builtin-integrations/jira/jira_boards/handler.ts +74 -0
  145. package/src/tools/builtin-integrations/jira/jira_issues/SKILL.md +28 -0
  146. package/src/tools/builtin-integrations/jira/jira_issues/handler.ts +140 -0
  147. package/src/tools/builtin-integrations/linear/linear_issues/SKILL.md +30 -0
  148. package/src/tools/builtin-integrations/linear/linear_issues/handler.ts +75 -0
  149. package/src/tools/builtin-integrations/linear/linear_projects/SKILL.md +21 -0
  150. package/src/tools/builtin-integrations/linear/linear_projects/handler.ts +43 -0
  151. package/src/tools/builtin-integrations/notion/notion_databases/SKILL.md +39 -0
  152. package/src/tools/builtin-integrations/notion/notion_databases/handler.ts +83 -0
  153. package/src/tools/builtin-integrations/notion/notion_pages/SKILL.md +43 -0
  154. package/src/tools/builtin-integrations/notion/notion_pages/handler.ts +130 -0
  155. package/src/tools/builtin-integrations/notion/notion_search/SKILL.md +27 -0
  156. package/src/tools/builtin-integrations/notion/notion_search/handler.ts +69 -0
  157. package/src/tools/builtin-integrations/slack/slack_messages/SKILL.md +42 -0
  158. package/src/tools/builtin-integrations/slack/slack_messages/handler.ts +72 -0
  159. package/src/tools/builtin-integrations/twitter/twitter_posts/SKILL.md +24 -0
  160. package/src/tools/builtin-integrations/twitter/twitter_posts/handler.ts +133 -0
  161. package/src/tools/builtin-skills/file-read/SKILL.md +26 -0
  162. package/src/tools/builtin-skills/file-read/handler.ts +66 -0
  163. package/src/tools/builtin-skills/file-write/SKILL.md +30 -0
  164. package/src/tools/builtin-skills/file-write/handler.ts +64 -0
  165. package/src/tools/builtin-skills/http-request/SKILL.md +34 -0
  166. package/src/tools/builtin-skills/http-request/handler.ts +87 -0
  167. package/src/tools/builtin-skills/shell/SKILL.md +26 -0
  168. package/src/tools/builtin-skills/shell/handler.ts +96 -0
  169. package/src/tools/builtin-skills/url-fetch/SKILL.md +26 -0
  170. package/src/tools/builtin-skills/url-fetch/handler.ts +37 -0
  171. package/src/tools/builtin-skills/web-search/SKILL.md +26 -0
  172. package/src/tools/builtin-skills/web-search/handler.ts +50 -0
  173. package/src/tools/executor.ts +205 -0
  174. package/src/tools/integration-installer.ts +106 -0
  175. package/src/tools/permissions.ts +45 -0
  176. package/src/tools/registry.ts +39 -0
  177. package/src/tools/sandbox-runner.ts +56 -0
  178. package/src/tools/sandbox.ts +82 -0
  179. package/src/tools/skill-installer.ts +52 -0
  180. package/src/tools/skill-loader.ts +259 -0
  181. package/src/types/optional-deps.d.ts +23 -0
  182. package/src/util/auth.ts +121 -0
  183. package/src/util/costs.ts +59 -0
  184. package/src/util/error-buffer.ts +32 -0
  185. package/src/util/google-tokens.ts +180 -0
  186. package/src/util/logger.ts +73 -0
  187. package/src/util/perf-collector.ts +35 -0
  188. package/src/util/rate-limiter.ts +70 -0
  189. package/src/util/tokens.ts +17 -0
  190. package/src/voice/stt.ts +57 -0
  191. package/src/voice/tts.ts +103 -0
  192. package/tests/agent/session.test.ts +109 -0
  193. package/tests/agent-loop.test.ts +54 -0
  194. package/tests/auth.test.ts +89 -0
  195. package/tests/channels.test.ts +67 -0
  196. package/tests/compaction.test.ts +44 -0
  197. package/tests/config.test.ts +51 -0
  198. package/tests/costs.test.ts +19 -0
  199. package/tests/cron.test.ts +55 -0
  200. package/tests/db/export.test.ts +219 -0
  201. package/tests/executor.test.ts +144 -0
  202. package/tests/export.test.ts +137 -0
  203. package/tests/helpers/mock-llm.ts +34 -0
  204. package/tests/helpers/test-db.ts +74 -0
  205. package/tests/integration/chat-flow.test.ts +48 -0
  206. package/tests/integrations.test.ts +97 -0
  207. package/tests/memory/engine.test.ts +114 -0
  208. package/tests/memory-engine.test.ts +57 -0
  209. package/tests/permissions.test.ts +21 -0
  210. package/tests/rate-limiter.test.ts +70 -0
  211. package/tests/registry.test.ts +67 -0
  212. package/tests/router.test.ts +36 -0
  213. package/tests/session.test.ts +58 -0
  214. package/tests/skill-loader.test.ts +44 -0
  215. package/tests/tokens.test.ts +30 -0
  216. package/tests/tools/executor.test.ts +130 -0
  217. package/tests/util/auth.test.ts +75 -0
  218. package/tests/util/rate-limiter.test.ts +73 -0
  219. package/tests/voice.test.ts +60 -0
  220. package/tests/webchat.test.ts +88 -0
  221. package/tests/workflow.test.ts +38 -0
  222. 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
+