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,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
|
+
}
|
package/src/db/export.ts
ADDED
|
@@ -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
|
+
}
|