zubo 0.1.0 → 0.1.2

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,31 @@
1
+ name: Deploy to GitHub Pages
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths: [site/**]
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+ pages: write
12
+ id-token: write
13
+
14
+ concurrency:
15
+ group: pages
16
+ cancel-in-progress: true
17
+
18
+ jobs:
19
+ deploy:
20
+ runs-on: ubuntu-latest
21
+ environment:
22
+ name: github-pages
23
+ url: ${{ steps.deployment.outputs.page_url }}
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - uses: actions/configure-pages@v5
27
+ - uses: actions/upload-pages-artifact@v3
28
+ with:
29
+ path: site
30
+ - id: deployment
31
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,52 @@
1
+ # Contributing to Zubo
2
+
3
+ Thanks for your interest in contributing to Zubo! Whether you are fixing a bug, adding a feature, or writing a custom skill, we appreciate the help.
4
+
5
+ ## Getting Started
6
+
7
+ ```bash
8
+ git clone https://github.com/your-org/zubo.git
9
+ cd zubo
10
+ bun install
11
+ zubo setup
12
+ bun run dev
13
+ ```
14
+
15
+ ## Making Changes
16
+
17
+ 1. Create a branch off `main` for your work.
18
+ 2. Write your code. Zubo is built with Bun and TypeScript, so keep things typed.
19
+ 3. Run tests before opening a PR:
20
+ ```bash
21
+ bun test
22
+ ```
23
+ 4. Make sure the project type-checks cleanly:
24
+ ```bash
25
+ npx tsc --noEmit
26
+ ```
27
+
28
+ ## Skills
29
+
30
+ Zubo supports custom skills -- self-contained TypeScript handlers that extend what the agent can do. You can contribute new skills by adding them to `~/.zubo/workspace/skills/`. Each skill is a TypeScript file that exports a handler. If you have an idea for a skill, open an issue first so we can discuss the design.
31
+
32
+ ## Pull Requests
33
+
34
+ - Give your PR a clear, descriptive title.
35
+ - Explain what you changed and why.
36
+ - Make sure all tests pass and there are no type errors.
37
+ - Keep PRs focused. One concern per PR is easier to review.
38
+
39
+ ## Reporting Bugs
40
+
41
+ If you run into a problem, please open an issue and include:
42
+
43
+ - A short description of the bug.
44
+ - Steps to reproduce it.
45
+ - What you expected to happen vs. what actually happened.
46
+ - Your OS and Bun version, if relevant.
47
+
48
+ ## Documentation
49
+
50
+ For more details on how Zubo works, visit the docs: https://zubo.bot/docs
51
+
52
+ Thanks again for contributing.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="site/logo.svg" width="80" height="80" alt="Zubo">
2
+ <img src="https://raw.githubusercontent.com/apwn/zubo/main/site/logo.svg" width="80" height="80" alt="Zubo">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">Zubo</h1>
package/package.json CHANGED
@@ -1,6 +1,24 @@
1
1
  {
2
2
  "name": "zubo",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
+ "description": "Your AI agent that never forgets. Persistent memory, 20+ tools, 6 channels, 11+ LLM providers — runs entirely on your machine.",
5
+ "license": "MIT",
6
+ "author": "thomaskanze",
7
+ "homepage": "https://zubo.bot",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/apwn/zubo.git"
11
+ },
12
+ "keywords": [
13
+ "ai",
14
+ "agent",
15
+ "llm",
16
+ "chatbot",
17
+ "memory",
18
+ "tools",
19
+ "bun",
20
+ "typescript"
21
+ ],
4
22
  "type": "module",
5
23
  "bin": {
6
24
  "zubo": "src/index.ts"
package/site/install.sh CHANGED
@@ -1,98 +1,98 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- # Zubo Installer
5
- # Usage: curl -fsSL https://zubo.bot/install.sh | bash
6
-
7
- BOLD='\033[1m'
8
- DIM='\033[2m'
9
- GREEN='\033[32m'
10
- YELLOW='\033[33m'
11
- RED='\033[31m'
12
- CYAN='\033[36m'
13
- RESET='\033[0m'
14
-
15
- info() { echo -e "${CYAN}${BOLD}→${RESET} $1"; }
16
- ok() { echo -e "${GREEN}${BOLD}✓${RESET} $1"; }
17
- warn() { echo -e "${YELLOW}${BOLD}!${RESET} $1"; }
18
- fail() { echo -e "${RED}${BOLD}✗${RESET} $1"; exit 1; }
19
-
20
- echo ""
21
- echo -e "${BOLD} zubo installer${RESET}"
22
- echo -e "${DIM} The AI agent that never forgets${RESET}"
23
- echo ""
24
-
25
- # --- OS detection ---
26
- OS="$(uname -s)"
27
- ARCH="$(uname -m)"
28
-
29
- case "$OS" in
30
- Linux*) PLATFORM="linux" ;;
31
- Darwin*) PLATFORM="darwin" ;;
32
- *) fail "Unsupported OS: $OS. Zubo supports macOS and Linux." ;;
33
- esac
34
-
35
- case "$ARCH" in
36
- x86_64|amd64) ARCH="x64" ;;
37
- arm64|aarch64) ARCH="aarch64" ;;
38
- *) fail "Unsupported architecture: $ARCH" ;;
39
- esac
40
-
41
- info "Detected ${PLATFORM}/${ARCH}"
42
-
43
- # --- Install Bun if missing ---
44
- if command -v bun &>/dev/null; then
45
- BUN_VERSION=$(bun --version 2>/dev/null || echo "unknown")
46
- ok "Bun already installed (v${BUN_VERSION})"
47
- else
48
- info "Installing Bun runtime..."
49
- curl -fsSL https://bun.sh/install | bash
50
-
51
- # Source the updated profile so bun is on PATH
52
- export BUN_INSTALL="${HOME}/.bun"
53
- export PATH="${BUN_INSTALL}/bin:${PATH}"
54
-
55
- if command -v bun &>/dev/null; then
56
- ok "Bun installed (v$(bun --version))"
57
- else
58
- fail "Bun installation failed. Install manually: https://bun.sh"
59
- fi
60
- fi
61
-
62
- # --- Install Zubo ---
63
- info "Installing Zubo..."
64
-
65
- if bun add -g zubo 2>/dev/null; then
66
- ok "Zubo installed globally"
67
- else
68
- warn "Global install failed, trying with sudo..."
69
- sudo bun add -g zubo || fail "Installation failed. Try: bun add -g zubo"
70
- ok "Zubo installed globally (with sudo)"
71
- fi
72
-
73
- # --- Verify ---
74
- if command -v zubo &>/dev/null; then
75
- ZUBO_VERSION=$(zubo --version 2>/dev/null || echo "installed")
76
- ok "Zubo is ready (${ZUBO_VERSION})"
77
- else
78
- # Bun global bin might not be on PATH yet
79
- ZUBO_BIN="${HOME}/.bun/bin/zubo"
80
- if [ -f "$ZUBO_BIN" ]; then
81
- warn "Zubo installed but not on PATH. Add this to your shell profile:"
82
- echo ""
83
- echo -e " ${DIM}export PATH=\"\$HOME/.bun/bin:\$PATH\"${RESET}"
84
- echo ""
85
- else
86
- fail "Zubo binary not found after install"
87
- fi
88
- fi
89
-
90
- echo ""
91
- echo -e "${BOLD} Next steps:${RESET}"
92
- echo ""
93
- echo -e " ${CYAN}1.${RESET} Run the setup wizard: ${BOLD}zubo setup${RESET}"
94
- echo -e " ${CYAN}2.${RESET} Start your agent: ${BOLD}zubo start${RESET}"
95
- echo -e " ${CYAN}3.${RESET} Open the dashboard: ${BOLD}zubo dashboard${RESET}"
96
- echo ""
97
- echo -e " ${DIM}Docs: https://zubo.bot/docs${RESET}"
98
- echo ""
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Zubo Installer
5
+ # Usage: curl -fsSL https://zubo.bot/install.sh | bash
6
+
7
+ BOLD='\033[1m'
8
+ DIM='\033[2m'
9
+ GREEN='\033[32m'
10
+ YELLOW='\033[33m'
11
+ RED='\033[31m'
12
+ CYAN='\033[36m'
13
+ RESET='\033[0m'
14
+
15
+ info() { echo -e "${CYAN}${BOLD}→${RESET} $1"; }
16
+ ok() { echo -e "${GREEN}${BOLD}✓${RESET} $1"; }
17
+ warn() { echo -e "${YELLOW}${BOLD}!${RESET} $1"; }
18
+ fail() { echo -e "${RED}${BOLD}✗${RESET} $1"; exit 1; }
19
+
20
+ echo ""
21
+ echo -e "${BOLD} zubo installer${RESET}"
22
+ echo -e "${DIM} The AI agent that never forgets${RESET}"
23
+ echo ""
24
+
25
+ # --- OS detection ---
26
+ OS="$(uname -s)"
27
+ ARCH="$(uname -m)"
28
+
29
+ case "$OS" in
30
+ Linux*) PLATFORM="linux" ;;
31
+ Darwin*) PLATFORM="darwin" ;;
32
+ *) fail "Unsupported OS: $OS. Zubo supports macOS and Linux." ;;
33
+ esac
34
+
35
+ case "$ARCH" in
36
+ x86_64|amd64) ARCH="x64" ;;
37
+ arm64|aarch64) ARCH="aarch64" ;;
38
+ *) fail "Unsupported architecture: $ARCH" ;;
39
+ esac
40
+
41
+ info "Detected ${PLATFORM}/${ARCH}"
42
+
43
+ # --- Install Bun if missing ---
44
+ if command -v bun &>/dev/null; then
45
+ BUN_VERSION=$(bun --version 2>/dev/null || echo "unknown")
46
+ ok "Bun already installed (v${BUN_VERSION})"
47
+ else
48
+ info "Installing Bun runtime..."
49
+ curl -fsSL https://bun.sh/install | bash
50
+
51
+ # Source the updated profile so bun is on PATH
52
+ export BUN_INSTALL="${HOME}/.bun"
53
+ export PATH="${BUN_INSTALL}/bin:${PATH}"
54
+
55
+ if command -v bun &>/dev/null; then
56
+ ok "Bun installed (v$(bun --version))"
57
+ else
58
+ fail "Bun installation failed. Install manually: https://bun.sh"
59
+ fi
60
+ fi
61
+
62
+ # --- Install Zubo ---
63
+ info "Installing Zubo..."
64
+
65
+ if bun add -g zubo 2>/dev/null; then
66
+ ok "Zubo installed globally"
67
+ else
68
+ warn "Global install failed, trying with sudo..."
69
+ sudo bun add -g zubo || fail "Installation failed. Try: bun add -g zubo"
70
+ ok "Zubo installed globally (with sudo)"
71
+ fi
72
+
73
+ # --- Verify ---
74
+ if command -v zubo &>/dev/null; then
75
+ ZUBO_VERSION=$(zubo --version 2>/dev/null || echo "installed")
76
+ ok "Zubo is ready (${ZUBO_VERSION})"
77
+ else
78
+ # Bun global bin might not be on PATH yet
79
+ ZUBO_BIN="${HOME}/.bun/bin/zubo"
80
+ if [ -f "$ZUBO_BIN" ]; then
81
+ warn "Zubo installed but not on PATH. Add this to your shell profile:"
82
+ echo ""
83
+ echo -e " ${DIM}export PATH=\"\$HOME/.bun/bin:\$PATH\"${RESET}"
84
+ echo ""
85
+ else
86
+ fail "Zubo binary not found after install"
87
+ fi
88
+ fi
89
+
90
+ echo ""
91
+ echo -e "${BOLD} Next steps:${RESET}"
92
+ echo ""
93
+ echo -e " ${CYAN}1.${RESET} Run the setup wizard: ${BOLD}zubo setup${RESET}"
94
+ echo -e " ${CYAN}2.${RESET} Start your agent: ${BOLD}zubo start${RESET}"
95
+ echo -e " ${CYAN}3.${RESET} Open the dashboard: ${BOLD}zubo dashboard${RESET}"
96
+ echo ""
97
+ echo -e " ${DIM}Docs: https://zubo.bot/docs${RESET}"
98
+ echo ""
@@ -196,7 +196,7 @@ function getConfigInfo(): {
196
196
  function switchModelConfig(provider: string, model: string): { ok: boolean; error?: string } {
197
197
  try {
198
198
  if (!model) {
199
- return { ok: false, error: "Model is required" };
199
+ return { ok: false, error: "Model name is required. Use the format: provider/model (e.g. anthropic/claude-sonnet-4-5-20250929)" };
200
200
  }
201
201
 
202
202
  const config = JSON.parse(readFileSync(paths.config, "utf-8"));
@@ -297,7 +297,7 @@ function handleDashboardApi(url: URL, req: Request): Response | null {
297
297
  return (async () => {
298
298
  const body = (await req.json()) as { provider?: string; model?: string };
299
299
  if (!body.provider) {
300
- return Response.json({ ok: false, error: "provider is required" }, { status: 400 });
300
+ return Response.json({ ok: false, error: "Provider name is required." }, { status: 400 });
301
301
  }
302
302
  const result = switchModelConfig(body.provider, body.model ?? "");
303
303
  return Response.json(result, { status: result.ok ? 200 : 400 });
@@ -1443,7 +1443,7 @@ export function createWebChatAdapter(
1443
1443
  const check = chatLimiter.check(ip);
1444
1444
  if (!check.allowed) {
1445
1445
  return Response.json(
1446
- { error: "Rate limit exceeded" },
1446
+ { error: "Too many requests. Please wait a moment and try again." },
1447
1447
  { status: 429, headers: { "Retry-After": String(Math.ceil((check.retryAfterMs ?? 1000) / 1000)) } }
1448
1448
  );
1449
1449
  }
@@ -1455,7 +1455,7 @@ export function createWebChatAdapter(
1455
1455
  const check = uploadLimiter.check(ip);
1456
1456
  if (!check.allowed) {
1457
1457
  return Response.json(
1458
- { error: "Rate limit exceeded" },
1458
+ { error: "Too many requests. Please wait a moment and try again." },
1459
1459
  { status: 429, headers: { "Retry-After": String(Math.ceil((check.retryAfterMs ?? 1000) / 1000)) } }
1460
1460
  );
1461
1461
  }
@@ -1695,7 +1695,7 @@ export function createWebChatAdapter(
1695
1695
 
1696
1696
  const stt = getSttProvider();
1697
1697
  if (!stt) {
1698
- return Response.json({ error: "STT not configured" }, { status: 400 });
1698
+ return Response.json({ error: "Voice not configured. Add voice settings to your config:\n\nzubo config set voice.stt.provider whisper\nzubo config set voice.stt.apiKey YOUR_OPENAI_API_KEY\n\nThen restart Zubo." }, { status: 400 });
1699
1699
  }
1700
1700
 
1701
1701
  // Transcribe
package/src/model.ts CHANGED
@@ -4,7 +4,7 @@ import { configExists } from "./config/loader";
4
4
 
5
5
  function readConfig(): any {
6
6
  if (!configExists()) {
7
- console.log("Config not found. Run 'zubo setup' first.");
7
+ console.log("No config found. Run 'zubo setup' to get started.");
8
8
  process.exit(1);
9
9
  }
10
10
  return JSON.parse(readFileSync(paths.config, "utf-8"));
@@ -33,7 +33,7 @@ function showCurrent() {
33
33
  } else if (config.anthropicApiKey) {
34
34
  console.log(`\n Active: anthropic/${config.model ?? "claude-sonnet-4-5-20250929"} (legacy config)`);
35
35
  } else {
36
- console.log("\n No provider configured.");
36
+ console.log("\n No provider configured. Run 'zubo setup' to add one.");
37
37
  }
38
38
  console.log("");
39
39
  }
@@ -95,8 +95,7 @@ function switchModel(target: string) {
95
95
  }
96
96
 
97
97
  // Provider not configured — offer quick setup for known providers
98
- console.log(`Provider "${providerName}" is not configured.`);
99
- console.log(`Run 'zubo setup' to add it, or edit ~/.zubo/config.json directly.`);
98
+ console.log(`Provider "${providerName}" is not configured. Run 'zubo setup' to add it.`);
100
99
  process.exit(1);
101
100
  }
102
101
 
@@ -3,7 +3,7 @@ const API = "https://gmail.googleapis.com/gmail/v1/users/me";
3
3
  async function getToken(): Promise<string> {
4
4
  const Zubo = (globalThis as any).Zubo;
5
5
  if (Zubo?.getGoogleToken) return Zubo.getGoogleToken();
6
- throw new Error("Google is NOT connected. Use google_oauth with action 'start' to set up Google.");
6
+ throw new Error("Google is not connected. Ask me to set up Google and I'll walk you through it.");
7
7
  }
8
8
 
9
9
  async function safeApiErr(res: Response, service: string): Promise<string> {
@@ -28,8 +28,8 @@ export default async function (input: Record<string, unknown>): Promise<string>
28
28
  } catch (err: any) {
29
29
  return JSON.stringify({
30
30
  error: err.message,
31
- action_required: "Google is NOT connected. The user needs to complete the OAuth 2.0 flow. " +
32
- "Ask the user for their Google OAuth client_id (ends with .apps.googleusercontent.com) " +
31
+ action_required: "Google is not connected. The user needs to set up Google OAuth. " +
32
+ "Ask them for their Google OAuth client_id (ends with .apps.googleusercontent.com) " +
33
33
  "and client_secret (starts with GOCSPX-), then use google_oauth tool with action 'start'.",
34
34
  });
35
35
  }
@@ -3,7 +3,7 @@ const API = "https://www.googleapis.com/calendar/v3";
3
3
  async function getToken(): Promise<string> {
4
4
  const Zubo = (globalThis as any).Zubo;
5
5
  if (Zubo?.getGoogleToken) return Zubo.getGoogleToken();
6
- throw new Error("Google is NOT connected. Use google_oauth with action 'start' to set up Google.");
6
+ throw new Error("Google is not connected. Ask me to set up Google and I'll walk you through it.");
7
7
  }
8
8
 
9
9
  function safeApiErr(status: number, statusText: string, service: string): string {
@@ -17,8 +17,8 @@ export default async function (input: Record<string, unknown>): Promise<string>
17
17
  } catch (err: any) {
18
18
  return JSON.stringify({
19
19
  error: err.message,
20
- action_required: "Google is NOT connected. The user needs to complete the OAuth 2.0 flow. " +
21
- "Ask the user for their Google OAuth client_id (ends with .apps.googleusercontent.com) " +
20
+ action_required: "Google is not connected. The user needs to set up Google OAuth. " +
21
+ "Ask them for their Google OAuth client_id (ends with .apps.googleusercontent.com) " +
22
22
  "and client_secret (starts with GOCSPX-), then use google_oauth tool with action 'start'.",
23
23
  });
24
24
  }
@@ -3,7 +3,7 @@ const API = "https://docs.googleapis.com/v1/documents";
3
3
  async function getToken(): Promise<string> {
4
4
  const Zubo = (globalThis as any).Zubo;
5
5
  if (Zubo?.getGoogleToken) return Zubo.getGoogleToken();
6
- throw new Error("Google is NOT connected. Use google_oauth with action 'start' to set up Google.");
6
+ throw new Error("Google is not connected. Ask me to set up Google and I'll walk you through it.");
7
7
  }
8
8
 
9
9
  function safeApiErr(status: number, statusText: string, service: string): string {
@@ -17,8 +17,8 @@ export default async function (input: Record<string, unknown>): Promise<string>
17
17
  } catch (err: any) {
18
18
  return JSON.stringify({
19
19
  error: err.message,
20
- action_required: "Google is NOT connected. The user needs to complete the OAuth 2.0 flow. " +
21
- "Ask the user for their Google OAuth client_id (ends with .apps.googleusercontent.com) " +
20
+ action_required: "Google is not connected. The user needs to set up Google OAuth. " +
21
+ "Ask them for their Google OAuth client_id (ends with .apps.googleusercontent.com) " +
22
22
  "and client_secret (starts with GOCSPX-), then use google_oauth tool with action 'start'.",
23
23
  });
24
24
  }
@@ -3,7 +3,7 @@ const API = "https://www.googleapis.com/drive/v3";
3
3
  async function getToken(): Promise<string> {
4
4
  const Zubo = (globalThis as any).Zubo;
5
5
  if (Zubo?.getGoogleToken) return Zubo.getGoogleToken();
6
- throw new Error("Google is NOT connected. Use google_oauth with action 'start' to set up Google.");
6
+ throw new Error("Google is not connected. Ask me to set up Google and I'll walk you through it.");
7
7
  }
8
8
 
9
9
  function safeApiErr(status: number, statusText: string, service: string): string {
@@ -17,8 +17,8 @@ export default async function (input: Record<string, unknown>): Promise<string>
17
17
  } catch (err: any) {
18
18
  return JSON.stringify({
19
19
  error: err.message,
20
- action_required: "Google is NOT connected. The user needs to complete the OAuth 2.0 flow. " +
21
- "Ask the user for their Google OAuth client_id (ends with .apps.googleusercontent.com) " +
20
+ action_required: "Google is not connected. The user needs to set up Google OAuth. " +
21
+ "Ask them for their Google OAuth client_id (ends with .apps.googleusercontent.com) " +
22
22
  "and client_secret (starts with GOCSPX-), then use google_oauth tool with action 'start'.",
23
23
  });
24
24
  }
@@ -3,7 +3,7 @@ const API = "https://sheets.googleapis.com/v4/spreadsheets";
3
3
  async function getToken(): Promise<string> {
4
4
  const Zubo = (globalThis as any).Zubo;
5
5
  if (Zubo?.getGoogleToken) return Zubo.getGoogleToken();
6
- throw new Error("Google is NOT connected. Use google_oauth with action 'start' to set up Google.");
6
+ throw new Error("Google is not connected. Ask me to set up Google and I'll walk you through it.");
7
7
  }
8
8
 
9
9
  function safeApiErr(status: number, statusText: string, service: string): string {
@@ -17,8 +17,8 @@ export default async function (input: Record<string, unknown>): Promise<string>
17
17
  } catch (err: any) {
18
18
  return JSON.stringify({
19
19
  error: err.message,
20
- action_required: "Google is NOT connected. The user needs to complete the OAuth 2.0 flow. " +
21
- "Ask the user for their Google OAuth client_id (ends with .apps.googleusercontent.com) " +
20
+ action_required: "Google is not connected. The user needs to set up Google OAuth. " +
21
+ "Ask them for their Google OAuth client_id (ends with .apps.googleusercontent.com) " +
22
22
  "and client_secret (starts with GOCSPX-), then use google_oauth tool with action 'start'.",
23
23
  });
24
24
  }
@@ -14,7 +14,7 @@ export default async function (input: Record<string, unknown>): Promise<string>
14
14
  const email = (globalThis as any).Zubo?.getSecret?.("jira_email");
15
15
  const baseUrl = (globalThis as any).Zubo?.getSecret?.("jira_url");
16
16
  if (!token || !email || !baseUrl) {
17
- return JSON.stringify({ error: "Jira not configured. Set jira_token, jira_email, and jira_url secrets." });
17
+ return JSON.stringify({ error: "Jira is not connected. Tell me your Jira details and I'll set it up. I need: your Jira URL (e.g. yourteam.atlassian.net), email, and an API token from id.atlassian.net/manage-profile/security/api-tokens." });
18
18
  }
19
19
 
20
20
  // Validate jira_url to prevent SSRF — must be HTTPS and a valid Jira host
@@ -22,7 +22,7 @@ async function gql(token: string, query: string, variables?: any): Promise<any>
22
22
 
23
23
  export default async function (input: Record<string, unknown>): Promise<string> {
24
24
  const token = (globalThis as any).Zubo?.getSecret?.("linear_token");
25
- if (!token) return JSON.stringify({ error: "Linear token not configured. Use secret_set to store 'linear_token'." });
25
+ if (!token) return JSON.stringify({ error: "Linear is not connected. Tell me your Linear API key and I'll set it up. You can create one at linear.app/settings/api." });
26
26
 
27
27
  const { action, issue_id, title, description, team_id, query, assignee_id, priority } = input as {
28
28
  action: string; issue_id?: string; title?: string; description?: string;
@@ -14,7 +14,7 @@ const API = "https://slack.com/api";
14
14
  export default async function (input: Record<string, unknown>): Promise<string> {
15
15
  const token = (globalThis as any).Zubo?.getSecret?.("slack_token");
16
16
  if (!token) {
17
- return JSON.stringify({ error: "Slack token not configured. Use secret_set to store 'slack_token'." });
17
+ return JSON.stringify({ error: "Slack is not connected. Tell me your Slack Bot Token (starts with xoxb-) and I'll set it up." });
18
18
  }
19
19
 
20
20
  const { action, channel, text, query, limit } = input as {
@@ -14,7 +14,7 @@ const API = "https://api.twitter.com/2";
14
14
  export default async function (input: Record<string, unknown>): Promise<string> {
15
15
  const bearer = (globalThis as any).Zubo?.getSecret?.("twitter_bearer_token");
16
16
  if (!bearer) {
17
- return JSON.stringify({ error: "Twitter bearer token not configured. Use secret_set to store 'twitter_bearer_token'." });
17
+ return JSON.stringify({ error: "Twitter is not connected. Tell me your Twitter Bearer Token and I'll set it up. You can get one from developer.x.com." });
18
18
  }
19
19
 
20
20
  const { action, text, query, tweet_id, max_results } = input as {
@@ -51,7 +51,7 @@ export default async function (input: Record<string, unknown>): Promise<string>
51
51
  const accessToken = (globalThis as any).Zubo?.getSecret?.("twitter_access_token");
52
52
  const accessSecret = (globalThis as any).Zubo?.getSecret?.("twitter_access_secret");
53
53
  if (!apiKey || !apiSecret || !accessToken || !accessSecret) {
54
- return JSON.stringify({ error: "Twitter OAuth credentials not configured. Need twitter_api_key, twitter_api_secret, twitter_access_token, twitter_access_secret." });
54
+ return JSON.stringify({ error: "Twitter posting requires OAuth credentials. Tell me your API Key, API Secret, Access Token, and Access Secret from developer.x.com and I'll set them up." });
55
55
  }
56
56
  // Use OAuth 1.0a -- simplified via fetch with pre-computed header
57
57
  const oauthHeader = computeOAuth1Header("POST", `${API}/tweets`, {}, apiKey, apiSecret, accessToken, accessSecret);
@@ -26,7 +26,7 @@ function validateUrl(raw: string): void {
26
26
  try {
27
27
  parsed = new URL(raw);
28
28
  } catch {
29
- throw new Error(`Invalid URL: ${raw}`);
29
+ throw new Error(`Invalid URL: "${raw}". Make sure it starts with http:// or https://`);
30
30
  }
31
31
 
32
32
  if (isBlockedHost(parsed.hostname)) {
@@ -57,7 +57,7 @@ export default async function (input: Record<string, unknown>): Promise<string>
57
57
  // Block obviously destructive commands
58
58
  for (const pattern of BLOCKED_PATTERNS) {
59
59
  if (pattern.test(command)) {
60
- throw new Error(`Blocked: command matches dangerous pattern`);
60
+ throw new Error(`Blocked: this command is not allowed for safety reasons. Destructive system commands (rm -rf /, mkfs, shutdown, etc.) are restricted.`);
61
61
  }
62
62
  }
63
63
 
@@ -12,7 +12,7 @@ export default async function (input: Record<string, unknown>): Promise<string>
12
12
  });
13
13
 
14
14
  if (!res.ok) {
15
- throw new Error(`Fetch failed: ${res.status} ${res.statusText}`);
15
+ throw new Error(`Could not fetch URL (${res.status}). The page may be down or blocking requests.`);
16
16
  }
17
17
 
18
18
  const contentType = res.headers.get("content-type") || "";
@@ -12,7 +12,7 @@ export default async function (input: Record<string, unknown>): Promise<string>
12
12
  });
13
13
 
14
14
  if (!res.ok) {
15
- throw new Error(`Search failed: ${res.status} ${res.statusText}`);
15
+ throw new Error(`Search failed (${res.status}). Try again in a moment.`);
16
16
  }
17
17
 
18
18
  const html = await res.text();
package/src/voice/stt.ts CHANGED
@@ -48,7 +48,7 @@ export function initStt(config: { provider: string; apiKey: string; model?: stri
48
48
  logger.info("STT initialized: Whisper API");
49
49
  break;
50
50
  default:
51
- logger.warn(`Unknown STT provider: ${config.provider}`);
51
+ logger.warn(`Unknown STT provider: "${config.provider}". Supported: whisper, openai`);
52
52
  }
53
53
  }
54
54
 
package/src/voice/tts.ts CHANGED
@@ -94,7 +94,7 @@ export function initTts(config: { provider: string; apiKey: string; voice?: stri
94
94
  logger.info("TTS initialized: ElevenLabs");
95
95
  break;
96
96
  default:
97
- logger.warn(`Unknown TTS provider: ${config.provider}`);
97
+ logger.warn(`Unknown TTS provider: "${config.provider}". Supported: openai, elevenlabs`);
98
98
  }
99
99
  }
100
100