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/start.ts ADDED
@@ -0,0 +1,373 @@
1
+ import { existsSync, readFileSync, unlinkSync, writeFileSync, mkdirSync } from "fs";
2
+ import { join } from "path";
3
+ import { loadConfig } from "./config/loader";
4
+ import { ensureDirectories, paths } from "./config/paths";
5
+ import type { ZuboConfig } from "./config/schema";
6
+ import { getDb, closeDb } from "./db/connection";
7
+ import { runMigrations } from "./db/migrations";
8
+ import { createProvider } from "./llm/factory";
9
+ import { registerDatetimeTool } from "./tools/builtin/datetime";
10
+ import { registerMemoryWriteTool } from "./tools/builtin/memory-write";
11
+ import { registerMemorySearchTool } from "./tools/builtin/memory-search";
12
+ import { registerManageSkillsTool } from "./tools/builtin/manage-skills";
13
+ import { registerCronTools } from "./tools/builtin/cron";
14
+ import { registerSecretTools } from "./tools/builtin/secrets";
15
+ import { registerConnectServiceTool } from "./tools/builtin/connect-service";
16
+ import { registerDelegateTool } from "./tools/builtin/delegate";
17
+ import { registerDelegateTaskTool } from "./tools/builtin/delegate-task";
18
+ import { registerDiagnoseTool } from "./tools/builtin/diagnose";
19
+ import { registerGoogleOAuthTool } from "./tools/builtin/google-oauth";
20
+ import { registerManageAgentsTool } from "./tools/builtin/manage-agents";
21
+ import { exposeSecretsRuntime, exposeGoogleTokenRuntime } from "./secrets/store";
22
+ import { loadSkills, watchSkills } from "./tools/skill-loader";
23
+ import { createRouter, type MessageRouter } from "./channels/router";
24
+ import { startHeartbeat } from "./scheduler/heartbeat";
25
+ import { initCronScheduler } from "./scheduler/cron";
26
+ import { initMemory } from "./memory/engine";
27
+ import { logger, enableFileLogging } from "./util/logger";
28
+
29
+ function openBrowser(url: string) {
30
+ try {
31
+ const cmd =
32
+ process.platform === "darwin"
33
+ ? ["open", url]
34
+ : process.platform === "win32"
35
+ ? ["cmd", "/c", "start", url]
36
+ : ["xdg-open", url];
37
+ Bun.spawn(cmd, { stdio: ["ignore", "ignore", "ignore"] });
38
+ } catch (err: any) {
39
+ logger.warn("Failed to open browser", { error: (err as Error).message });
40
+ }
41
+ }
42
+
43
+ function getTelegramToken(config: ZuboConfig): string | null {
44
+ if (config.channels?.telegram?.enabled !== false && config.channels?.telegram?.botToken) {
45
+ return config.channels.telegram.botToken;
46
+ }
47
+ // Legacy fallback
48
+ if (config.telegramBotToken) {
49
+ return config.telegramBotToken;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function getTelegramAllowedUsers(config: ZuboConfig): number[] {
55
+ if (config.channels?.telegram?.allowedUsers?.length) {
56
+ return config.channels.telegram.allowedUsers;
57
+ }
58
+ return config.telegramAllowedUsers ?? [];
59
+ }
60
+
61
+ async function startChannels(config: ZuboConfig, router: MessageRouter) {
62
+ const stoppers: (() => void)[] = [];
63
+
64
+ // Telegram
65
+ const tgToken = getTelegramToken(config);
66
+ if (tgToken) {
67
+ const { createTelegramAdapter } = await import("./channels/telegram");
68
+ // Build a compat config object for the telegram adapter
69
+ const tgConfig = {
70
+ ...config,
71
+ telegramBotToken: tgToken,
72
+ telegramAllowedUsers: getTelegramAllowedUsers(config),
73
+ };
74
+ const telegram = createTelegramAdapter(tgToken, tgConfig, router);
75
+ router.addAdapter(telegram);
76
+ telegram.start();
77
+ stoppers.push(() => telegram.stop());
78
+ logger.info("Telegram channel started");
79
+ }
80
+
81
+ // Discord
82
+ if (config.channels?.discord?.enabled !== false && config.channels?.discord?.botToken) {
83
+ const { createDiscordAdapter } = await import("./channels/discord");
84
+ const discord = createDiscordAdapter(
85
+ config.channels.discord.botToken,
86
+ config.channels.discord.allowedUsers ?? [],
87
+ router
88
+ );
89
+ router.addAdapter(discord);
90
+ discord.start();
91
+ stoppers.push(() => discord.stop());
92
+ logger.info("Discord channel started");
93
+ }
94
+
95
+ // Slack
96
+ if (config.channels?.slack?.enabled !== false && config.channels?.slack?.botToken) {
97
+ const { createSlackAdapter } = await import("./channels/slack");
98
+ const slack = createSlackAdapter(
99
+ config.channels.slack.botToken,
100
+ config.channels.slack.appToken,
101
+ config.channels.slack.allowedUsers ?? [],
102
+ router
103
+ );
104
+ router.addAdapter(slack);
105
+ slack.start();
106
+ stoppers.push(() => slack.stop());
107
+ logger.info("Slack channel started");
108
+ }
109
+
110
+ // WhatsApp
111
+ if (config.channels?.whatsapp?.enabled !== false && config.channels?.whatsapp) {
112
+ const { createWhatsAppAdapter } = await import("./channels/whatsapp");
113
+ const whatsapp = createWhatsAppAdapter(
114
+ config.channels.whatsapp.allowedNumbers ?? [],
115
+ config.channels.whatsapp.authDir,
116
+ router
117
+ );
118
+ router.addAdapter(whatsapp);
119
+ whatsapp.start();
120
+ stoppers.push(() => whatsapp.stop());
121
+ logger.info("WhatsApp channel started");
122
+ }
123
+
124
+ // Signal
125
+ if (config.channels?.signal?.enabled !== false && config.channels?.signal?.phoneNumber) {
126
+ const { createSignalAdapter } = await import("./channels/signal");
127
+ const signal = createSignalAdapter(
128
+ config.channels.signal.phoneNumber,
129
+ config.channels.signal.allowedNumbers ?? [],
130
+ config.channels.signal.signalCliPath,
131
+ router
132
+ );
133
+ router.addAdapter(signal);
134
+ signal.start();
135
+ stoppers.push(() => signal.stop());
136
+ logger.info("Signal channel started");
137
+ }
138
+
139
+ // WebChat + Dashboard (always enabled)
140
+ if (config.channels?.webchat?.enabled !== false) {
141
+ const requestedPort = config.channels?.webchat?.port ?? 0;
142
+ const { createWebChatAdapter } = await import("./channels/webchat");
143
+ const webchat = createWebChatAdapter(requestedPort, router);
144
+ router.addAdapter(webchat);
145
+ webchat.start();
146
+ stoppers.push(() => webchat.stop());
147
+
148
+ // Print URLs with the actual resolved port and auto-open browser
149
+ const actualPort = webchat.getPort();
150
+ const url = `http://localhost:${actualPort}`;
151
+ console.log(`\n Chat: ${url}`);
152
+ console.log(` Dashboard: ${url}/dashboard\n`);
153
+ openBrowser(url);
154
+ }
155
+
156
+ return () => {
157
+ for (const stop of stoppers) stop();
158
+ };
159
+ }
160
+
161
+ export async function startZubo(isDaemon = false) {
162
+ if (isDaemon) {
163
+ return startDaemon();
164
+ }
165
+
166
+ enableFileLogging();
167
+ logger.info("Starting Zubo...");
168
+
169
+ // Load config
170
+ const config = await loadConfig();
171
+ ensureDirectories();
172
+
173
+ // Init DB
174
+ const db = getDb();
175
+ runMigrations(db);
176
+
177
+ // Init memory (embedder + index existing files)
178
+ logger.info("Initializing memory system...");
179
+ await initMemory(db);
180
+
181
+ // Init LLM
182
+ const llm = createProvider(config);
183
+
184
+ // Init voice (STT/TTS) if configured
185
+ if (config.voice?.stt) {
186
+ const { initStt } = await import("./voice/stt");
187
+ initStt(config.voice.stt);
188
+ }
189
+ if (config.voice?.tts) {
190
+ const { initTts } = await import("./voice/tts");
191
+ initTts(config.voice.tts);
192
+ }
193
+
194
+ // Register tools
195
+ registerDatetimeTool();
196
+ registerMemoryWriteTool();
197
+ registerMemorySearchTool(db);
198
+ registerManageSkillsTool();
199
+ registerSecretTools();
200
+ exposeSecretsRuntime();
201
+
202
+ // Expose Google OAuth token helper for installed skill handlers
203
+ const { getGoogleAccessToken } = await import("./util/google-tokens");
204
+ exposeGoogleTokenRuntime(getGoogleAccessToken);
205
+
206
+ registerConnectServiceTool();
207
+
208
+ // Register skill registry tool
209
+ const { registerSkillRegistryTool } = await import("./tools/builtin/skill-registry");
210
+ registerSkillRegistryTool();
211
+
212
+ // Load skills
213
+ try {
214
+ const skillNames = await loadSkills(paths.skills);
215
+ if (skillNames.length) {
216
+ logger.info(`Skills loaded: ${skillNames.join(", ")}`);
217
+ } else {
218
+ logger.info("No skills found in workspace");
219
+ }
220
+ } catch (err: any) {
221
+ logger.error(`Failed to load skills: ${err.message}`);
222
+ }
223
+
224
+ // Start skill hot-reload watcher
225
+ const stopSkillWatcher = watchSkills();
226
+
227
+ // Create message router
228
+ const router = createRouter(llm, db);
229
+
230
+ // Register cron tools (need router for scheduling)
231
+ registerCronTools(db, router, config, llm);
232
+
233
+ // Register delegation tools
234
+ registerDelegateTool(llm);
235
+ registerDelegateTaskTool(llm);
236
+ registerManageAgentsTool();
237
+
238
+ // Register diagnostics
239
+ registerDiagnoseTool();
240
+
241
+ // Register Google OAuth tool
242
+ registerGoogleOAuthTool();
243
+
244
+ // Register workflow + team tools
245
+ const { registerManageWorkflowsTool } = await import("./tools/builtin/manage-workflows");
246
+ const { registerRunWorkflowTool } = await import("./tools/builtin/run-workflow");
247
+ const { registerManageTeamsTool } = await import("./tools/builtin/manage-teams");
248
+ registerManageWorkflowsTool();
249
+ registerRunWorkflowTool(llm);
250
+ registerManageTeamsTool();
251
+
252
+ // Register proactive intelligence tools
253
+ const { registerManageTriggersTool } = await import("./tools/builtin/manage-triggers");
254
+ registerManageTriggersTool();
255
+
256
+ // Start all configured channels
257
+ const stopChannels = await startChannels(config, router);
258
+
259
+ // Start scheduler
260
+ startHeartbeat(config.heartbeatMinutes);
261
+ initCronScheduler(db, router, config, llm);
262
+ logger.info("Scheduler started");
263
+
264
+ // Register daily backup handler
265
+ const { onHeartbeat } = await import("./scheduler/heartbeat");
266
+ const { backupDatabase } = await import("./db/export");
267
+ let lastBackupDay = "";
268
+ onHeartbeat(async () => {
269
+ const today = new Date().toISOString().slice(0, 10);
270
+ if (today === lastBackupDay) return;
271
+ try {
272
+ const backupDir = join(paths.workspace, "backups");
273
+ mkdirSync(backupDir, { recursive: true });
274
+ backupDatabase(paths.db, backupDir);
275
+ lastBackupDay = today;
276
+ // Prune old backups — keep last 7
277
+ const { readdirSync: ls, unlinkSync: rm } = await import("fs");
278
+ const files = ls(backupDir).filter((f: string) => f.startsWith("zubo-backup-")).sort();
279
+ while (files.length > 7) {
280
+ rm(join(backupDir, files.shift()!));
281
+ }
282
+ logger.info("Daily backup complete");
283
+ } catch (err: any) {
284
+ logger.warn("Daily backup failed", { error: err.message });
285
+ }
286
+ });
287
+
288
+ // Init proactive intelligence
289
+ const { initProactiveIntelligence } = await import("./scheduler/proactive");
290
+ initProactiveIntelligence(db, router, llm, config);
291
+
292
+ // Ensure morning briefing cron exists
293
+ const { ensureBriefingCron } = await import("./scheduler/briefing");
294
+ ensureBriefingCron(db);
295
+
296
+ // Init performance collector
297
+ const { initPerfCollector } = await import("./util/perf-collector");
298
+ initPerfCollector();
299
+
300
+ logger.info("Zubo is running. Press Ctrl+C to stop.");
301
+
302
+ // Graceful shutdown
303
+ const shutdown = () => {
304
+ logger.info("Shutting down...");
305
+ stopSkillWatcher();
306
+ stopChannels();
307
+ closeDb();
308
+ cleanupPidFile();
309
+ process.exit(0);
310
+ };
311
+ process.on("SIGINT", shutdown);
312
+ process.on("SIGTERM", shutdown);
313
+ }
314
+
315
+ function cleanupPidFile() {
316
+ try {
317
+ if (existsSync(paths.pidFile)) unlinkSync(paths.pidFile);
318
+ } catch (err: any) {
319
+ logger.warn("Failed to clean up PID file", { error: (err as Error).message });
320
+ }
321
+ }
322
+
323
+ function startDaemon() {
324
+ ensureDirectories();
325
+
326
+ // Spawn detached child process
327
+ const child = Bun.spawn(["bun", "run", "src/index.ts", "start"], {
328
+ cwd: import.meta.dir.replace(/\/src$/, ""),
329
+ stdio: ["ignore", "ignore", "ignore"],
330
+ env: { ...process.env },
331
+ });
332
+
333
+ // Write child PID
334
+ const pid = child.pid;
335
+ writeFileSync(paths.pidFile, String(pid));
336
+
337
+ console.log(`Zubo started in background (PID ${pid})`);
338
+ console.log(`Logs: ${paths.logFile}`);
339
+ console.log(`Stop: bun run stop`);
340
+
341
+ // Unref so parent can exit
342
+ child.unref();
343
+ process.exit(0);
344
+ }
345
+
346
+ export function stopDaemon() {
347
+ if (!existsSync(paths.pidFile)) {
348
+ console.log("Zubo is not running (no PID file found).");
349
+ return;
350
+ }
351
+
352
+ const pid = parseInt(readFileSync(paths.pidFile, "utf-8").trim(), 10);
353
+
354
+ if (isNaN(pid)) {
355
+ console.log("Invalid PID file. Removing it.");
356
+ unlinkSync(paths.pidFile);
357
+ return;
358
+ }
359
+
360
+ try {
361
+ process.kill(pid, 0); // check if alive
362
+ process.kill(pid, "SIGTERM");
363
+ console.log(`Sent SIGTERM to Zubo (PID ${pid}).`);
364
+ } catch {
365
+ console.log(`Process ${pid} is not running. Cleaning up PID file.`);
366
+ }
367
+
368
+ try {
369
+ unlinkSync(paths.pidFile);
370
+ } catch (err: any) {
371
+ logger.warn("Failed to remove PID file", { error: (err as Error).message });
372
+ }
373
+ }
package/src/status.ts ADDED
@@ -0,0 +1,165 @@
1
+ import { existsSync, readFileSync, readdirSync } from "fs";
2
+ import { join } from "path";
3
+ import { paths } from "./config/paths";
4
+ import { configExists } from "./config/loader";
5
+ import { logger } from "./util/logger";
6
+
7
+ function isDaemonRunning(): { running: boolean; pid?: number } {
8
+ if (!existsSync(paths.pidFile)) return { running: false };
9
+ try {
10
+ const pid = parseInt(readFileSync(paths.pidFile, "utf-8").trim(), 10);
11
+ if (isNaN(pid)) return { running: false };
12
+ process.kill(pid, 0); // throws if process doesn't exist
13
+ return { running: true, pid };
14
+ } catch {
15
+ return { running: false };
16
+ }
17
+ }
18
+
19
+ interface UsageStat {
20
+ provider: string;
21
+ model: string;
22
+ total_input: number;
23
+ total_output: number;
24
+ calls: number;
25
+ }
26
+
27
+ function getDbStats(): {
28
+ messages: number;
29
+ memories: number;
30
+ usage: UsageStat[];
31
+ } | null {
32
+ if (!existsSync(paths.db)) return null;
33
+ try {
34
+ const { Database } = require("bun:sqlite");
35
+ const db = new Database(paths.db, { readonly: true });
36
+ const messages =
37
+ (db.query("SELECT COUNT(*) as count FROM messages").get() as any)
38
+ ?.count ?? 0;
39
+ const memories =
40
+ (db.query("SELECT COUNT(*) as count FROM memory_chunks").get() as any)
41
+ ?.count ?? 0;
42
+
43
+ let usage: UsageStat[] = [];
44
+ try {
45
+ usage = db
46
+ .query(
47
+ `SELECT provider, model,
48
+ SUM(input_tokens) as total_input,
49
+ SUM(output_tokens) as total_output,
50
+ COUNT(*) as calls
51
+ FROM usage GROUP BY provider, model`
52
+ )
53
+ .all() as UsageStat[];
54
+ } catch {
55
+ // usage table may not exist yet
56
+ }
57
+
58
+ db.close();
59
+ return { messages, memories, usage };
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ export function showStatus() {
66
+ console.log("\n Zubo Status\n");
67
+
68
+ // Config
69
+ if (configExists()) {
70
+ console.log(" Config: ~/.zubo/config.json ✓");
71
+ } else {
72
+ console.log(" Config: not found (run 'zubo setup')");
73
+ }
74
+
75
+ // Database
76
+ const stats = getDbStats();
77
+ if (stats) {
78
+ console.log(` Database: ~/.zubo/zubo.db ✓`);
79
+ console.log(` Messages: ${stats.messages}`);
80
+ console.log(` Memories: ${stats.memories}`);
81
+ } else {
82
+ console.log(" Database: not found");
83
+ }
84
+
85
+ // Usage
86
+ if (stats?.usage?.length) {
87
+ console.log(" Usage:");
88
+ for (const u of stats.usage) {
89
+ const total = u.total_input + u.total_output;
90
+ console.log(
91
+ ` ${u.provider}/${u.model}: ${u.calls} calls, ${total.toLocaleString()} tokens (${u.total_input.toLocaleString()} in / ${u.total_output.toLocaleString()} out)`
92
+ );
93
+ }
94
+ }
95
+
96
+ // LLM Provider
97
+ if (configExists()) {
98
+ try {
99
+ const config = JSON.parse(readFileSync(paths.config, "utf-8"));
100
+ if (config.providers && config.activeProvider) {
101
+ const active = config.providers[config.activeProvider];
102
+ const failover = config.failover?.length
103
+ ? ` → ${config.failover.join(", ")}`
104
+ : "";
105
+ console.log(` Provider: ${config.activeProvider}/${active?.model ?? "?"}${failover}`);
106
+ } else if (config.anthropicApiKey) {
107
+ console.log(` Provider: anthropic/${config.model ?? "claude-sonnet-4-5-20250929"} (legacy)`);
108
+ }
109
+ } catch (err: any) {
110
+ logger.warn("Failed to read provider config for status", { error: (err as Error).message });
111
+ }
112
+ }
113
+
114
+ // Channels
115
+ if (configExists()) {
116
+ try {
117
+ const config = JSON.parse(readFileSync(paths.config, "utf-8"));
118
+ const active: string[] = [];
119
+ if (config.channels?.telegram?.enabled !== false && (config.channels?.telegram?.botToken || config.telegramBotToken)) {
120
+ active.push("telegram");
121
+ }
122
+ if (config.channels?.discord?.enabled !== false && config.channels?.discord?.botToken) {
123
+ active.push("discord");
124
+ }
125
+ if (config.channels?.webchat?.enabled !== false && config.channels?.webchat) {
126
+ const p = config.channels.webchat.port;
127
+ active.push(p ? `webchat(:${p})` : "webchat(auto)");
128
+ }
129
+ console.log(` Channels: ${active.length ? active.join(", ") : "none"}`);
130
+ } catch (err: any) {
131
+ logger.warn("Failed to read channel config for status", { error: (err as Error).message });
132
+ }
133
+ }
134
+
135
+ // Skills
136
+ if (existsSync(paths.skills)) {
137
+ try {
138
+ const skillEntries = readdirSync(paths.skills).filter((e) =>
139
+ existsSync(join(paths.skills, e, "SKILL.md"))
140
+ );
141
+ console.log(` Skills: ${skillEntries.length} installed`);
142
+ } catch {
143
+ console.log(" Skills: error reading");
144
+ }
145
+ } else {
146
+ console.log(" Skills: none");
147
+ }
148
+
149
+ // SYSTEM.md
150
+ if (existsSync(paths.systemPrompt)) {
151
+ console.log(" Prompt: ~/.zubo/workspace/SYSTEM.md ✓");
152
+ } else {
153
+ console.log(" Prompt: using default (no SYSTEM.md)");
154
+ }
155
+
156
+ // Daemon status
157
+ const daemon = isDaemonRunning();
158
+ if (daemon.running) {
159
+ console.log(` Status: running (PID ${daemon.pid})`);
160
+ } else {
161
+ console.log(" Status: not running");
162
+ }
163
+
164
+ console.log("");
165
+ }