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.
@@ -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
- const baseUrl = await prompt(" Ollama URL [http://localhost:11434/v1]: ");
81
- const model = await prompt(" Model [llama3.3]: ");
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: baseUrl || "http://localhost:11434/v1",
145
+ baseUrl,
86
146
  apiKey: "ollama",
87
- model: model || "llama3.3",
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 = await prompt(" LM Studio URL [http://localhost:1234/v1]: ");
221
- const model = await prompt(" Model name: ");
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: baseUrl || "http://localhost:1234/v1",
335
+ baseUrl,
226
336
  apiKey: "lm-studio",
227
- model: model || "default",
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
- ok(`${result.name} configured (${result.config.model})`);
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
- ok(`${fb.name} added as fallback (${fb.config.model})`);
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
- return `Config updated: ${key} = ${JSON.stringify(value)}. Changes take effect on next restart (run \`zubo start\` again).`;
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
  }
@@ -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: Tool '${name}' is not available in this agent context.`,
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: Tool '${name}' is not permitted.`,
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?q=${encodeURIComponent(query)}&limit=${limit}`;
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 servers: any[] = Array.isArray(data) ? data : data.servers ?? [];
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
- setCache(cacheKey, data);
87
- return data;
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: Array.isArray(data) ? data : data.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;
@@ -15,8 +15,8 @@ const DEFAULT_PERMISSIONS: Record<string, ToolPermission> = {
15
15
  secret_list: "auto",
16
16
  secret_delete: "confirm",
17
17
 
18
- // Config & integrations require confirmation (can change system behavior)
19
- config_update: "confirm",
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