zubo 0.1.19 → 0.1.21
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/README.md +2 -2
- package/package.json +1 -1
- package/site/docs/agents.html +2 -2
- package/site/docs/api.html +2 -2
- package/site/docs/cli.html +7 -2
- package/site/docs/config.html +92 -0
- package/site/docs/index.html +8 -6
- package/site/docs/integrations.html +3 -3
- package/site/docs/marketplace.html +9 -9
- package/site/docs/security.html +4 -4
- package/site/docs/skills.html +1 -1
- package/site/docs/webhooks.html +17 -0
- package/site/index.html +4 -4
- package/site/install.sh +11 -5
- package/src/agent/compaction.ts +20 -4
- package/src/agent/history.ts +7 -2
- package/src/agent/loop.ts +50 -18
- package/src/agent/prompts.ts +2 -0
- package/src/agent/session.ts +69 -2
- package/src/agent/summarizer.ts +223 -0
- package/src/channels/dashboard.html.ts +98 -56
- package/src/channels/telegram.ts +10 -1
- package/src/channels/webchat.ts +40 -8
- package/src/llm/claude-code.ts +1 -2
- package/src/llm/codex.ts +3 -3
- package/src/llm/factory.ts +81 -2
- package/src/llm/failover.ts +59 -4
- package/src/llm/smart-router.ts +14 -6
- package/src/memory/knowledge-graph.ts +1 -1
- package/src/memory/vector-index.ts +1 -1
- package/src/scheduler/visual-workflows.ts +1 -1
- package/src/setup-web.html.ts +1371 -0
- package/src/setup-web.ts +165 -0
- package/src/setup.ts +266 -15
- package/src/start.ts +12 -2
- package/src/tools/builtin/config-update.ts +18 -1
- package/src/tools/executor.ts +2 -2
- package/src/tools/mcp-registry.ts +12 -6
- package/src/tools/permissions.ts +2 -2
package/src/setup-web.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { configSchema } from "./config/schema";
|
|
2
|
+
import type { ProviderConfig } from "./config/schema";
|
|
3
|
+
import { createProvider, validateProvider } from "./llm/factory";
|
|
4
|
+
import { finalizeSetup, type SetupResult } from "./setup";
|
|
5
|
+
import { SETUP_WIZARD_HTML } from "./setup-web.html";
|
|
6
|
+
|
|
7
|
+
const BOLD = "\x1b[1m";
|
|
8
|
+
const DIM = "\x1b[2m";
|
|
9
|
+
const CYAN = "\x1b[36m";
|
|
10
|
+
const YELLOW = "\x1b[33m";
|
|
11
|
+
const RESET = "\x1b[0m";
|
|
12
|
+
|
|
13
|
+
const TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
14
|
+
|
|
15
|
+
export async function runWebSetup() {
|
|
16
|
+
let resolve: (result: SetupResult) => void;
|
|
17
|
+
let reject: (err: Error) => void;
|
|
18
|
+
const done = new Promise<SetupResult>((res, rej) => {
|
|
19
|
+
resolve = res;
|
|
20
|
+
reject = rej;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const server = Bun.serve({
|
|
24
|
+
port: 0,
|
|
25
|
+
hostname: "127.0.0.1",
|
|
26
|
+
|
|
27
|
+
async fetch(req) {
|
|
28
|
+
const url = new URL(req.url);
|
|
29
|
+
|
|
30
|
+
// ── Serve wizard HTML ──
|
|
31
|
+
if (url.pathname === "/" && req.method === "GET") {
|
|
32
|
+
return new Response(SETUP_WIZARD_HTML, {
|
|
33
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── API: Validate provider ──
|
|
38
|
+
if (url.pathname === "/api/validate-provider" && req.method === "POST") {
|
|
39
|
+
try {
|
|
40
|
+
const body = await req.json() as { name: string; config: ProviderConfig };
|
|
41
|
+
const testConfig = configSchema.parse({
|
|
42
|
+
providers: { [body.name]: body.config },
|
|
43
|
+
activeProvider: body.name,
|
|
44
|
+
});
|
|
45
|
+
const provider = await createProvider(testConfig);
|
|
46
|
+
const err = await validateProvider(provider);
|
|
47
|
+
if (err) return Response.json({ ok: false, error: err });
|
|
48
|
+
return Response.json({ ok: true });
|
|
49
|
+
} catch (e: any) {
|
|
50
|
+
return Response.json({ ok: false, error: e.message ?? String(e) });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── API: Detect local models (Ollama / LM Studio) ──
|
|
55
|
+
if (url.pathname === "/api/detect-local" && req.method === "POST") {
|
|
56
|
+
const body = await req.json() as { type: "ollama" | "lmstudio" };
|
|
57
|
+
const results: { running: boolean; models: string[] } = { running: false, models: [] };
|
|
58
|
+
|
|
59
|
+
if (body.type === "ollama") {
|
|
60
|
+
// Check CLI
|
|
61
|
+
const which = Bun.spawnSync(["which", "ollama"], { stdout: "pipe", stderr: "pipe" });
|
|
62
|
+
const cliInstalled = which.exitCode === 0;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch("http://localhost:11434/api/tags", {
|
|
66
|
+
signal: AbortSignal.timeout(3000),
|
|
67
|
+
});
|
|
68
|
+
if (res.ok) {
|
|
69
|
+
const data = await res.json() as { models?: { name: string }[] };
|
|
70
|
+
results.running = true;
|
|
71
|
+
results.models = (data.models ?? []).map((m) => m.name);
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
74
|
+
return Response.json({ ...results, cliInstalled });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (body.type === "lmstudio") {
|
|
78
|
+
try {
|
|
79
|
+
const res = await fetch("http://localhost:1234/v1/models", {
|
|
80
|
+
signal: AbortSignal.timeout(3000),
|
|
81
|
+
});
|
|
82
|
+
if (res.ok) {
|
|
83
|
+
const data = await res.json() as { data?: { id: string }[] };
|
|
84
|
+
results.running = true;
|
|
85
|
+
results.models = (data.data ?? []).map((m) => m.id);
|
|
86
|
+
}
|
|
87
|
+
} catch {}
|
|
88
|
+
return Response.json(results);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return Response.json(results);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── API: Detect CLI tools (Claude Code / Codex) ──
|
|
95
|
+
if (url.pathname === "/api/detect-cli" && req.method === "POST") {
|
|
96
|
+
const body = await req.json() as { tool: "claude" | "codex" };
|
|
97
|
+
const bin = body.tool === "claude" ? "claude" : "codex";
|
|
98
|
+
const which = Bun.spawnSync(["which", bin], { stdout: "pipe", stderr: "pipe" });
|
|
99
|
+
return Response.json({ installed: which.exitCode === 0 });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── API: Complete setup ──
|
|
103
|
+
if (url.pathname === "/api/complete" && req.method === "POST") {
|
|
104
|
+
try {
|
|
105
|
+
const body = await req.json() as SetupResult;
|
|
106
|
+
// Basic validation
|
|
107
|
+
if (!body.providers || !body.activeProvider) {
|
|
108
|
+
return Response.json({ ok: false, error: "Missing provider configuration" });
|
|
109
|
+
}
|
|
110
|
+
resolve!(body);
|
|
111
|
+
return Response.json({ ok: true });
|
|
112
|
+
} catch (e: any) {
|
|
113
|
+
return Response.json({ ok: false, error: e.message ?? String(e) });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return new Response("Not found", { status: 404 });
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const port = server.port;
|
|
122
|
+
const url = `http://localhost:${port}`;
|
|
123
|
+
|
|
124
|
+
// Auto-open in browser
|
|
125
|
+
const platform = process.platform;
|
|
126
|
+
if (platform === "darwin") {
|
|
127
|
+
Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" });
|
|
128
|
+
} else if (platform === "linux") {
|
|
129
|
+
Bun.spawn(["xdg-open", url], { stdout: "ignore", stderr: "ignore" });
|
|
130
|
+
} else if (platform === "win32") {
|
|
131
|
+
Bun.spawn(["cmd", "/c", "start", url], { stdout: "ignore", stderr: "ignore" });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log("");
|
|
135
|
+
console.log(` ${CYAN}→${RESET} Setup wizard opened at ${BOLD}${url}${RESET}`);
|
|
136
|
+
console.log(` ${DIM} Waiting for setup to complete in the browser...${RESET}`);
|
|
137
|
+
console.log(` ${DIM} Press Ctrl+C to cancel.${RESET}`);
|
|
138
|
+
console.log("");
|
|
139
|
+
|
|
140
|
+
// Timeout
|
|
141
|
+
const timer = setTimeout(() => {
|
|
142
|
+
reject!(new Error("Setup timed out after 30 minutes"));
|
|
143
|
+
}, TIMEOUT_MS);
|
|
144
|
+
|
|
145
|
+
// SIGINT handler
|
|
146
|
+
const cleanup = () => {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
server.stop();
|
|
149
|
+
};
|
|
150
|
+
process.on("SIGINT", () => {
|
|
151
|
+
cleanup();
|
|
152
|
+
console.log(`\n ${YELLOW}!${RESET} Setup cancelled.\n`);
|
|
153
|
+
process.exit(0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const result = await done;
|
|
158
|
+
cleanup();
|
|
159
|
+
await finalizeSetup(result);
|
|
160
|
+
} catch (err: any) {
|
|
161
|
+
cleanup();
|
|
162
|
+
console.log(` ${YELLOW}!${RESET} ${err.message}\n`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
}
|
package/src/setup.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { paths, ensureDirectories } from "./config/paths";
|
|
2
2
|
import { saveConfig } from "./config/loader";
|
|
3
3
|
import { configSchema } from "./config/schema";
|
|
4
|
-
import type { ProviderConfig } from "./config/schema";
|
|
4
|
+
import type { ProviderConfig, ZuboConfig } from "./config/schema";
|
|
5
5
|
import { getDb } from "./db/connection";
|
|
6
6
|
import { runMigrations } from "./db/migrations";
|
|
7
|
+
import { createProvider, validateProvider } from "./llm/factory";
|
|
7
8
|
import { logger } from "./util/logger";
|
|
8
9
|
import { existsSync } from "fs";
|
|
9
10
|
import { installBuiltinSkills } from "./tools/skill-installer";
|
|
@@ -77,15 +78,73 @@ const PROVIDER_OPTIONS: ProviderOption[] = [
|
|
|
77
78
|
key: "3",
|
|
78
79
|
label: "Ollama (local)",
|
|
79
80
|
setup: async () => {
|
|
80
|
-
|
|
81
|
-
const
|
|
81
|
+
// Check if ollama is installed
|
|
82
|
+
const which = Bun.spawnSync(["which", "ollama"], { stdout: "pipe", stderr: "pipe" });
|
|
83
|
+
if (which.exitCode !== 0) {
|
|
84
|
+
warn("'ollama' not found on your system.\n");
|
|
85
|
+
console.log(` Install Ollama from ${CYAN}https://ollama.com${RESET}`);
|
|
86
|
+
console.log(` ${DIM}macOS: brew install ollama${RESET}`);
|
|
87
|
+
console.log(` ${DIM}Linux: curl -fsSL https://ollama.com/install.sh | sh${RESET}`);
|
|
88
|
+
console.log(` ${DIM}Windows: Download from https://ollama.com/download${RESET}\n`);
|
|
89
|
+
const cont = await prompt(" Press Enter after installing, or 'skip' to configure anyway: ");
|
|
90
|
+
if (cont.toLowerCase() !== "skip") {
|
|
91
|
+
const recheck = Bun.spawnSync(["which", "ollama"], { stdout: "pipe", stderr: "pipe" });
|
|
92
|
+
if (recheck.exitCode !== 0) {
|
|
93
|
+
warn("Still not found. Saving config anyway — install Ollama before starting.\n");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
ok("'ollama' CLI found");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check if Ollama server is running
|
|
101
|
+
const baseUrl = "http://localhost:11434/v1";
|
|
102
|
+
info("Checking if Ollama server is running...");
|
|
103
|
+
let models: string[] = [];
|
|
104
|
+
try {
|
|
105
|
+
const res = await fetch("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(3000) });
|
|
106
|
+
if (res.ok) {
|
|
107
|
+
const data = await res.json() as { models?: { name: string }[] };
|
|
108
|
+
models = (data.models ?? []).map(m => m.name);
|
|
109
|
+
ok(`Ollama is running (${models.length} model${models.length !== 1 ? "s" : ""} available)`);
|
|
110
|
+
} else {
|
|
111
|
+
warn("Ollama server responded with an error. Make sure it's running: ollama serve");
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
warn("Ollama server not reachable at localhost:11434.");
|
|
115
|
+
info("Start it with: ollama serve");
|
|
116
|
+
info("Or on macOS, just open the Ollama app.\n");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Show available models or suggest pulling one
|
|
120
|
+
let model = "";
|
|
121
|
+
if (models.length > 0) {
|
|
122
|
+
console.log(`\n ${BOLD}Available models:${RESET}`);
|
|
123
|
+
models.slice(0, 10).forEach((m, i) => console.log(` ${DIM}${(i + 1).toString().padStart(2)}.${RESET} ${m}`));
|
|
124
|
+
if (models.length > 10) console.log(` ${DIM} ... and ${models.length - 10} more${RESET}`);
|
|
125
|
+
console.log("");
|
|
126
|
+
const modelInput = await prompt(` Model [${models[0]}]: `);
|
|
127
|
+
const num = parseInt(modelInput, 10);
|
|
128
|
+
if (modelInput && !isNaN(num) && num >= 1 && num <= models.length) {
|
|
129
|
+
model = models[num - 1];
|
|
130
|
+
} else {
|
|
131
|
+
model = modelInput || models[0];
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
info("No models downloaded yet. Pull one with:");
|
|
135
|
+
console.log(` ${DIM}ollama pull llama3.3${RESET}`);
|
|
136
|
+
console.log(` ${DIM}ollama pull mistral${RESET}`);
|
|
137
|
+
console.log(` ${DIM}ollama pull qwen2.5${RESET}\n`);
|
|
138
|
+
model = await prompt(" Model [llama3.3]: ");
|
|
139
|
+
model = model || "llama3.3";
|
|
140
|
+
}
|
|
141
|
+
|
|
82
142
|
return {
|
|
83
143
|
name: "ollama",
|
|
84
144
|
config: {
|
|
85
|
-
baseUrl
|
|
145
|
+
baseUrl,
|
|
86
146
|
apiKey: "ollama",
|
|
87
|
-
model
|
|
88
|
-
streaming: false,
|
|
147
|
+
model,
|
|
89
148
|
},
|
|
90
149
|
};
|
|
91
150
|
},
|
|
@@ -217,20 +276,147 @@ const PROVIDER_OPTIONS: ProviderOption[] = [
|
|
|
217
276
|
key: "12",
|
|
218
277
|
label: "LM Studio (local)",
|
|
219
278
|
setup: async () => {
|
|
220
|
-
const baseUrl =
|
|
221
|
-
|
|
279
|
+
const baseUrl = "http://localhost:1234/v1";
|
|
280
|
+
info("Checking if LM Studio server is running...");
|
|
281
|
+
|
|
282
|
+
let models: string[] = [];
|
|
283
|
+
try {
|
|
284
|
+
const res = await fetch(`${baseUrl}/models`, { signal: AbortSignal.timeout(3000) });
|
|
285
|
+
if (res.ok) {
|
|
286
|
+
const data = await res.json() as { data?: { id: string }[] };
|
|
287
|
+
models = (data.data ?? []).map(m => m.id);
|
|
288
|
+
ok(`LM Studio is running (${models.length} model${models.length !== 1 ? "s" : ""} loaded)`);
|
|
289
|
+
}
|
|
290
|
+
} catch {
|
|
291
|
+
warn("LM Studio server not reachable at localhost:1234.\n");
|
|
292
|
+
console.log(` ${BOLD}To set up LM Studio:${RESET}`);
|
|
293
|
+
console.log(` ${DIM}1.${RESET} Download from ${CYAN}https://lmstudio.ai${RESET}`);
|
|
294
|
+
console.log(` ${DIM}2.${RESET} Open LM Studio and download a model (e.g. Llama 3.3, Mistral)`);
|
|
295
|
+
console.log(` ${DIM}3.${RESET} Go to the "Local Server" tab (left sidebar)`);
|
|
296
|
+
console.log(` ${DIM}4.${RESET} Click "Start Server" — it runs on port 1234 by default\n`);
|
|
297
|
+
const cont = await prompt(" Press Enter once LM Studio server is running, or 'skip' to configure anyway: ");
|
|
298
|
+
if (cont.toLowerCase() !== "skip") {
|
|
299
|
+
try {
|
|
300
|
+
const retry = await fetch(`${baseUrl}/models`, { signal: AbortSignal.timeout(3000) });
|
|
301
|
+
if (retry.ok) {
|
|
302
|
+
const data = await retry.json() as { data?: { id: string }[] };
|
|
303
|
+
models = (data.data ?? []).map(m => m.id);
|
|
304
|
+
ok(`LM Studio is running (${models.length} model${models.length !== 1 ? "s" : ""} loaded)`);
|
|
305
|
+
} else {
|
|
306
|
+
warn("Still not reachable. Config saved — start LM Studio before running Zubo.");
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
warn("Still not reachable. Config saved — start LM Studio before running Zubo.");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
let model = "";
|
|
315
|
+
if (models.length > 0) {
|
|
316
|
+
console.log(`\n ${BOLD}Loaded models:${RESET}`);
|
|
317
|
+
models.forEach((m, i) => console.log(` ${DIM}${(i + 1).toString().padStart(2)}.${RESET} ${m}`));
|
|
318
|
+
console.log("");
|
|
319
|
+
const lmsInput = await prompt(` Model [${models[0]}]: `);
|
|
320
|
+
const lmsNum = parseInt(lmsInput, 10);
|
|
321
|
+
if (lmsInput && !isNaN(lmsNum) && lmsNum >= 1 && lmsNum <= models.length) {
|
|
322
|
+
model = models[lmsNum - 1];
|
|
323
|
+
} else {
|
|
324
|
+
model = lmsInput || models[0];
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
info("No models detected. Load a model in LM Studio first.");
|
|
328
|
+
model = await prompt(" Model name: ");
|
|
329
|
+
model = model || "default";
|
|
330
|
+
}
|
|
331
|
+
|
|
222
332
|
return {
|
|
223
333
|
name: "lmstudio",
|
|
224
334
|
config: {
|
|
225
|
-
baseUrl
|
|
335
|
+
baseUrl,
|
|
226
336
|
apiKey: "lm-studio",
|
|
227
|
-
model
|
|
337
|
+
model,
|
|
228
338
|
},
|
|
229
339
|
};
|
|
230
340
|
},
|
|
231
341
|
},
|
|
232
342
|
{
|
|
233
343
|
key: "13",
|
|
344
|
+
label: "Claude Code (CLI)",
|
|
345
|
+
setup: async () => {
|
|
346
|
+
// Check if claude CLI is installed
|
|
347
|
+
const which = Bun.spawnSync(["which", "claude"], { stdout: "pipe", stderr: "pipe" });
|
|
348
|
+
if (which.exitCode !== 0) {
|
|
349
|
+
warn("'claude' CLI not found. Install it first:");
|
|
350
|
+
console.log(` ${DIM}npm install -g @anthropic-ai/claude-code${RESET}\n`);
|
|
351
|
+
const cont = await prompt(" Press Enter after installing, or 'skip' to continue anyway: ");
|
|
352
|
+
if (cont.toLowerCase() === "skip") {
|
|
353
|
+
return { name: "claude-code", config: { model: "default" } };
|
|
354
|
+
}
|
|
355
|
+
const recheck = Bun.spawnSync(["which", "claude"], { stdout: "pipe", stderr: "pipe" });
|
|
356
|
+
if (recheck.exitCode !== 0) {
|
|
357
|
+
warn("Still not found. Config saved — install 'claude' CLI before starting.");
|
|
358
|
+
return { name: "claude-code", config: { model: "default" } };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
ok("'claude' CLI found");
|
|
362
|
+
// Quick auth check — run claude with a trivial prompt and short timeout
|
|
363
|
+
const check = Bun.spawnSync(["claude", "-p", "hi", "--max-turns", "1"], {
|
|
364
|
+
stdout: "pipe", stderr: "pipe", timeout: 10_000,
|
|
365
|
+
});
|
|
366
|
+
if (check.exitCode === 0) {
|
|
367
|
+
ok("Claude Code authenticated");
|
|
368
|
+
} else {
|
|
369
|
+
warn("Claude Code may not be authenticated yet.");
|
|
370
|
+
info("Open a separate terminal and run: claude");
|
|
371
|
+
info("Complete the login flow, then come back here.\n");
|
|
372
|
+
await prompt(" Press Enter once you've authenticated... ");
|
|
373
|
+
// Re-check
|
|
374
|
+
const recheck = Bun.spawnSync(["claude", "-p", "hi", "--max-turns", "1"], {
|
|
375
|
+
stdout: "pipe", stderr: "pipe", timeout: 10_000,
|
|
376
|
+
});
|
|
377
|
+
if (recheck.exitCode === 0) {
|
|
378
|
+
ok("Claude Code authenticated");
|
|
379
|
+
} else {
|
|
380
|
+
warn("Still not authenticated. Run 'claude' in a terminal to log in before starting Zubo.");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return { name: "claude-code", config: { model: "default" } };
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
key: "14",
|
|
388
|
+
label: "OpenAI Codex (CLI)",
|
|
389
|
+
setup: async () => {
|
|
390
|
+
// Check if codex CLI is installed
|
|
391
|
+
const which = Bun.spawnSync(["which", "codex"], { stdout: "pipe", stderr: "pipe" });
|
|
392
|
+
if (which.exitCode !== 0) {
|
|
393
|
+
warn("'codex' CLI not found. Install it first:");
|
|
394
|
+
console.log(` ${DIM}npm install -g @openai/codex${RESET}\n`);
|
|
395
|
+
const cont = await prompt(" Press Enter after installing, or 'skip' to continue anyway: ");
|
|
396
|
+
if (cont.toLowerCase() === "skip") {
|
|
397
|
+
return { name: "codex", config: { model: "default" } };
|
|
398
|
+
}
|
|
399
|
+
const recheck = Bun.spawnSync(["which", "codex"], { stdout: "pipe", stderr: "pipe" });
|
|
400
|
+
if (recheck.exitCode !== 0) {
|
|
401
|
+
warn("Still not found. Config saved — install 'codex' CLI before starting.");
|
|
402
|
+
return { name: "codex", config: { model: "default" } };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
ok("'codex' CLI found");
|
|
406
|
+
info("Authenticating — this will open your browser if needed...");
|
|
407
|
+
const login = Bun.spawnSync(["codex", "login"], {
|
|
408
|
+
stdin: "inherit", stdout: "inherit", stderr: "inherit",
|
|
409
|
+
});
|
|
410
|
+
if (login.exitCode !== 0) {
|
|
411
|
+
warn("Login may not have completed. Run 'codex login' manually later.");
|
|
412
|
+
} else {
|
|
413
|
+
ok("Codex ready");
|
|
414
|
+
}
|
|
415
|
+
return { name: "codex", config: { model: "default" } };
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
key: "15",
|
|
234
420
|
label: "Other (OpenAI-compatible)",
|
|
235
421
|
setup: async () => {
|
|
236
422
|
const name = await prompt(" Provider name: ");
|
|
@@ -295,7 +481,29 @@ async function setupProvider(): Promise<{
|
|
|
295
481
|
const anthropicApiKey =
|
|
296
482
|
result.name === "anthropic" ? result.config.apiKey : undefined;
|
|
297
483
|
|
|
298
|
-
|
|
484
|
+
const isCliProvider = result.name === "claude-code" || result.name === "codex";
|
|
485
|
+
ok(`${result.name} configured` + (isCliProvider ? "" : ` (${result.config.model})`));
|
|
486
|
+
|
|
487
|
+
// Quick validation — test if the provider actually works
|
|
488
|
+
if (!isCliProvider) {
|
|
489
|
+
info("Testing connection...");
|
|
490
|
+
try {
|
|
491
|
+
const testConfig = configSchema.parse({
|
|
492
|
+
providers: { [result.name]: result.config },
|
|
493
|
+
activeProvider: result.name,
|
|
494
|
+
}) as ZuboConfig;
|
|
495
|
+
const testLlm = await createProvider(testConfig);
|
|
496
|
+
const validationErr = await validateProvider(testLlm);
|
|
497
|
+
if (validationErr) {
|
|
498
|
+
warn(validationErr);
|
|
499
|
+
info("You can fix this later with: zubo config set providers." + result.name + ".apiKey <key>");
|
|
500
|
+
} else {
|
|
501
|
+
ok("Connection verified — API key works!");
|
|
502
|
+
}
|
|
503
|
+
} catch {
|
|
504
|
+
// Validation itself failed — not critical, move on
|
|
505
|
+
}
|
|
506
|
+
}
|
|
299
507
|
|
|
300
508
|
// Offer fallback
|
|
301
509
|
const addFallback = await prompt("\n Add a fallback provider? (y/N): ");
|
|
@@ -307,7 +515,8 @@ async function setupProvider(): Promise<{
|
|
|
307
515
|
if (fb) {
|
|
308
516
|
providers[fb.name] = fb.config;
|
|
309
517
|
failover.push(fb.name);
|
|
310
|
-
|
|
518
|
+
const isFbCli = fb.name === "claude-code" || fb.name === "codex";
|
|
519
|
+
ok(`${fb.name} added as fallback` + (isFbCli ? "" : ` (${fb.config.model})`));
|
|
311
520
|
}
|
|
312
521
|
}
|
|
313
522
|
|
|
@@ -515,6 +724,18 @@ export async function runSetup() {
|
|
|
515
724
|
console.log("");
|
|
516
725
|
console.log(` ${DIM}This wizard will configure your agent in 4 steps.${RESET}`);
|
|
517
726
|
console.log(` ${DIM}Press Enter at any prompt to accept the default [in brackets].${RESET}`);
|
|
727
|
+
console.log("");
|
|
728
|
+
|
|
729
|
+
// ── Setup mode choice ──
|
|
730
|
+
console.log(` ${BOLD}Setup mode:${RESET}`);
|
|
731
|
+
console.log(` ${DIM}1.${RESET} Terminal`);
|
|
732
|
+
console.log(` ${DIM}2.${RESET} Dashboard ${DIM}(opens in your browser)${RESET}`);
|
|
733
|
+
console.log("");
|
|
734
|
+
const mode = await prompt(" Choice [1]: ");
|
|
735
|
+
if (mode === "2" || mode.toLowerCase() === "d" || mode.toLowerCase() === "dashboard") {
|
|
736
|
+
const { runWebSetup } = await import("./setup-web");
|
|
737
|
+
return runWebSetup();
|
|
738
|
+
}
|
|
518
739
|
|
|
519
740
|
// ── Step 1: LLM Provider ──
|
|
520
741
|
step(1, 4, "LLM Provider");
|
|
@@ -533,6 +754,39 @@ export async function runSetup() {
|
|
|
533
754
|
const smartRouting = await setupSmartRouting(providers, activeProvider);
|
|
534
755
|
|
|
535
756
|
// ── Finalize ──────────────────────────────────────────────────
|
|
757
|
+
await finalizeSetup({
|
|
758
|
+
providers,
|
|
759
|
+
activeProvider,
|
|
760
|
+
failover,
|
|
761
|
+
anthropicApiKey,
|
|
762
|
+
telegramBotToken,
|
|
763
|
+
channels,
|
|
764
|
+
smartRouting,
|
|
765
|
+
agentName,
|
|
766
|
+
personality,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// ── Shared finalization (used by both terminal & web setup) ──
|
|
771
|
+
|
|
772
|
+
export interface SetupResult {
|
|
773
|
+
providers: Record<string, ProviderConfig>;
|
|
774
|
+
activeProvider: string;
|
|
775
|
+
failover: string[];
|
|
776
|
+
anthropicApiKey?: string;
|
|
777
|
+
telegramBotToken?: string;
|
|
778
|
+
channels: Record<string, any>;
|
|
779
|
+
smartRouting: { enabled: boolean; fastProvider?: string; fastModel?: string };
|
|
780
|
+
agentName: string;
|
|
781
|
+
personality: string;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
export async function finalizeSetup(result: SetupResult) {
|
|
785
|
+
const {
|
|
786
|
+
providers, activeProvider, failover, anthropicApiKey, telegramBotToken,
|
|
787
|
+
channels, smartRouting, agentName, personality,
|
|
788
|
+
} = result;
|
|
789
|
+
|
|
536
790
|
console.log("");
|
|
537
791
|
console.log(` ${DIM}─────────────────────────────────────────${RESET}`);
|
|
538
792
|
console.log(` ${BOLD}Setting up...${RESET}`);
|
|
@@ -590,8 +844,6 @@ export async function runSetup() {
|
|
|
590
844
|
ok("Memory file ready");
|
|
591
845
|
|
|
592
846
|
// Create SYSTEM.md only if the user customized the name or personality.
|
|
593
|
-
// Otherwise, let the built-in DEFAULT_PERSONALITY in prompts.ts be used —
|
|
594
|
-
// it has the full capability set and stays up-to-date with new features.
|
|
595
847
|
if (!existsSync(paths.systemPrompt)) {
|
|
596
848
|
if (agentName !== "Zubo" || personality) {
|
|
597
849
|
const nameLine = agentName !== "Zubo"
|
|
@@ -606,7 +858,6 @@ export async function runSetup() {
|
|
|
606
858
|
`${nameLine}${personalityLine}\n`
|
|
607
859
|
);
|
|
608
860
|
}
|
|
609
|
-
// If no customization, no SYSTEM.md is created — the default personality is used.
|
|
610
861
|
}
|
|
611
862
|
ok("System prompt ready");
|
|
612
863
|
|
package/src/start.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { ensureDirectories, paths } from "./config/paths";
|
|
|
5
5
|
import type { ZuboConfig } from "./config/schema";
|
|
6
6
|
import { getDb, closeDb } from "./db/connection";
|
|
7
7
|
import { runMigrations } from "./db/migrations";
|
|
8
|
-
import { createProvider } from "./llm/factory";
|
|
8
|
+
import { createProvider, validateProvider } from "./llm/factory";
|
|
9
9
|
import { registerDatetimeTool } from "./tools/builtin/datetime";
|
|
10
10
|
import { registerMemoryWriteTool } from "./tools/builtin/memory-write";
|
|
11
11
|
import { registerMemorySearchTool } from "./tools/builtin/memory-search";
|
|
@@ -108,7 +108,17 @@ export async function startZubo(isDaemon = false) {
|
|
|
108
108
|
await initMemory(db);
|
|
109
109
|
|
|
110
110
|
// Init LLM
|
|
111
|
-
const llm = createProvider(config);
|
|
111
|
+
const llm = await createProvider(config);
|
|
112
|
+
|
|
113
|
+
// Validate LLM connectivity (non-blocking — warn but don't prevent startup)
|
|
114
|
+
validateProvider(llm).then((err) => {
|
|
115
|
+
if (err) {
|
|
116
|
+
logger.warn(`LLM validation: ${err}`);
|
|
117
|
+
console.log(`\n ⚠ ${err}\n`);
|
|
118
|
+
} else {
|
|
119
|
+
logger.info("LLM connectivity verified");
|
|
120
|
+
}
|
|
121
|
+
}).catch(() => {});
|
|
112
122
|
|
|
113
123
|
// Init voice (STT/TTS) if configured
|
|
114
124
|
if (config.voice?.stt) {
|
|
@@ -113,7 +113,24 @@ export function registerConfigUpdateTool() {
|
|
|
113
113
|
const validated = configSchema.parse(configObj);
|
|
114
114
|
await saveConfig(validated);
|
|
115
115
|
logger.info(`Config updated: ${key} = ${JSON.stringify(value)}`);
|
|
116
|
-
|
|
116
|
+
|
|
117
|
+
// Hot-swap LLM provider if activeProvider changed
|
|
118
|
+
if (key === "activeProvider") {
|
|
119
|
+
try {
|
|
120
|
+
const { createProvider } = await import("../../llm/factory");
|
|
121
|
+
const newLlm = await createProvider(validated);
|
|
122
|
+
const router = (globalThis as any).__zuboRouter;
|
|
123
|
+
if (router?.setLlm) {
|
|
124
|
+
router.setLlm(newLlm);
|
|
125
|
+
return `Switched to ${value}. Active now — no restart needed.`;
|
|
126
|
+
}
|
|
127
|
+
} catch (swapErr: any) {
|
|
128
|
+
logger.warn("Provider hot-swap failed", { error: swapErr.message });
|
|
129
|
+
return `Config updated: activeProvider = ${JSON.stringify(value)}, but hot-swap failed: ${swapErr.message}. Restart to apply.`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return `Config updated: ${key} = ${JSON.stringify(value)}.`;
|
|
117
134
|
} catch (err: any) {
|
|
118
135
|
return `Error: Invalid config value. ${err.message}`;
|
|
119
136
|
}
|
package/src/tools/executor.ts
CHANGED
|
@@ -121,7 +121,7 @@ export async function executeTool(
|
|
|
121
121
|
logger.warn(`Tool blocked by allowedTools: ${name}`);
|
|
122
122
|
return {
|
|
123
123
|
tool_use_id: toolUseId,
|
|
124
|
-
content: `Error:
|
|
124
|
+
content: `Error: The '${name}' feature is not available right now.`,
|
|
125
125
|
is_error: true,
|
|
126
126
|
};
|
|
127
127
|
}
|
|
@@ -141,7 +141,7 @@ export async function executeTool(
|
|
|
141
141
|
logger.warn(`Tool denied: ${name}`);
|
|
142
142
|
return {
|
|
143
143
|
tool_use_id: toolUseId,
|
|
144
|
-
content: `Error:
|
|
144
|
+
content: `Error: The '${name}' feature is not permitted. You can change permissions in Settings.`,
|
|
145
145
|
is_error: true,
|
|
146
146
|
};
|
|
147
147
|
}
|
|
@@ -46,14 +46,16 @@ export async function searchRegistry(
|
|
|
46
46
|
if (cached) return cached;
|
|
47
47
|
|
|
48
48
|
try {
|
|
49
|
-
const url = `${BASE_URL}/servers?
|
|
49
|
+
const url = `${BASE_URL}/servers?search=${encodeURIComponent(query)}&limit=${limit}`;
|
|
50
50
|
const res = await fetch(url);
|
|
51
51
|
if (!res.ok) {
|
|
52
52
|
logger.warn(`MCP registry search failed: ${res.status} ${res.statusText}`);
|
|
53
53
|
return [];
|
|
54
54
|
}
|
|
55
55
|
const data = await res.json();
|
|
56
|
-
const
|
|
56
|
+
const raw: any[] = Array.isArray(data) ? data : data.servers ?? [];
|
|
57
|
+
// Registry wraps each entry in { server: {...}, _meta: {...} } — unwrap
|
|
58
|
+
const servers: any[] = raw.map((e: any) => e.server ?? e);
|
|
57
59
|
setCache(cacheKey, servers);
|
|
58
60
|
return servers;
|
|
59
61
|
} catch (err: any) {
|
|
@@ -83,8 +85,10 @@ export async function getServerDetail(name: string): Promise<any | null> {
|
|
|
83
85
|
return null;
|
|
84
86
|
}
|
|
85
87
|
const data = await res.json();
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
// Registry may wrap in { server: {...}, _meta: {...} } — unwrap
|
|
89
|
+
const server = data.server ?? data;
|
|
90
|
+
setCache(cacheKey, server);
|
|
91
|
+
return server;
|
|
88
92
|
} catch (err: any) {
|
|
89
93
|
logger.warn(`MCP registry detail error for "${name}"`, { error: err.message });
|
|
90
94
|
return null;
|
|
@@ -115,9 +119,11 @@ export async function listRegistry(
|
|
|
115
119
|
return { servers: [] };
|
|
116
120
|
}
|
|
117
121
|
const data = await res.json();
|
|
122
|
+
const raw: any[] = Array.isArray(data) ? data : data.servers ?? [];
|
|
123
|
+
// Registry wraps each entry in { server: {...}, _meta: {...} } — unwrap
|
|
118
124
|
const result = {
|
|
119
|
-
servers:
|
|
120
|
-
nextCursor: data.nextCursor as string | undefined,
|
|
125
|
+
servers: raw.map((e: any) => e.server ?? e),
|
|
126
|
+
nextCursor: (data.metadata?.nextCursor ?? data.nextCursor) as string | undefined,
|
|
121
127
|
};
|
|
122
128
|
setCache(cacheKey, result);
|
|
123
129
|
return result;
|
package/src/tools/permissions.ts
CHANGED
|
@@ -15,8 +15,8 @@ const DEFAULT_PERMISSIONS: Record<string, ToolPermission> = {
|
|
|
15
15
|
secret_list: "auto",
|
|
16
16
|
secret_delete: "confirm",
|
|
17
17
|
|
|
18
|
-
// Config
|
|
19
|
-
config_update: "
|
|
18
|
+
// Config — auto (tool has built-in guards: blocks secrets, security settings, validates via schema)
|
|
19
|
+
config_update: "auto",
|
|
20
20
|
connect_service: "confirm",
|
|
21
21
|
|
|
22
22
|
// Agent delegation — delegate is auto, but creating/managing agents requires confirmation
|