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,119 @@
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
+ import { join } from "path";
6
+ import { paths } from "../config/paths";
7
+
8
+ export function createWhatsAppAdapter(
9
+ allowedNumbers: string[],
10
+ authDir: string | undefined,
11
+ router: MessageRouter
12
+ ): ChannelAdapter {
13
+ let sock: any = null;
14
+ const sessionDir = authDir ?? join(paths.root, "whatsapp-auth");
15
+
16
+ return {
17
+ channelName: "whatsapp",
18
+
19
+ start() {
20
+ import("@whiskeysockets/baileys").then(async (baileys) => {
21
+ const { default: makeWASocket, useMultiFileAuthState, DisconnectReason } = baileys;
22
+ const { mkdirSync } = await import("fs");
23
+ mkdirSync(sessionDir, { recursive: true });
24
+
25
+ const { state, saveCreds } = await useMultiFileAuthState(sessionDir);
26
+
27
+ sock = makeWASocket({
28
+ auth: state,
29
+ printQRInTerminal: true,
30
+ });
31
+
32
+ sock.ev.on("creds.update", saveCreds);
33
+
34
+ sock.ev.on("connection.update", (update: any) => {
35
+ const { connection, lastDisconnect, qr } = update;
36
+ if (qr) {
37
+ logger.info("WhatsApp QR code displayed in terminal — scan to connect");
38
+ }
39
+ if (connection === "close") {
40
+ const shouldReconnect =
41
+ lastDisconnect?.error?.output?.statusCode !== DisconnectReason.loggedOut;
42
+ if (shouldReconnect) {
43
+ logger.info("WhatsApp reconnecting...");
44
+ // Re-create connection
45
+ }
46
+ } else if (connection === "open") {
47
+ logger.info("WhatsApp connected");
48
+ }
49
+ });
50
+
51
+ sock.ev.on("messages.upsert", async ({ messages }: any) => {
52
+ for (const msg of messages) {
53
+ if (!msg.message || msg.key.fromMe) continue;
54
+ const jid = msg.key.remoteJid;
55
+ if (!jid || jid.endsWith("@g.us")) continue; // Skip groups
56
+
57
+ const number = jid.replace("@s.whatsapp.net", "");
58
+
59
+ // Auto-pair
60
+ if (allowedNumbers.length === 0) {
61
+ allowedNumbers.push(number);
62
+ logger.info(`Auto-registered WhatsApp number: ${number}`);
63
+ await sock.sendMessage(jid, { text: "Welcome! You've been registered as the owner." });
64
+ }
65
+
66
+ if (allowedNumbers.length > 0 && !allowedNumbers.includes(number)) continue;
67
+
68
+ const text =
69
+ msg.message.conversation ||
70
+ msg.message.extendedTextMessage?.text ||
71
+ "";
72
+ if (!text) continue;
73
+
74
+ const sessionKey = `whatsapp:${number}`;
75
+ const inbound: InboundMessage = {
76
+ channel: "whatsapp",
77
+ userId: number,
78
+ sessionKey,
79
+ text,
80
+ };
81
+
82
+ // Show composing indicator
83
+ await sock.sendPresenceUpdate("composing", jid);
84
+
85
+ await router.handleMessage(inbound, async (reply) => {
86
+ await sock.sendPresenceUpdate("paused", jid);
87
+ const chunks = splitMessage(reply, 4000);
88
+ for (const chunk of chunks) {
89
+ await sock.sendMessage(jid, { text: chunk });
90
+ }
91
+ });
92
+ }
93
+ });
94
+ }).catch((err: any) => {
95
+ logger.error("Failed to start WhatsApp — install @whiskeysockets/baileys", { error: err.message });
96
+ });
97
+ },
98
+
99
+ stop() {
100
+ if (sock) {
101
+ sock.end();
102
+ sock = null;
103
+ }
104
+ },
105
+
106
+ async sendMessage(sessionKey: string, text: string) {
107
+ if (!sock) return;
108
+ const number = sessionKey.split(":")[1];
109
+ if (!number) return;
110
+ const jid = `${number}@s.whatsapp.net`;
111
+ const chunks = splitMessage(text, 4000);
112
+ for (const chunk of chunks) {
113
+ await sock.sendMessage(jid, { text: chunk }).catch((err: any) => {
114
+ logger.error("WhatsApp send failed", { error: err.message });
115
+ });
116
+ }
117
+ },
118
+ };
119
+ }
@@ -0,0 +1,22 @@
1
+ import { paths, ensureDirectories } from "./paths";
2
+ import { configSchema, type ZuboConfig } from "./schema";
3
+ import { existsSync } from "fs";
4
+
5
+ export async function loadConfig(): Promise<ZuboConfig> {
6
+ if (!existsSync(paths.config)) {
7
+ throw new Error(
8
+ `Config not found at ${paths.config}. Run 'zubo setup' first.`
9
+ );
10
+ }
11
+ const raw = await Bun.file(paths.config).json();
12
+ return configSchema.parse(raw);
13
+ }
14
+
15
+ export async function saveConfig(config: ZuboConfig): Promise<void> {
16
+ ensureDirectories();
17
+ await Bun.write(paths.config, JSON.stringify(config, null, 2) + "\n");
18
+ }
19
+
20
+ export function configExists(): boolean {
21
+ return existsSync(paths.config);
22
+ }
@@ -0,0 +1,43 @@
1
+ import { join } from "path";
2
+ import { homedir } from "os";
3
+
4
+ const ZUBO_HOME = join(homedir(), ".zubo");
5
+
6
+ export const paths = {
7
+ root: ZUBO_HOME,
8
+ config: join(ZUBO_HOME, "config.json"),
9
+ db: join(ZUBO_HOME, "zubo.db"),
10
+ workspace: join(ZUBO_HOME, "workspace"),
11
+ memory: join(ZUBO_HOME, "workspace", "memory"),
12
+ memoryFile: join(ZUBO_HOME, "workspace", "MEMORY.md"),
13
+ systemPrompt: join(ZUBO_HOME, "workspace", "SYSTEM.md"),
14
+ sessions: join(ZUBO_HOME, "sessions"),
15
+ models: join(ZUBO_HOME, "models"),
16
+ logs: join(ZUBO_HOME, "logs"),
17
+ logFile: join(ZUBO_HOME, "logs", "zubo.log"),
18
+ skills: join(ZUBO_HOME, "workspace", "skills"),
19
+ agents: join(ZUBO_HOME, "workspace", "agents"),
20
+ workflows: join(ZUBO_HOME, "workspace", "workflows"),
21
+ teams: join(ZUBO_HOME, "workspace", "teams"),
22
+ uploads: join(ZUBO_HOME, "workspace", "uploads"),
23
+ pidFile: join(ZUBO_HOME, "zubo.pid"),
24
+ };
25
+
26
+ export function ensureDirectories() {
27
+ const dirs = [
28
+ paths.root,
29
+ paths.workspace,
30
+ paths.memory,
31
+ paths.sessions,
32
+ paths.models,
33
+ paths.logs,
34
+ paths.skills,
35
+ paths.agents,
36
+ paths.workflows,
37
+ paths.teams,
38
+ paths.uploads,
39
+ ];
40
+ for (const dir of dirs) {
41
+ Bun.spawnSync(["mkdir", "-p", dir]);
42
+ }
43
+ }
@@ -0,0 +1,121 @@
1
+ import { z } from "zod";
2
+
3
+ const providerConfigSchema = z.object({
4
+ apiKey: z.string().optional(),
5
+ baseUrl: z.string().optional(),
6
+ model: z.string(),
7
+ streaming: z.boolean().optional(),
8
+ contextWindow: z.number().optional(),
9
+ });
10
+
11
+ export type ProviderConfig = z.infer<typeof providerConfigSchema>;
12
+
13
+ const channelsConfigSchema = z.object({
14
+ telegram: z
15
+ .object({
16
+ enabled: z.boolean().default(true),
17
+ botToken: z.string().min(1),
18
+ allowedUsers: z.array(z.number()).default([]),
19
+ })
20
+ .optional(),
21
+ discord: z
22
+ .object({
23
+ enabled: z.boolean().default(true),
24
+ botToken: z.string().min(1),
25
+ allowedUsers: z.array(z.string()).default([]),
26
+ })
27
+ .optional(),
28
+ webchat: z
29
+ .object({
30
+ enabled: z.boolean().default(true),
31
+ port: z.number().default(0),
32
+ })
33
+ .optional(),
34
+ slack: z.object({
35
+ enabled: z.boolean().default(true),
36
+ botToken: z.string().min(1),
37
+ appToken: z.string().min(1),
38
+ allowedUsers: z.array(z.string()).default([]),
39
+ }).optional(),
40
+ whatsapp: z.object({
41
+ enabled: z.boolean().default(true),
42
+ authDir: z.string().optional(),
43
+ allowedNumbers: z.array(z.string()).default([]),
44
+ }).optional(),
45
+ signal: z.object({
46
+ enabled: z.boolean().default(true),
47
+ phoneNumber: z.string().min(1),
48
+ signalCliPath: z.string().optional(),
49
+ allowedNumbers: z.array(z.string()).default([]),
50
+ }).optional(),
51
+ });
52
+
53
+ export type ChannelsConfig = z.infer<typeof channelsConfigSchema>;
54
+
55
+ export const configSchema = z.object({
56
+ // Legacy fields (still work for backward compat)
57
+ anthropicApiKey: z.string().optional(),
58
+ model: z.string().optional(),
59
+ telegramBotToken: z.string().optional(),
60
+ telegramAllowedUsers: z.array(z.number()).default([]),
61
+
62
+ // Multi-provider system
63
+ providers: z.record(z.string(), providerConfigSchema).optional(),
64
+ activeProvider: z.string().optional(),
65
+ failover: z.array(z.string()).optional(),
66
+
67
+ // Multi-channel system
68
+ channels: channelsConfigSchema.optional(),
69
+
70
+ // Voice (STT/TTS)
71
+ voice: z.object({
72
+ stt: z.object({
73
+ provider: z.string().default("whisper"),
74
+ apiKey: z.string().min(1),
75
+ model: z.string().optional(),
76
+ }).optional(),
77
+ tts: z.object({
78
+ provider: z.string().default("openai"),
79
+ apiKey: z.string().min(1),
80
+ voice: z.string().optional(),
81
+ }).optional(),
82
+ }).optional(),
83
+
84
+ // Agent
85
+ maxTurns: z.number().default(50),
86
+ heartbeatMinutes: z.number().min(1).max(1440).default(30),
87
+ createdAt: z.string().default(() => new Date().toISOString()),
88
+
89
+ // Rate limiting
90
+ rateLimit: z.object({
91
+ chatPerMinute: z.number().default(60),
92
+ uploadPerMinute: z.number().default(10),
93
+ }).optional(),
94
+
95
+ // API authentication
96
+ auth: z.object({
97
+ enabled: z.boolean().default(false),
98
+ }).optional(),
99
+
100
+ // Skill sandboxing
101
+ sandbox: z.object({
102
+ enabled: z.boolean().default(true),
103
+ timeoutMs: z.number().default(30_000),
104
+ }).optional(),
105
+
106
+ // Smart model routing
107
+ smartRouting: z.object({
108
+ enabled: z.boolean().default(false),
109
+ fastProvider: z.string().optional(),
110
+ fastModel: z.string().optional(),
111
+ }).optional(),
112
+
113
+ // Budget controls
114
+ budget: z.object({
115
+ dailyLimitUsd: z.number().optional(),
116
+ monthlyLimitUsd: z.number().optional(),
117
+ alertThreshold: z.number().min(0).max(1).default(0.8),
118
+ }).optional(),
119
+ });
120
+
121
+ export type ZuboConfig = z.infer<typeof configSchema>;
@@ -0,0 +1,20 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { paths } from "../config/paths";
3
+
4
+ let _db: Database | null = null;
5
+
6
+ export function getDb(): Database {
7
+ if (_db) return _db;
8
+ _db = new Database(paths.db, { create: true });
9
+ _db.run("PRAGMA journal_mode = WAL");
10
+ _db.run("PRAGMA foreign_keys = ON");
11
+ _db.run("PRAGMA busy_timeout = 5000");
12
+ return _db;
13
+ }
14
+
15
+ export function closeDb() {
16
+ if (_db) {
17
+ _db.close();
18
+ _db = null;
19
+ }
20
+ }
@@ -0,0 +1,148 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "fs";
3
+ import { join, dirname } from "path";
4
+
5
+ /**
6
+ * Database export/import/backup utilities.
7
+ */
8
+
9
+ interface ExportData {
10
+ version: 1;
11
+ exportedAt: string;
12
+ tables: Record<string, any[]>;
13
+ }
14
+
15
+ function quoteId(name: string): string {
16
+ return '"' + name.replace(/"/g, '""') + '"';
17
+ }
18
+
19
+ // Tables to include in export (skip FTS virtual tables and internal sqlite tables)
20
+ // Tables to include in export — secrets and api_keys are excluded by default
21
+ // to prevent accidental credential exposure
22
+ const EXPORT_TABLES = [
23
+ "sessions",
24
+ "messages",
25
+ "memory_chunks",
26
+ "cron_jobs",
27
+ "cron_logs",
28
+ "usage",
29
+ "agents",
30
+ "workflows",
31
+ "workflow_executions",
32
+ "proactive_triggers",
33
+ "uploads",
34
+ "tool_metrics",
35
+ "perf_snapshots",
36
+ ];
37
+
38
+ export function exportDatabase(db: Database, outputPath: string): void {
39
+ const data: ExportData = {
40
+ version: 1,
41
+ exportedAt: new Date().toISOString(),
42
+ tables: {},
43
+ };
44
+
45
+ for (const table of EXPORT_TABLES) {
46
+ try {
47
+ const rows = db.query(`SELECT * FROM ${quoteId(table)}`).all();
48
+ if (rows.length > 0) {
49
+ data.tables[table] = rows;
50
+ }
51
+ } catch {
52
+ // Table may not exist yet — skip
53
+ }
54
+ }
55
+
56
+ mkdirSync(dirname(outputPath), { recursive: true });
57
+ writeFileSync(outputPath, JSON.stringify(data, null, 2));
58
+ }
59
+
60
+ export function importDatabase(db: Database, inputPath: string): { imported: number; skipped: number } {
61
+ const raw = readFileSync(inputPath, "utf-8");
62
+ const data = JSON.parse(raw) as ExportData;
63
+
64
+ if (data.version !== 1) {
65
+ throw new Error(`Unsupported export version: ${data.version}`);
66
+ }
67
+
68
+ let imported = 0;
69
+ let skipped = 0;
70
+
71
+ for (const [table, rows] of Object.entries(data.tables)) {
72
+ if (!EXPORT_TABLES.includes(table)) continue;
73
+ if (!/^[a-z_]+$/.test(table)) { skipped += (rows as any[]).length; continue; }
74
+
75
+ // Verify table exists
76
+ const tableCheck = db
77
+ .query("SELECT name FROM sqlite_master WHERE type='table' AND name=?")
78
+ .get(table) as any;
79
+ if (!tableCheck) continue;
80
+
81
+ // Validate column names against actual table schema to prevent SQL injection
82
+ const tableInfo = db.query(`PRAGMA table_info(${quoteId(table)})`).all() as { name: string }[];
83
+ const validColumns = new Set(tableInfo.map((c) => c.name));
84
+
85
+ for (const row of rows) {
86
+ const columns = Object.keys(row).filter((c) => validColumns.has(c));
87
+ if (columns.length === 0) { skipped++; continue; }
88
+ const placeholders = columns.map(() => "?").join(", ");
89
+ const values = columns.map((c) => row[c]);
90
+ try {
91
+ db.prepare(
92
+ `INSERT OR IGNORE INTO ${quoteId(table)} (${columns.map(quoteId).join(", ")}) VALUES (${placeholders})`
93
+ ).run(...values);
94
+ imported++;
95
+ } catch {
96
+ skipped++;
97
+ }
98
+ }
99
+ }
100
+
101
+ return { imported, skipped };
102
+ }
103
+
104
+ export function backupDatabase(dbPath: string, backupDir: string): string {
105
+ mkdirSync(backupDir, { recursive: true });
106
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
107
+ const backupPath = join(backupDir, `zubo-backup-${timestamp}.db`);
108
+
109
+ // Validate backup path doesn't escape the target directory
110
+ const resolvedBackup = require("path").resolve(backupPath);
111
+ const resolvedDir = require("path").resolve(backupDir);
112
+ if (!resolvedBackup.startsWith(resolvedDir + "/") && resolvedBackup !== resolvedDir) {
113
+ throw new Error("Invalid backup path");
114
+ }
115
+
116
+ // Use SQLite VACUUM INTO for atomic backup — path is validated above
117
+ const db = new Database(dbPath, { readonly: true });
118
+ try {
119
+ db.run(`VACUUM INTO '${backupPath.replace(/'/g, "''")}'`);
120
+ } finally {
121
+ db.close();
122
+ }
123
+
124
+ return backupPath;
125
+ }
126
+
127
+ export function getDbStats(db: Database): { tables: Record<string, number>; sizeBytes?: number } {
128
+ const stats: Record<string, number> = {};
129
+
130
+ for (const table of EXPORT_TABLES) {
131
+ try {
132
+ const row = db.query(`SELECT COUNT(*) as c FROM ${quoteId(table)}`).get() as any;
133
+ stats[table] = row?.c ?? 0;
134
+ } catch {
135
+ // Table doesn't exist
136
+ }
137
+ }
138
+
139
+ return { tables: stats };
140
+ }
141
+
142
+ export function getDbSizeBytes(dbPath: string): number {
143
+ try {
144
+ return statSync(dbPath).size;
145
+ } catch {
146
+ return 0;
147
+ }
148
+ }
@@ -0,0 +1,42 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { readdirSync, readFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { logger } from "../util/logger";
5
+
6
+ const MIGRATIONS_DIR = join(import.meta.dir, "../../migrations");
7
+
8
+ export function runMigrations(db: Database) {
9
+ // Create migrations tracking table
10
+ db.run(`
11
+ CREATE TABLE IF NOT EXISTS _migrations (
12
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13
+ name TEXT NOT NULL UNIQUE,
14
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
15
+ )
16
+ `);
17
+
18
+ // Get already applied migrations
19
+ const applied = new Set(
20
+ db
21
+ .query("SELECT name FROM _migrations")
22
+ .all()
23
+ .map((row: any) => row.name)
24
+ );
25
+
26
+ // Read and sort migration files
27
+ const files = readdirSync(MIGRATIONS_DIR)
28
+ .filter((f) => f.endsWith(".sql"))
29
+ .sort();
30
+
31
+ for (const file of files) {
32
+ if (applied.has(file)) continue;
33
+
34
+ const sql = readFileSync(join(MIGRATIONS_DIR, file), "utf-8");
35
+ logger.info(`Running migration: ${file}`);
36
+
37
+ db.transaction(() => {
38
+ db.run(sql);
39
+ db.prepare("INSERT INTO _migrations (name) VALUES (?)").run(file);
40
+ })();
41
+ }
42
+ }