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 +1 -1
- package/site/index.html +3 -3
- package/src/agent/delegate.ts +2 -2
- package/src/agent/prompts.ts +17 -0
- package/src/channels/email.ts +6 -2
- package/src/channels/webchat.ts +16 -4
- package/src/channels/whatsapp.ts +8 -3
- package/src/llm/claude-code.ts +10 -5
- package/src/llm/codex.ts +10 -5
- package/src/llm/factory.ts +1 -0
- package/src/llm/openai-compat.ts +2 -1
- package/src/llm/smart-router.ts +0 -10
- package/src/setup.ts +18 -3
- package/src/tools/builtin/delegate.ts +3 -1
- package/src/tools/builtin/manage-agents.ts +1 -0
- package/src/tools/oauth.ts +5 -0
- package/src/util/costs.ts +4 -0
package/package.json
CHANGED
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,
|
|
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>
|
|
308
|
+
<p>12+ providers including Anthropic, OpenAI, MiniMax, Ollama, Groq, DeepSeek, and more. Smart routing sends simple queries to fast, cheap models automatically — 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">
|
|
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>
|
package/src/agent/delegate.ts
CHANGED
|
@@ -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", "
|
|
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)
|
package/src/agent/prompts.ts
CHANGED
|
@@ -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.
|
package/src/channels/email.ts
CHANGED
|
@@ -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
|
|
158
|
-
body = body
|
|
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
|
|
package/src/channels/webchat.ts
CHANGED
|
@@ -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
|
|
2017
|
-
|
|
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
|
|
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;
|
package/src/channels/whatsapp.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/llm/claude-code.ts
CHANGED
|
@@ -67,11 +67,16 @@ export class ClaudeCodeProvider implements LlmProvider {
|
|
|
67
67
|
try { proc.kill(); } catch {}
|
|
68
68
|
}, 300000);
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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)}`);
|
package/src/llm/factory.ts
CHANGED
|
@@ -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
|
};
|
package/src/llm/openai-compat.ts
CHANGED
|
@@ -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
|
|
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
|
package/src/llm/smart-router.ts
CHANGED
|
@@ -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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
package/src/tools/oauth.ts
CHANGED
|
@@ -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 },
|