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
package/src/index.ts ADDED
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env bun
2
+ export {};
3
+
4
+ const command = process.argv[2];
5
+
6
+ switch (command) {
7
+ case "setup": {
8
+ const { runSetup } = await import("./setup");
9
+ await runSetup();
10
+ break;
11
+ }
12
+ case "start": {
13
+ const isDaemon = process.argv.includes("--daemon");
14
+ const { startZubo } = await import("./start");
15
+ await startZubo(isDaemon);
16
+ break;
17
+ }
18
+ case "stop": {
19
+ const { stopDaemon } = await import("./start");
20
+ stopDaemon();
21
+ break;
22
+ }
23
+ case "status": {
24
+ const { showStatus } = await import("./status");
25
+ showStatus();
26
+ break;
27
+ }
28
+ case "logs": {
29
+ const follow = process.argv.includes("--follow") || process.argv.includes("-f");
30
+ const { showLogs } = await import("./logs");
31
+ await showLogs(follow);
32
+ break;
33
+ }
34
+ case "model": {
35
+ const { runModelCommand } = await import("./model");
36
+ runModelCommand(process.argv.slice(3));
37
+ break;
38
+ }
39
+ case "skills": {
40
+ const { runSkillsCommand } = await import("./skills");
41
+ await runSkillsCommand(process.argv.slice(3));
42
+ break;
43
+ }
44
+ case "install": {
45
+ const skillName = process.argv[3];
46
+ if (!skillName) {
47
+ console.log("Usage: zubo install <skill-name>");
48
+ process.exit(1);
49
+ }
50
+ const { handleRegistryInstall } = await import("./registry/cli");
51
+ await handleRegistryInstall(skillName);
52
+ break;
53
+ }
54
+ case "search": {
55
+ const query = process.argv.slice(3).join(" ");
56
+ if (!query) {
57
+ console.log("Usage: zubo search <query>");
58
+ process.exit(1);
59
+ }
60
+ const { handleRegistrySearch } = await import("./registry/cli");
61
+ await handleRegistrySearch(query);
62
+ break;
63
+ }
64
+ case "publish": {
65
+ const pubName = process.argv[3] ?? "";
66
+ const { handleRegistryPublish } = await import("./registry/cli");
67
+ await handleRegistryPublish(pubName);
68
+ break;
69
+ }
70
+ case "auth": {
71
+ const authAction = process.argv[3];
72
+ const { getDb } = await import("./db/connection");
73
+ const { runMigrations } = await import("./db/migrations");
74
+ const { initAuth, createApiKey, listApiKeys } = await import("./util/auth");
75
+ const authDb = getDb();
76
+ runMigrations(authDb);
77
+ initAuth(authDb);
78
+
79
+ if (authAction === "create-key") {
80
+ const label = process.argv[4] ?? "default";
81
+ const result = createApiKey(authDb, label);
82
+ console.log(`API key created (id: ${result.id}, label: ${label}):`);
83
+ console.log(` ${result.key}`);
84
+ console.log("\nStore this key securely — it cannot be retrieved again.");
85
+ } else if (authAction === "list-keys") {
86
+ const keys = listApiKeys(authDb);
87
+ if (keys.length === 0) {
88
+ console.log("No API keys found. Create one with: zubo auth create-key [label]");
89
+ } else {
90
+ console.log("API Keys:");
91
+ for (const k of keys) {
92
+ console.log(` #${k.id} ${k.label || "(no label)"} created: ${k.created_at} last used: ${k.last_used_at ?? "never"}`);
93
+ }
94
+ }
95
+ } else if (authAction === "delete-key") {
96
+ const { deleteApiKey } = await import("./util/auth");
97
+ const keyId = parseInt(process.argv[4] ?? "", 10);
98
+ if (isNaN(keyId)) {
99
+ console.log("Usage: zubo auth delete-key <id>");
100
+ process.exit(1);
101
+ }
102
+ const deleted = deleteApiKey(authDb, keyId);
103
+ console.log(deleted ? `API key #${keyId} deleted.` : `API key #${keyId} not found.`);
104
+ } else {
105
+ console.log("Usage: zubo auth <command>\n");
106
+ console.log("Commands:");
107
+ console.log(" create-key [label] Create a new API key");
108
+ console.log(" list-keys List all API keys");
109
+ console.log(" delete-key <id> Delete an API key");
110
+ }
111
+ break;
112
+ }
113
+ case "config": {
114
+ const configAction = process.argv[3];
115
+ if (configAction === "set") {
116
+ const key = process.argv[4];
117
+ const value = process.argv[5];
118
+ if (!key || value === undefined) {
119
+ console.log("Usage: zubo config set <key> <value>");
120
+ console.log("\nExamples:");
121
+ console.log(" zubo config set heartbeatMinutes 60");
122
+ console.log(" zubo config set maxTurns 100");
123
+ console.log(" zubo config set activeProvider ollama");
124
+ console.log(" zubo config set model llama3.3");
125
+ console.log(" zubo config set auth.enabled true");
126
+ console.log(" zubo config set rateLimit.chatPerMinute 30");
127
+ process.exit(1);
128
+ }
129
+ const { readFileSync, writeFileSync } = await import("fs");
130
+ const { paths: cfgPaths } = await import("./config/paths");
131
+ try {
132
+ const config = JSON.parse(readFileSync(cfgPaths.config, "utf-8"));
133
+ // Support dotted keys (e.g. auth.enabled, rateLimit.chatPerMinute)
134
+ const keys = key.split(".");
135
+ let target = config;
136
+ for (let i = 0; i < keys.length - 1; i++) {
137
+ if (!target[keys[i]] || typeof target[keys[i]] !== "object") {
138
+ target[keys[i]] = {};
139
+ }
140
+ target = target[keys[i]];
141
+ }
142
+ const finalKey = keys[keys.length - 1];
143
+ // Parse value types
144
+ let parsed: any = value;
145
+ if (value === "true") parsed = true;
146
+ else if (value === "false") parsed = false;
147
+ else if (/^\d+$/.test(value)) parsed = parseInt(value, 10);
148
+ else if (/^\d+\.\d+$/.test(value)) parsed = parseFloat(value);
149
+
150
+ target[finalKey] = parsed;
151
+ writeFileSync(cfgPaths.config, JSON.stringify(config, null, 2) + "\n");
152
+ console.log(`Set ${key} = ${JSON.stringify(parsed)}`);
153
+ } catch (err: any) {
154
+ console.error(`Error: ${err.message}`);
155
+ process.exit(1);
156
+ }
157
+ } else if (configAction === "get") {
158
+ const key = process.argv[4];
159
+ const { readFileSync } = await import("fs");
160
+ const { paths: cfgPaths } = await import("./config/paths");
161
+ try {
162
+ const config = JSON.parse(readFileSync(cfgPaths.config, "utf-8"));
163
+ if (!key) {
164
+ // Print full config (mask sensitive values)
165
+ const masked = { ...config };
166
+ if (masked.anthropicApiKey) masked.anthropicApiKey = "***";
167
+ if (masked.providers) {
168
+ for (const p of Object.values(masked.providers) as any[]) {
169
+ if (p.apiKey) p.apiKey = "***";
170
+ }
171
+ }
172
+ console.log(JSON.stringify(masked, null, 2));
173
+ } else {
174
+ const keys = key.split(".");
175
+ let val: any = config;
176
+ for (const k of keys) val = val?.[k];
177
+ console.log(val !== undefined ? JSON.stringify(val) : "(not set)");
178
+ }
179
+ } catch (err: any) {
180
+ console.error(`Error: ${err.message}`);
181
+ process.exit(1);
182
+ }
183
+ } else {
184
+ console.log("Usage: zubo config <command>\n");
185
+ console.log("Commands:");
186
+ console.log(" set <key> <value> Set a config value");
187
+ console.log(" get [key] Get a config value (or all if no key)");
188
+ }
189
+ break;
190
+ }
191
+ case "export": {
192
+ const { getDb } = await import("./db/connection");
193
+ const { runMigrations } = await import("./db/migrations");
194
+ const { exportDatabase, backupDatabase, getDbStats, getDbSizeBytes } = await import("./db/export");
195
+ const { paths: exportPaths } = await import("./config/paths");
196
+ const exportDb = getDb();
197
+ runMigrations(exportDb);
198
+
199
+ const format = process.argv.includes("--format") ? process.argv[process.argv.indexOf("--format") + 1] : "json";
200
+ const outputIdx = process.argv.indexOf("--output");
201
+ const output = outputIdx !== -1 ? process.argv[outputIdx + 1] : undefined;
202
+
203
+ if (format === "sqlite") {
204
+ const backupDir = output ?? process.cwd();
205
+ const backupPath = backupDatabase(exportPaths.db, backupDir);
206
+ console.log(`SQLite backup created: ${backupPath}`);
207
+ } else {
208
+ const outputPath = output ?? `zubo-export-${new Date().toISOString().replace(/[:.]/g, "-")}.json`;
209
+ exportDatabase(exportDb, outputPath);
210
+ const stats = getDbStats(exportDb);
211
+ const totalRows = Object.values(stats.tables).reduce((a, b) => a + b, 0);
212
+ console.log(`Exported ${totalRows} rows to ${outputPath}`);
213
+ }
214
+ break;
215
+ }
216
+ case "import": {
217
+ const importPath = process.argv[3];
218
+ if (!importPath) {
219
+ console.log("Usage: zubo import <path>");
220
+ process.exit(1);
221
+ }
222
+ const { getDb } = await import("./db/connection");
223
+ const { runMigrations } = await import("./db/migrations");
224
+ const { importDatabase } = await import("./db/export");
225
+ const importDb = getDb();
226
+ runMigrations(importDb);
227
+ const result = importDatabase(importDb, importPath);
228
+ console.log(`Imported ${result.imported} rows (${result.skipped} skipped)`);
229
+ break;
230
+ }
231
+ default:
232
+ console.log("Usage: zubo <command>\n");
233
+ console.log("Commands:");
234
+ console.log(" setup Configure Zubo (API keys, Telegram token)");
235
+ console.log(" start Start the Zubo agent");
236
+ console.log(" start --daemon Start in background");
237
+ console.log(" stop Stop the background daemon");
238
+ console.log(" status Show config and runtime status");
239
+ console.log(" logs Show last 50 log lines");
240
+ console.log(" logs --follow Stream logs live");
241
+ console.log(" model Show active LLM provider/model");
242
+ console.log(" model <p/m> Switch provider/model (e.g. ollama/llama3.3)");
243
+ console.log(" model --list List all configured providers");
244
+ console.log(" skills Manage skills (interactive menu)");
245
+ console.log(" skills list List installed skills");
246
+ console.log(" skills new Create a new skill");
247
+ console.log(" skills reinstall Reinstall built-in skills");
248
+ console.log(" skills remove Remove a skill");
249
+ console.log(" install <skill> Install a skill from the registry");
250
+ console.log(" search <query> Search the skill registry");
251
+ console.log(" publish How to publish a skill");
252
+ console.log(" auth create-key Create an API key");
253
+ console.log(" auth list-keys List all API keys");
254
+ console.log(" auth delete-key Delete an API key");
255
+ console.log(" config set <k> <v> Set a config value");
256
+ console.log(" config get [key] Show config value(s)");
257
+ console.log(" export Export database as JSON");
258
+ console.log(" export --format sqlite Backup as SQLite file");
259
+ console.log(" import <path> Import data from JSON export");
260
+ process.exit(1);
261
+ }
@@ -0,0 +1,193 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import type {
3
+ LlmProvider,
4
+ LlmRequest,
5
+ LlmResponse,
6
+ LlmContentBlock,
7
+ LlmStreamEvent,
8
+ } from "./provider";
9
+ import { logger } from "../util/logger";
10
+
11
+ export class ClaudeProvider implements LlmProvider {
12
+ providerName = "anthropic";
13
+ model: string;
14
+ contextWindow = 200_000;
15
+ private client: Anthropic;
16
+
17
+ constructor(apiKey: string, model: string) {
18
+ this.client = new Anthropic({
19
+ apiKey,
20
+ defaultHeaders: { "anthropic-beta": "prompt-caching-2024-07-31" },
21
+ });
22
+ this.model = model;
23
+ }
24
+
25
+ async chat(request: LlmRequest): Promise<LlmResponse> {
26
+ logger.debug("Claude request", {
27
+ messageCount: request.messages.length,
28
+ toolCount: request.tools?.length ?? 0,
29
+ });
30
+
31
+ const tools = request.tools?.length
32
+ ? request.tools.map((t, i, arr) =>
33
+ i === arr.length - 1
34
+ ? { ...t, cache_control: { type: "ephemeral" as const } }
35
+ : t,
36
+ )
37
+ : undefined;
38
+
39
+ const response = await this.client.messages.create({
40
+ model: this.model,
41
+ max_tokens: request.maxTokens ?? 4096,
42
+ system: [
43
+ {
44
+ type: "text" as const,
45
+ text: request.system,
46
+ cache_control: { type: "ephemeral" as const },
47
+ },
48
+ ],
49
+ messages: request.messages as any,
50
+ tools: tools as any,
51
+ });
52
+
53
+ const content: LlmContentBlock[] = response.content.map((block: any) => {
54
+ if (block.type === "text") {
55
+ return { type: "text" as const, text: block.text };
56
+ }
57
+ if (block.type === "tool_use") {
58
+ return {
59
+ type: "tool_use" as const,
60
+ id: block.id,
61
+ name: block.name,
62
+ input: block.input,
63
+ };
64
+ }
65
+ return block;
66
+ });
67
+
68
+ const apiUsage = response.usage as any;
69
+
70
+ return {
71
+ content,
72
+ stopReason: response.stop_reason ?? "end_turn",
73
+ usage: {
74
+ inputTokens: response.usage.input_tokens,
75
+ outputTokens: response.usage.output_tokens,
76
+ cacheCreationTokens: apiUsage.cache_creation_input_tokens ?? undefined,
77
+ cacheReadTokens: apiUsage.cache_read_input_tokens ?? undefined,
78
+ },
79
+ };
80
+ }
81
+
82
+ async *chatStream(request: LlmRequest): AsyncIterable<LlmStreamEvent> {
83
+ logger.debug("Claude stream request", {
84
+ messageCount: request.messages.length,
85
+ toolCount: request.tools?.length ?? 0,
86
+ });
87
+
88
+ const tools = request.tools?.length
89
+ ? request.tools.map((t, i, arr) =>
90
+ i === arr.length - 1
91
+ ? { ...t, cache_control: { type: "ephemeral" as const } }
92
+ : t,
93
+ )
94
+ : undefined;
95
+
96
+ const params: any = {
97
+ model: this.model,
98
+ max_tokens: request.maxTokens ?? 4096,
99
+ system: [
100
+ {
101
+ type: "text" as const,
102
+ text: request.system,
103
+ cache_control: { type: "ephemeral" as const },
104
+ },
105
+ ],
106
+ messages: request.messages as any,
107
+ };
108
+ if (tools) {
109
+ params.tools = tools as any;
110
+ }
111
+
112
+ const stream = this.client.messages.stream(params);
113
+
114
+ // Collect full response for the final message_done event
115
+ const contentBlocks: LlmContentBlock[] = [];
116
+ let currentToolId = "";
117
+ let currentToolName = "";
118
+ let currentToolJson = "";
119
+ let inputTokens = 0;
120
+ let outputTokens = 0;
121
+ let stopReason = "end_turn";
122
+
123
+ for await (const event of stream) {
124
+ if (event.type === "message_start") {
125
+ const msg = (event as any).message;
126
+ if (msg?.usage) {
127
+ inputTokens = msg.usage.input_tokens ?? 0;
128
+ }
129
+ } else if (event.type === "message_delta") {
130
+ const delta = (event as any).delta;
131
+ if (delta?.stop_reason) stopReason = delta.stop_reason;
132
+ const usage = (event as any).usage;
133
+ if (usage?.output_tokens) outputTokens = usage.output_tokens;
134
+ } else if (event.type === "content_block_start") {
135
+ const block = (event as any).content_block;
136
+ if (block?.type === "tool_use") {
137
+ currentToolId = block.id;
138
+ currentToolName = block.name;
139
+ currentToolJson = "";
140
+ yield { type: "tool_use_start", id: block.id, name: block.name };
141
+ }
142
+ } else if (event.type === "content_block_delta") {
143
+ const delta = (event as any).delta;
144
+ if (delta?.type === "text_delta" && delta.text) {
145
+ yield { type: "text_delta", text: delta.text };
146
+ } else if (delta?.type === "input_json_delta" && delta.partial_json) {
147
+ currentToolJson += delta.partial_json;
148
+ yield { type: "tool_use_delta", id: currentToolId, json: delta.partial_json };
149
+ }
150
+ } else if (event.type === "content_block_stop") {
151
+ if (currentToolId) {
152
+ let input: Record<string, unknown> = {};
153
+ try { input = JSON.parse(currentToolJson); } catch (err: any) { logger.warn("Failed to parse tool input JSON", { error: (err as Error).message }); }
154
+ contentBlocks.push({
155
+ type: "tool_use",
156
+ id: currentToolId,
157
+ name: currentToolName,
158
+ input,
159
+ });
160
+ yield { type: "tool_use_end", id: currentToolId };
161
+ currentToolId = "";
162
+ currentToolName = "";
163
+ currentToolJson = "";
164
+ }
165
+ }
166
+ }
167
+
168
+ // Collect final text from stream
169
+ const finalMessage = await stream.finalMessage();
170
+ // Build content blocks from final message for accuracy
171
+ const finalContent: LlmContentBlock[] = finalMessage.content.map((block: any) => {
172
+ if (block.type === "text") return { type: "text" as const, text: block.text };
173
+ if (block.type === "tool_use") return { type: "tool_use" as const, id: block.id, name: block.name, input: block.input };
174
+ return block;
175
+ });
176
+
177
+ const finalUsage = finalMessage.usage as any;
178
+
179
+ yield {
180
+ type: "message_done",
181
+ response: {
182
+ content: finalContent,
183
+ stopReason: finalMessage.stop_reason ?? "end_turn",
184
+ usage: {
185
+ inputTokens: finalMessage.usage.input_tokens,
186
+ outputTokens: finalMessage.usage.output_tokens,
187
+ cacheCreationTokens: finalUsage.cache_creation_input_tokens ?? undefined,
188
+ cacheReadTokens: finalUsage.cache_read_input_tokens ?? undefined,
189
+ },
190
+ },
191
+ };
192
+ }
193
+ }
@@ -0,0 +1,115 @@
1
+ import type { ZuboConfig, ProviderConfig } from "../config/schema";
2
+ import type { LlmProvider } from "./provider";
3
+ import { ClaudeProvider } from "./claude";
4
+ import { OpenAICompatProvider } from "./openai-compat";
5
+ import { FailoverProvider } from "./failover";
6
+ import { SmartRouterProvider } from "./smart-router";
7
+ import { logger } from "../util/logger";
8
+
9
+ const KNOWN_BASE_URLS: Record<string, string> = {
10
+ openai: "https://api.openai.com/v1",
11
+ groq: "https://api.groq.com/openai/v1",
12
+ together: "https://api.together.xyz/v1",
13
+ openrouter: "https://openrouter.ai/api/v1",
14
+ deepseek: "https://api.deepseek.com/v1",
15
+ fireworks: "https://api.fireworks.ai/inference/v1",
16
+ cerebras: "https://api.cerebras.ai/v1",
17
+ perplexity: "https://api.perplexity.ai",
18
+ xai: "https://api.x.ai/v1",
19
+ ollama: "http://localhost:11434/v1",
20
+ lmstudio: "http://localhost:1234/v1",
21
+ };
22
+
23
+ function buildSingleProvider(
24
+ name: string,
25
+ providerCfg: ProviderConfig
26
+ ): LlmProvider {
27
+ const baseUrl = providerCfg.baseUrl ?? KNOWN_BASE_URLS[name];
28
+
29
+ if (name === "anthropic") {
30
+ if (!providerCfg.apiKey) {
31
+ throw new Error("Anthropic provider requires an apiKey");
32
+ }
33
+ const provider = new ClaudeProvider(providerCfg.apiKey, providerCfg.model);
34
+ if (providerCfg.contextWindow) provider.contextWindow = providerCfg.contextWindow;
35
+ return provider;
36
+ }
37
+
38
+ // Everything else goes through OpenAI-compatible
39
+ if (!baseUrl) {
40
+ throw new Error(
41
+ `Provider "${name}" requires a baseUrl. Known providers: ${Object.keys(KNOWN_BASE_URLS).join(", ")}`
42
+ );
43
+ }
44
+
45
+ return new OpenAICompatProvider({
46
+ name,
47
+ baseUrl,
48
+ apiKey: providerCfg.apiKey ?? "no-key",
49
+ model: providerCfg.model,
50
+ streaming: providerCfg.streaming,
51
+ contextWindow: providerCfg.contextWindow,
52
+ });
53
+ }
54
+
55
+ export function createProvider(config: ZuboConfig): LlmProvider {
56
+ // New multi-provider config
57
+ if (config.providers && config.activeProvider) {
58
+ const activeCfg = config.providers[config.activeProvider];
59
+ if (!activeCfg) {
60
+ throw new Error(
61
+ `Active provider "${config.activeProvider}" not found in providers config`
62
+ );
63
+ }
64
+
65
+ const primary = buildSingleProvider(config.activeProvider, activeCfg);
66
+ logger.info(`LLM provider: ${primary.providerName}/${primary.model}`);
67
+
68
+ // Build failover chain
69
+ let provider: LlmProvider = primary;
70
+ if (config.failover?.length) {
71
+ const fallbacks: LlmProvider[] = [];
72
+ for (const fbName of config.failover) {
73
+ const fbCfg = config.providers[fbName];
74
+ if (fbCfg) {
75
+ fallbacks.push(buildSingleProvider(fbName, fbCfg));
76
+ logger.info(` failover: ${fbName}/${fbCfg.model}`);
77
+ } else {
78
+ logger.warn(`Failover provider "${fbName}" not configured, skipping`);
79
+ }
80
+ }
81
+ if (fallbacks.length) {
82
+ provider = new FailoverProvider(primary, fallbacks);
83
+ }
84
+ }
85
+
86
+ // Wrap in smart router if enabled
87
+ if (config.smartRouting?.enabled) {
88
+ const fastProviderName = config.smartRouting.fastProvider;
89
+ if (fastProviderName && config.providers[fastProviderName]) {
90
+ const fastCfg = { ...config.providers[fastProviderName] };
91
+ if (config.smartRouting.fastModel) {
92
+ fastCfg.model = config.smartRouting.fastModel;
93
+ }
94
+ const fast = buildSingleProvider(fastProviderName, fastCfg);
95
+ logger.info(`Smart routing enabled: fast=${fast.providerName}/${fast.model}`);
96
+ provider = new SmartRouterProvider(provider, fast, true);
97
+ } else {
98
+ logger.warn("Smart routing enabled but fastProvider not configured, skipping");
99
+ }
100
+ }
101
+
102
+ return provider;
103
+ }
104
+
105
+ // Legacy config: anthropicApiKey + model at top level
106
+ if (config.anthropicApiKey) {
107
+ const model = config.model ?? "claude-sonnet-4-5-20250929";
108
+ logger.info(`LLM provider: anthropic/${model} (legacy config)`);
109
+ return new ClaudeProvider(config.anthropicApiKey, model);
110
+ }
111
+
112
+ throw new Error(
113
+ "No LLM provider configured. Run 'zubo setup' or add a providers section to config.json"
114
+ );
115
+ }
@@ -0,0 +1,101 @@
1
+ import type { LlmProvider, LlmRequest, LlmResponse, LlmStreamEvent } from "./provider";
2
+ import { logger } from "../util/logger";
3
+
4
+ export class FailoverProvider implements LlmProvider {
5
+ providerName: string;
6
+ model: string;
7
+ contextWindow: number;
8
+
9
+ constructor(
10
+ private primary: LlmProvider,
11
+ private fallbacks: LlmProvider[]
12
+ ) {
13
+ this.providerName = primary.providerName;
14
+ this.model = primary.model;
15
+ this.contextWindow = primary.contextWindow;
16
+ }
17
+
18
+ async chat(request: LlmRequest): Promise<LlmResponse> {
19
+ try {
20
+ return await this.primary.chat(request);
21
+ } catch (err: any) {
22
+ logger.warn(`Primary provider (${this.primary.providerName}) failed`, {
23
+ error: err.message,
24
+ });
25
+
26
+ for (const fb of this.fallbacks) {
27
+ try {
28
+ logger.info(`Trying fallback: ${fb.providerName}/${fb.model}`);
29
+ const result = await fb.chat(request);
30
+ this.providerName = fb.providerName;
31
+ this.model = fb.model;
32
+ return result;
33
+ } catch (fbErr: any) {
34
+ logger.warn(`Fallback ${fb.providerName} also failed`, {
35
+ error: fbErr.message,
36
+ });
37
+ }
38
+ }
39
+
40
+ throw new Error(
41
+ `All providers failed. Primary: ${err.message}`
42
+ );
43
+ }
44
+ }
45
+
46
+ async *chatStream(request: LlmRequest): AsyncIterable<LlmStreamEvent> {
47
+ const MAX_STREAM_EVENTS = 50_000;
48
+
49
+ // Collect all events from a stream — if it succeeds fully, yield them.
50
+ // This prevents partial output from a failing stream from corrupting state.
51
+ async function collectStream(provider: LlmProvider): Promise<LlmStreamEvent[] | null> {
52
+ if (!provider.chatStream) return null;
53
+ const events: LlmStreamEvent[] = [];
54
+ try {
55
+ for await (const event of provider.chatStream(request)) {
56
+ if (events.length >= MAX_STREAM_EVENTS) {
57
+ throw new Error(`Stream exceeded maximum event limit (${MAX_STREAM_EVENTS})`);
58
+ }
59
+ events.push(event);
60
+ }
61
+ return events;
62
+ } catch (err: any) {
63
+ logger.warn(`Stream from ${provider.providerName} failed after ${events.length} events`, {
64
+ error: err.message,
65
+ });
66
+ return null;
67
+ }
68
+ }
69
+
70
+ // Try primary
71
+ const primaryEvents = await collectStream(this.primary);
72
+ if (primaryEvents) {
73
+ for (const event of primaryEvents) yield event;
74
+ return;
75
+ }
76
+
77
+ // Try fallbacks
78
+ for (const fb of this.fallbacks) {
79
+ const fbEvents = await collectStream(fb);
80
+ if (fbEvents) {
81
+ this.providerName = fb.providerName;
82
+ this.model = fb.model;
83
+ for (const event of fbEvents) yield event;
84
+ return;
85
+ }
86
+ }
87
+
88
+ // If no provider supports streaming, fall back to non-streaming
89
+ logger.info("No streaming providers available, falling back to non-streaming");
90
+ const response = await this.chat(request);
91
+ for (const block of response.content) {
92
+ if (block.type === "text" && block.text) {
93
+ yield { type: "text_delta", text: block.text };
94
+ } else if (block.type === "tool_use") {
95
+ yield { type: "tool_use_start", id: block.id!, name: block.name! };
96
+ yield { type: "tool_use_end", id: block.id! };
97
+ }
98
+ }
99
+ yield { type: "message_done", response };
100
+ }
101
+ }