zubo 0.1.8 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zubo",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Your AI agent that never forgets. Persistent memory, 25+ tools, 7 channels, 11+ LLM providers — runs entirely on your machine.",
5
5
  "license": "MIT",
6
6
  "author": "thomaskanze",
package/site/index.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Zubo — Your AI Agent. Your Machine. Your Rules.</title>
7
- <meta name="description" content="Open-source AI agent that runs on your machine. One command to install, one file to configure. Persistent memory, 25+ tools, 7 messaging channels, 11+ LLM providers. MCP compatible. Zero complexity.">
7
+ <meta name="description" content="Open-source AI agent that runs on your machine. One command to install, one file to configure. Persistent memory, 25+ tools, 7 messaging channels, 12+ LLM providers. MCP compatible. Zero complexity.">
8
8
  <meta name="theme-color" content="#060608">
9
9
  <link rel="canonical" href="https://zubo.bot/">
10
10
  <meta property="og:title" content="Zubo — Your AI Agent. Your Machine. Your Rules.">
@@ -305,11 +305,11 @@
305
305
  <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="7.5 4.21 12 6.81 16.5 4.21"/><polyline points="7.5 19.79 7.5 14.6 3 12"/><polyline points="21 12 16.5 14.6 16.5 19.79"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>
306
306
  </div>
307
307
  <h3>Any LLM, Your Choice</h3>
308
- <p>11+ providers including Anthropic, OpenAI, Gemini, Ollama, Groq, DeepSeek, and more. Smart routing sends simple queries to fast, cheap models automatically &mdash; so you save money without thinking about it.</p>
308
+ <p>12+ providers including Anthropic, OpenAI, MiniMax, Ollama, Groq, DeepSeek, and more. Smart routing sends simple queries to fast, cheap models automatically &mdash; so you save money without thinking about it.</p>
309
309
  <div class="bento-models">
310
310
  <span class="model-tag">Claude</span>
311
311
  <span class="model-tag">GPT-4o</span>
312
- <span class="model-tag">Gemini</span>
312
+ <span class="model-tag">MiniMax</span>
313
313
  <span class="model-tag">DeepSeek</span>
314
314
  <span class="model-tag">Llama</span>
315
315
  <span class="model-tag">Mistral</span>
@@ -104,8 +104,8 @@ export async function delegateToAgent(
104
104
 
105
105
  // Filter out privileged tools from sub-agents to prevent escalation
106
106
  const FORBIDDEN_DELEGATION_TOOLS = new Set([
107
- "delegate", "manage_agents", "config_update",
108
- "secret_set", "secret_delete", "manage_skills",
107
+ "delegate", "delegate_task", "manage_agents",
108
+ "config_update", "secret_set", "secret_delete", "manage_skills",
109
109
  ]);
110
110
  const filteredTools = agent.tools.filter(
111
111
  (t) => !FORBIDDEN_DELEGATION_TOOLS.has(t)
@@ -58,6 +58,23 @@ const DEFAULT_PERSONALITY = `You are Zubo, a personal AI agent. You are friendly
58
58
  - **Twitter/X**: Bearer token for reading, full OAuth keys for posting. Store as twitter_bearer_token.
59
59
  - When the user says "connect my GitHub" or similar, ask for the credentials, store them with secret_set, then call connect_service.
60
60
 
61
+ ## LLM providers
62
+
63
+ You support 12+ LLM providers. The user can switch at any time using config_update.
64
+
65
+ **API-based providers** (need an API key):
66
+ - Anthropic (Claude), OpenAI (GPT), MiniMax (M2.5), Groq, Together, DeepSeek, xAI (Grok), Fireworks, Cerebras, Perplexity, OpenRouter
67
+ - To set up: config_update with path "providers.<name>" value {"apiKey":"...","model":"..."}, then set "activeProvider" to "<name>".
68
+
69
+ **Local providers** (no API key needed):
70
+ - Ollama, LM Studio — run models locally
71
+
72
+ **CLI-based providers** (NO API key needed — they use the user's own CLI authentication):
73
+ - **Claude Code** (provider name: "claude-code"): Spawns the Claude Code CLI. The user must have it installed and authenticated on their machine. To activate: config_update with path "providers.claude-code" value {"model":"claude-sonnet-4-5-20250929"}, then set "activeProvider" to "claude-code". NO apiKey field needed.
74
+ - **OpenAI Codex** (provider name: "codex"): Spawns the Codex CLI. The user must have it installed and authenticated on their machine. To activate: config_update with path "providers.codex" value {"model":"o4-mini"}, then set "activeProvider" to "codex". NO apiKey field needed.
75
+
76
+ IMPORTANT: When a user says "use codex" or "use claude code", do NOT ask for an API key. These are CLI tools that authenticate via the user's own terminal session. Just set the provider config and activate it.
77
+
61
78
  ## Tool confirmation
62
79
 
63
80
  Some tools (shell, file_write) require user confirmation. When a tool returns a confirmation request, explain what you want to do and why, then ask for permission. Never set _confirmed without explicit user approval.
@@ -154,8 +154,12 @@ class ImapClient {
154
154
  const bodyMatch = result.match(/\r\n\r\n([\s\S]*?)(?:\r\n\)|\r\nA\d+)/);
155
155
  if (bodyMatch) body = bodyMatch[1].trim();
156
156
 
157
- // Strip HTML tags if present
158
- body = body.replace(/<[^>]+>/g, "").trim();
157
+ // Strip script/style blocks and all HTML tags
158
+ body = body
159
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
160
+ .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "")
161
+ .replace(/<[^>]+>/g, "")
162
+ .trim();
159
163
  // Decode basic quoted-printable
160
164
  body = body.replace(/=\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
161
165
 
@@ -2013,8 +2013,15 @@ async function handleRequest(
2013
2013
  // CORS protection: reject cross-origin API requests
2014
2014
  if (url.pathname.startsWith("/api/")) {
2015
2015
  const origin = req.headers.get("origin");
2016
- if (origin && !origin.startsWith("http://localhost:") && !origin.startsWith("http://127.0.0.1:")) {
2017
- return Response.json({ error: "Cross-origin requests are not allowed" }, { status: 403 });
2016
+ if (origin) {
2017
+ const actualPort = server?.port ?? port;
2018
+ const allowed = [
2019
+ `http://localhost:${actualPort}`,
2020
+ `http://127.0.0.1:${actualPort}`,
2021
+ ];
2022
+ if (!allowed.includes(origin)) {
2023
+ return Response.json({ error: "Cross-origin requests are not allowed" }, { status: 403 });
2024
+ }
2018
2025
  }
2019
2026
  }
2020
2027
 
@@ -2038,15 +2045,20 @@ async function handleRequest(
2038
2045
  });
2039
2046
  }
2040
2047
 
2041
- // --- OAuth routes (no auth required — part of OAuth flow) ---
2048
+ // --- OAuth routes ---
2042
2049
 
2043
- // GET /oauth/:provider/authorize — redirect user to provider's auth page
2050
+ // GET /oauth/:provider/authorize — redirect user to provider's auth page (requires session key)
2044
2051
  const ALLOWED_OAUTH_PROVIDERS = new Set(["google", "github", "notion", "linear", "slack"]);
2045
2052
  if (url.pathname.match(/^\/oauth\/[a-z]+\/authorize$/) && req.method === "GET") {
2046
2053
  const provider = url.pathname.split("/")[2];
2047
2054
  if (!ALLOWED_OAUTH_PROVIDERS.has(provider)) {
2048
2055
  return Response.json({ error: "Unknown OAuth provider" }, { status: 400 });
2049
2056
  }
2057
+ // Require session key to prevent unauthorized OAuth initiation
2058
+ const sk = url.searchParams.get("sk");
2059
+ if (sk !== sessionKey) {
2060
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
2061
+ }
2050
2062
  try {
2051
2063
  const { getAuthUrl } = await import("../tools/oauth");
2052
2064
  const actualPort = server?.port ?? port;
@@ -13,7 +13,7 @@ export function createWhatsAppAdapter(
13
13
  let sock: any = null;
14
14
  const sessionDir = authDir ?? join(paths.root, "whatsapp-auth");
15
15
 
16
- return {
16
+ const adapter: ChannelAdapter = {
17
17
  channelName: "whatsapp",
18
18
 
19
19
  start() {
@@ -40,8 +40,11 @@ export function createWhatsAppAdapter(
40
40
  const shouldReconnect =
41
41
  lastDisconnect?.error?.output?.statusCode !== DisconnectReason.loggedOut;
42
42
  if (shouldReconnect) {
43
- logger.info("WhatsApp reconnecting...");
44
- // Re-create connection
43
+ logger.info("WhatsApp reconnecting in 3s...");
44
+ sock = null;
45
+ setTimeout(() => adapter.start(), 3000);
46
+ } else {
47
+ logger.info("WhatsApp logged out — not reconnecting");
45
48
  }
46
49
  } else if (connection === "open") {
47
50
  logger.info("WhatsApp connected");
@@ -116,4 +119,6 @@ export function createWhatsAppAdapter(
116
119
  }
117
120
  },
118
121
  };
122
+
123
+ return adapter;
119
124
  }
@@ -67,11 +67,16 @@ export class ClaudeCodeProvider implements LlmProvider {
67
67
  try { proc.kill(); } catch {}
68
68
  }, 300000);
69
69
 
70
- const exitCode = await proc.exited;
71
- clearTimeout(timeout);
72
-
73
- const stdout = await new Response(proc.stdout as ReadableStream).text();
74
- const stderr = await new Response(proc.stderr as ReadableStream).text();
70
+ let exitCode: number;
71
+ let stdout: string;
72
+ let stderr: string;
73
+ try {
74
+ exitCode = await proc.exited;
75
+ stdout = await new Response(proc.stdout as ReadableStream).text();
76
+ stderr = await new Response(proc.stderr as ReadableStream).text();
77
+ } finally {
78
+ clearTimeout(timeout);
79
+ }
75
80
 
76
81
  if (exitCode !== 0) {
77
82
  throw new Error(`Claude Code CLI exited with code ${exitCode}: ${stderr.slice(0, 500)}`);
package/src/llm/codex.ts CHANGED
@@ -64,11 +64,16 @@ export class CodexProvider implements LlmProvider {
64
64
  try { proc.kill(); } catch {}
65
65
  }, 300000);
66
66
 
67
- const exitCode = await proc.exited;
68
- clearTimeout(timeout);
69
-
70
- const stdout = await new Response(proc.stdout as ReadableStream).text();
71
- const stderr = await new Response(proc.stderr as ReadableStream).text();
67
+ let exitCode: number;
68
+ let stdout: string;
69
+ let stderr: string;
70
+ try {
71
+ exitCode = await proc.exited;
72
+ stdout = await new Response(proc.stdout as ReadableStream).text();
73
+ stderr = await new Response(proc.stderr as ReadableStream).text();
74
+ } finally {
75
+ clearTimeout(timeout);
76
+ }
72
77
 
73
78
  if (exitCode !== 0) {
74
79
  throw new Error(`Codex CLI exited with code ${exitCode}: ${stderr.slice(0, 500)}`);
@@ -18,6 +18,7 @@ const KNOWN_BASE_URLS: Record<string, string> = {
18
18
  cerebras: "https://api.cerebras.ai/v1",
19
19
  perplexity: "https://api.perplexity.ai",
20
20
  xai: "https://api.x.ai/v1",
21
+ minimax: "https://api.minimax.io/v1",
21
22
  ollama: "http://localhost:11434/v1",
22
23
  lmstudio: "http://localhost:1234/v1",
23
24
  };
@@ -28,6 +28,7 @@ const DEFAULT_CONTEXT_WINDOWS: Record<string, number> = {
28
28
  cerebras: 128_000,
29
29
  perplexity: 128_000,
30
30
  xai: 131_072, // Grok 4.1 supports up to 2M; safe default
31
+ minimax: 204_800, // MiniMax M2.5
31
32
  ollama: 8_000,
32
33
  lmstudio: 8_000,
33
34
  };
@@ -375,7 +376,7 @@ export class OpenAICompatProvider implements LlmProvider {
375
376
  } finally {
376
377
  reader.releaseLock();
377
378
  // Ensure the response body is fully consumed/cancelled to free resources
378
- try { await res.body!.cancel(); } catch (err: any) { logger.warn("Failed to cancel response body stream", { error: (err as Error).message }); }
379
+ try { await res.body?.cancel(); } catch (err: any) { logger.warn("Failed to cancel response body stream", { error: (err as Error).message }); }
379
380
  }
380
381
 
381
382
  // Emit tool_use_end for all tool calls
@@ -147,8 +147,6 @@ export class SmartRouterProvider implements LlmProvider {
147
147
  model: this.fast.model,
148
148
  reason: "simple query",
149
149
  });
150
- this.providerName = this.fast.providerName;
151
- this.model = this.fast.model;
152
150
  return this.fast;
153
151
  }
154
152
 
@@ -157,8 +155,6 @@ export class SmartRouterProvider implements LlmProvider {
157
155
  model: this.primary.model,
158
156
  reason: "complex query",
159
157
  });
160
- this.providerName = this.primary.providerName;
161
- this.model = this.primary.model;
162
158
  return this.primary;
163
159
  }
164
160
 
@@ -172,8 +168,6 @@ export class SmartRouterProvider implements LlmProvider {
172
168
  logger.warn("Fast model failed, falling back to primary", {
173
169
  error: err.message,
174
170
  });
175
- this.providerName = this.primary.providerName;
176
- this.model = this.primary.model;
177
171
  return this.primary.chat(request);
178
172
  }
179
173
  }
@@ -212,13 +206,9 @@ export class SmartRouterProvider implements LlmProvider {
212
206
  }
213
207
 
214
208
  // Fallback to primary stream
215
- this.providerName = this.primary.providerName;
216
- this.model = this.primary.model;
217
209
  } else {
218
210
  // Fast model has no streaming, fall back to primary
219
211
  logger.info("Fast model has no streaming support, using primary");
220
- this.providerName = this.primary.providerName;
221
- this.model = this.primary.model;
222
212
  }
223
213
  }
224
214
 
package/src/setup.ts CHANGED
@@ -168,6 +168,21 @@ const PROVIDER_OPTIONS: ProviderOption[] = [
168
168
  },
169
169
  {
170
170
  key: "9",
171
+ label: "MiniMax (M2.5)",
172
+ setup: async () => {
173
+ const apiKey = await prompt(" MiniMax API key: ");
174
+ const model = await prompt(" Model [MiniMax-M2.5] (or MiniMax-M2.5-highspeed): ");
175
+ return {
176
+ name: "minimax",
177
+ config: {
178
+ apiKey,
179
+ model: model || "MiniMax-M2.5",
180
+ },
181
+ };
182
+ },
183
+ },
184
+ {
185
+ key: "10",
171
186
  label: "Fireworks AI",
172
187
  setup: async () => {
173
188
  const apiKey = await prompt(" Fireworks API key: ");
@@ -183,7 +198,7 @@ const PROVIDER_OPTIONS: ProviderOption[] = [
183
198
  },
184
199
  },
185
200
  {
186
- key: "10",
201
+ key: "11",
187
202
  label: "Cerebras",
188
203
  setup: async () => {
189
204
  const apiKey = await prompt(" Cerebras API key: ");
@@ -199,7 +214,7 @@ const PROVIDER_OPTIONS: ProviderOption[] = [
199
214
  },
200
215
  },
201
216
  {
202
- key: "11",
217
+ key: "12",
203
218
  label: "LM Studio (local)",
204
219
  setup: async () => {
205
220
  const baseUrl = await prompt(" LM Studio URL [http://localhost:1234/v1]: ");
@@ -215,7 +230,7 @@ const PROVIDER_OPTIONS: ProviderOption[] = [
215
230
  },
216
231
  },
217
232
  {
218
- key: "12",
233
+ key: "13",
219
234
  label: "Other (OpenAI-compatible)",
220
235
  setup: async () => {
221
236
  const name = await prompt(" Provider name: ");
@@ -35,7 +35,9 @@ export function registerDelegateTool(llm: LlmProvider) {
35
35
 
36
36
  // Dynamic import to avoid circular dependency
37
37
  const { delegateToAgent } = await import("../../agent/delegate");
38
- const result = await delegateToAgent(llm, agent, task);
38
+ const crypto = await import("crypto");
39
+ const contextId = "delegation-" + crypto.randomUUID().slice(0, 8);
40
+ const result = await delegateToAgent(llm, agent, task, contextId);
39
41
  return result;
40
42
  },
41
43
  });
@@ -75,6 +75,7 @@ export function registerManageAgentsTool() {
75
75
  const FORBIDDEN_SUBAGENT_TOOLS = [
76
76
  "manage_agents", // Prevents recursive agent creation
77
77
  "delegate", // Prevents delegation loops
78
+ "delegate_task", // Prevents ad-hoc delegation loops
78
79
  "config_update", // Prevents config tampering
79
80
  "secret_set", // Prevents secret manipulation
80
81
  "secret_delete", // Prevents secret deletion
@@ -433,6 +433,11 @@ export async function getToken(provider: string): Promise<string | null> {
433
433
  return await refreshPromise;
434
434
  } catch (err: any) {
435
435
  logger.warn(`[OAuth] Token refresh failed for ${provider}`, { error: err.message });
436
+ // If refresh token is invalid/revoked, remove stale token to stop retry loops
437
+ if (err.message?.includes("400") || err.message?.includes("401") || err.message?.includes("invalid")) {
438
+ logger.warn(`[OAuth] Removing stale token for ${provider} — user must re-authenticate`);
439
+ try { revokeToken(provider); } catch {}
440
+ }
436
441
  return null;
437
442
  }
438
443
  }
package/src/util/costs.ts CHANGED
@@ -24,6 +24,10 @@ const MODEL_PRICING: Record<string, { input: number; output: number }> = {
24
24
  "deepseek-chat": { input: 0.56, output: 1.68 },
25
25
  "deepseek-reasoner": { input: 0.55, output: 2.19 },
26
26
 
27
+ // MiniMax
28
+ "MiniMax-M2.5": { input: 0.3, output: 1.2 },
29
+ "MiniMax-M2.5-highspeed": { input: 0.3, output: 2.4 },
30
+
27
31
  // Groq
28
32
  "llama-3.3-70b-versatile": { input: 0.59, output: 0.79 },
29
33
  "llama-3.1-8b-instant": { input: 0.05, output: 0.08 },