zubo 0.1.4 → 0.1.5

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/.dockerignore ADDED
@@ -0,0 +1,12 @@
1
+ node_modules
2
+ dist
3
+ desktop
4
+ site
5
+ .env
6
+ .env.*
7
+ .zubo
8
+ .git
9
+ .DS_Store
10
+ *.db
11
+ *.db-wal
12
+ *.db-shm
package/Dockerfile ADDED
@@ -0,0 +1,24 @@
1
+ # Zubo — Dockerfile
2
+ # Runs the Zubo AI agent using the official Bun runtime image.
3
+ # Persistent data (config, database, memory, skills) lives in /root/.zubo
4
+ # and should be mounted as a volume.
5
+
6
+ FROM oven/bun:1
7
+
8
+ WORKDIR /app
9
+
10
+ # Install dependencies first (layer cache optimisation)
11
+ COPY package.json bun.lock ./
12
+ RUN bun install --frozen-lockfile
13
+
14
+ # Copy the rest of the project
15
+ COPY . .
16
+
17
+ # Webchat dashboard port
18
+ EXPOSE 8787
19
+
20
+ # Persistent storage mount point — config.json, zubo.db, workspace, logs, etc.
21
+ VOLUME /root/.zubo
22
+
23
+ # Start the agent
24
+ CMD ["bun", "run", "src/index.ts", "start"]
@@ -0,0 +1,20 @@
1
+ # Zubo — Docker Compose
2
+ # Usage:
3
+ # 1. Copy .env.example to .env and fill in your API keys
4
+ # 2. docker compose up -d
5
+ # 3. Open http://localhost:8787 for the webchat dashboard
6
+
7
+ services:
8
+ zubo:
9
+ build: .
10
+ container_name: zubo
11
+ restart: unless-stopped
12
+
13
+ ports:
14
+ - "8787:8787" # Webchat dashboard
15
+
16
+ volumes:
17
+ - ~/.zubo:/root/.zubo # Persistent data (config, DB, memory, skills, logs)
18
+
19
+ env_file:
20
+ - .env # API keys: ANTHROPIC_API_KEY, OPENAI_API_KEY, TELEGRAM_BOT_TOKEN, etc.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zubo",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Your AI agent that never forgets. Persistent memory, 20+ tools, 6 channels, 11+ LLM providers — runs entirely on your machine.",
5
5
  "license": "MIT",
6
6
  "author": "thomaskanze",
@@ -4,18 +4,18 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Getting Started — Zubo Docs</title>
7
- <meta name="description" content="Get started with Zubo, a self-hosted AI agent with persistent semantic memory, multi-channel presence, 35+ tools, sub-agent delegation, and workflow automation.">
7
+ <meta name="description" content="Get started with Zubo, a self-hosted AI agent with persistent semantic memory, multi-channel presence, 20+ tools, sub-agent delegation, and workflow automation.">
8
8
  <meta name="theme-color" content="#060608">
9
9
  <link rel="canonical" href="https://zubo.bot/docs/">
10
10
  <meta property="og:title" content="Getting Started — Zubo Docs">
11
- <meta property="og:description" content="Get started with Zubo, a self-hosted AI agent with persistent semantic memory, multi-channel presence, 35+ tools, sub-agent delegation, and workflow automation.">
11
+ <meta property="og:description" content="Get started with Zubo, a self-hosted AI agent with persistent semantic memory, multi-channel presence, 20+ tools, sub-agent delegation, and workflow automation.">
12
12
  <meta property="og:type" content="article">
13
13
  <meta property="og:url" content="https://zubo.bot/docs/">
14
14
  <meta property="og:image" content="https://zubo.bot/og-image.png">
15
15
  <meta property="og:site_name" content="Zubo">
16
16
  <meta name="twitter:card" content="summary_large_image">
17
17
  <meta name="twitter:title" content="Getting Started — Zubo Docs">
18
- <meta name="twitter:description" content="Get started with Zubo, a self-hosted AI agent with persistent semantic memory, 35+ tools, and workflow automation.">
18
+ <meta name="twitter:description" content="Get started with Zubo, a self-hosted AI agent with persistent semantic memory, 20+ tools, and workflow automation.">
19
19
  <meta name="twitter:image" content="https://zubo.bot/og-image.png">
20
20
  <meta name="twitter:creator" content="@thomaskanze">
21
21
  <link rel="preconnect" href="https://fonts.googleapis.com">
@@ -87,7 +87,7 @@
87
87
  <h1>Getting Started with Zubo</h1>
88
88
 
89
89
  <p>
90
- Zubo is a self-hosted AI agent with persistent semantic memory, multi-channel presence across Telegram, Discord, Slack, WhatsApp, Signal, and Web Chat, over 35 smart built-in tools, sub-agent delegation, and workflow automation. It runs entirely on your machine &mdash; your data never leaves your infrastructure. Whether you need a personal assistant that remembers everything, an automation hub that connects your services, or an AI copilot embedded in every chat platform you use, Zubo handles it from a single process with a single configuration file.
90
+ Zubo is a self-hosted AI agent with persistent semantic memory, multi-channel presence across Telegram, Discord, Slack, WhatsApp, Signal, and Web Chat, 20+ smart built-in tools, sub-agent delegation, and workflow automation. It runs entirely on your machine &mdash; your data never leaves your infrastructure. Whether you need a personal assistant that remembers everything, an automation hub that connects your services, or an AI copilot embedded in every chat platform you use, Zubo handles it from a single process with a single configuration file.
91
91
  </p>
92
92
 
93
93
  <!-- ================================================================ -->
@@ -125,7 +125,7 @@
125
125
 
126
126
  <h3>Tools</h3>
127
127
  <ul>
128
- <li><strong>35+ built-in tools</strong> including shell command execution, file I/O (read, write, list, search), web search (DuckDuckGo, Brave, Google), HTTP requests, memory read/write/search, secrets management, and cron job scheduling.</li>
128
+ <li><strong>20+ built-in tools</strong> including shell command execution, file I/O (read, write, list, search), web search (DuckDuckGo, Brave, Google), HTTP requests, memory read/write/search, secrets management, and cron job scheduling.</li>
129
129
  <li>Tools are permission-gated with three levels: <code>safe</code>, <code>sensitive</code>, and <code>dangerous</code>. Dangerous operations require explicit confirmation tokens.</li>
130
130
  </ul>
131
131
 
@@ -200,21 +200,69 @@
200
200
  <!-- ================================================================ -->
201
201
  <h2 id="installation">Installation</h2>
202
202
 
203
+ <p>Choose the installation method that fits your platform.</p>
204
+
205
+ <h3 id="install-mac-linux">macOS &amp; Linux</h3>
206
+
203
207
  <p>Zubo requires <strong>Bun v1.0+</strong> as its runtime. If you do not have Bun installed, visit <a href="https://bun.sh" target="_blank" rel="noopener">bun.sh</a> and run the one-line installer.</p>
204
208
 
205
- <h3>Step 1: Install Zubo</h3>
209
+ <p><strong>One-liner (recommended):</strong></p>
210
+
211
+ <pre><code>curl -fsSL https://zubo.bot/install.sh | bash</code></pre>
206
212
 
207
- <p>Install Zubo globally using Bun (recommended) or npm:</p>
213
+ <p>This installs Bun (if needed), installs Zubo globally, and runs the setup wizard automatically.</p>
214
+
215
+ <p><strong>Manual install:</strong></p>
208
216
 
209
217
  <pre><code># Using Bun (recommended)
210
218
  bun add -g zubo
211
219
 
212
220
  # Or using npm
213
- npm i -g zubo</code></pre>
221
+ npm i -g zubo
222
+
223
+ # Then run the setup wizard
224
+ zubo setup</code></pre>
225
+
226
+ <h3 id="install-windows">Windows (via WSL)</h3>
227
+
228
+ <p>Zubo runs on Windows through the Windows Subsystem for Linux (WSL). This gives you full compatibility with zero workarounds.</p>
229
+
230
+ <p><strong>Step 1:</strong> Install WSL (requires Windows 10 version 2004+ or Windows 11):</p>
231
+
232
+ <pre><code>wsl --install</code></pre>
233
+
234
+ <p>Restart your computer when prompted. This installs Ubuntu by default.</p>
235
+
236
+ <p><strong>Step 2:</strong> Open the Ubuntu terminal from the Start Menu and install Zubo:</p>
237
+
238
+ <pre><code>curl -fsSL https://zubo.bot/install.sh | bash</code></pre>
239
+
240
+ <p>That's it. Zubo runs inside WSL with full access to all features. The web dashboard is accessible from your Windows browser at <code>http://localhost:3000</code>.</p>
241
+
242
+ <h3 id="install-docker">Docker</h3>
243
+
244
+ <p>Run Zubo as a container with no local dependencies.</p>
245
+
246
+ <pre><code># Clone the repo
247
+ git clone https://github.com/apwn/zubo.git &amp;&amp; cd zubo
248
+
249
+ # Copy the example env and add your API key
250
+ cp .env.example .env
251
+
252
+ # Start Zubo
253
+ docker compose up -d</code></pre>
254
+
255
+ <p>The container exposes port <code>8787</code> for the web dashboard and mounts <code>~/.zubo</code> for persistent data. Stop with <code>docker compose down</code>.</p>
256
+
257
+ <p>To configure, edit <code>.env</code> or mount your own <code>config.json</code>:</p>
258
+
259
+ <pre><code>volumes:
260
+ - ~/.zubo:/root/.zubo
261
+ - ./my-config.json:/root/.zubo/config.json</code></pre>
214
262
 
215
- <p>This places the <code>zubo</code> binary on your PATH, making it available from any terminal session.</p>
263
+ <h3 id="setup-wizard">The Setup Wizard</h3>
216
264
 
217
- <h3>Step 2: Run the Setup Wizard</h3>
265
+ <p>After installing (on any platform), run the setup wizard:</p>
218
266
 
219
267
  <pre><code>zubo setup</code></pre>
220
268
 
@@ -228,7 +276,7 @@ npm i -g zubo</code></pre>
228
276
  <li>Create a default <code>SYSTEM.md</code> system prompt that you can customize later.</li>
229
277
  </ul>
230
278
 
231
- <h3>Step 3: Start Zubo</h3>
279
+ <h3 id="starting">Starting Zubo</h3>
232
280
 
233
281
  <pre><code># Start in foreground (logs to stdout)
234
282
  zubo start
package/site/index.html CHANGED
@@ -440,86 +440,179 @@
440
440
  <h2 class="section-title">Running in <span class="gradient-text">60 seconds</span>.</h2>
441
441
  </div>
442
442
 
443
- <div class="qs-terminal glow-border reveal">
444
- <div class="qs-chrome">
445
- <div class="terminal-dots">
446
- <span class="td td-r"></span>
447
- <span class="td td-y"></span>
448
- <span class="td td-g"></span>
449
- </div>
450
- <div class="qs-tabs">
451
- <button class="qs-tab active" data-tab="one-liner">One-liner</button>
452
- <button class="qs-tab" data-tab="bun">bun</button>
453
- <button class="qs-tab" data-tab="npm">npm</button>
454
- </div>
455
- <div class="qs-chrome-spacer"></div>
456
- </div>
457
- <div class="qs-body">
458
- <!-- one-liner tab -->
459
- <div class="qs-pane active" id="qs-one-liner">
460
- <div class="qs-line">
461
- <span class="qs-comment"># Install and launch in one shot</span>
443
+ <!-- OS toggle -->
444
+ <div class="os-toggle reveal">
445
+ <button class="os-btn active" data-os="mac" onclick="switchOS('mac')">
446
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
447
+ macOS / Linux
448
+ </button>
449
+ <button class="os-btn" data-os="windows" onclick="switchOS('windows')">
450
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.9-1.801"/></svg>
451
+ Windows
452
+ </button>
453
+ <button class="os-btn" data-os="docker" onclick="switchOS('docker')">
454
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.186.186 0 00-.185.186v1.887c0 .102.083.185.185.185zm-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.186.186 0 00-.185.185v1.888c0 .102.082.185.185.186zm0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.186.186 0 00-.185.185v1.887c0 .102.082.186.185.186zm-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.186.186 0 00-.185.185v1.887c0 .102.083.186.185.186zm-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.186.186 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.186.186.186zm5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.186v1.887c0 .102.082.185.185.185zm-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.186v1.887c0 .102.083.185.185.185zm-2.964 0h2.119a.186.186 0 00.185-.185V9.006a.186.186 0 00-.185-.186H5.136a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185zm-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185zM23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.687 11.687 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.33 12.33 0 003.544-1.372 10.078 10.078 0 002.506-2.053c1.076-1.223 1.718-2.58 2.198-3.794h.19c1.18 0 1.907-.482 2.31-.882.265-.26.478-.57.625-.916l.087-.252z"/></svg>
455
+ Docker
456
+ </button>
457
+ </div>
458
+
459
+ <!-- macOS / Linux terminal -->
460
+ <div class="os-content active" data-os-content="mac">
461
+ <div class="qs-terminal glow-border reveal">
462
+ <div class="qs-chrome">
463
+ <div class="terminal-dots">
464
+ <span class="td td-r"></span>
465
+ <span class="td td-y"></span>
466
+ <span class="td td-g"></span>
462
467
  </div>
463
- <div class="qs-line qs-cmd">
464
- <span class="qs-prompt">$</span>
465
- <span class="qs-text">curl -fsSL https://zubo.bot/install.sh | bash</span>
466
- <button class="copy-btn" data-copy="curl -fsSL https://zubo.bot/install.sh | bash" aria-label="Copy command">
467
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
468
- </button>
468
+ <div class="qs-tabs">
469
+ <button class="qs-tab active" data-tab="one-liner">One-liner</button>
470
+ <button class="qs-tab" data-tab="bun">bun</button>
471
+ <button class="qs-tab" data-tab="npm">npm</button>
472
+ </div>
473
+ <div class="qs-chrome-spacer"></div>
474
+ </div>
475
+ <div class="qs-body">
476
+ <div class="qs-pane active" id="qs-one-liner">
477
+ <div class="qs-line">
478
+ <span class="qs-comment"># Install and launch in one shot</span>
479
+ </div>
480
+ <div class="qs-line qs-cmd">
481
+ <span class="qs-prompt">$</span>
482
+ <span class="qs-text">curl -fsSL https://zubo.bot/install.sh | bash</span>
483
+ <button class="copy-btn" data-copy="curl -fsSL https://zubo.bot/install.sh | bash" aria-label="Copy command">
484
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
485
+ </button>
486
+ </div>
487
+ </div>
488
+ <div class="qs-pane" id="qs-bun">
489
+ <div class="qs-line">
490
+ <span class="qs-comment"># Install Zubo</span>
491
+ </div>
492
+ <div class="qs-line qs-cmd">
493
+ <span class="qs-prompt">$</span>
494
+ <span class="qs-text">bun add -g zubo</span>
495
+ <button class="copy-btn" data-copy="bun add -g zubo" aria-label="Copy command">
496
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
497
+ </button>
498
+ </div>
499
+ <div class="qs-line qs-spacer"></div>
500
+ <div class="qs-line">
501
+ <span class="qs-comment"># Meet your new agent</span>
502
+ </div>
503
+ <div class="qs-line qs-cmd">
504
+ <span class="qs-prompt">$</span>
505
+ <span class="qs-text">zubo setup</span>
506
+ <button class="copy-btn" data-copy="zubo setup" aria-label="Copy command">
507
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
508
+ </button>
509
+ </div>
510
+ </div>
511
+ <div class="qs-pane" id="qs-npm">
512
+ <div class="qs-line">
513
+ <span class="qs-comment"># Install Zubo</span>
514
+ </div>
515
+ <div class="qs-line qs-cmd">
516
+ <span class="qs-prompt">$</span>
517
+ <span class="qs-text">npm i -g zubo</span>
518
+ <button class="copy-btn" data-copy="npm i -g zubo" aria-label="Copy command">
519
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
520
+ </button>
521
+ </div>
522
+ <div class="qs-line qs-spacer"></div>
523
+ <div class="qs-line">
524
+ <span class="qs-comment"># Meet your new agent</span>
525
+ </div>
526
+ <div class="qs-line qs-cmd">
527
+ <span class="qs-prompt">$</span>
528
+ <span class="qs-text">zubo setup</span>
529
+ <button class="copy-btn" data-copy="zubo setup" aria-label="Copy command">
530
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
531
+ </button>
532
+ </div>
469
533
  </div>
470
534
  </div>
471
- <!-- bun tab -->
472
- <div class="qs-pane" id="qs-bun">
535
+ </div>
536
+ <p class="qs-note reveal">Works on macOS &amp; Linux. The installer sets up Bun and everything else for you.</p>
537
+ </div>
538
+
539
+ <!-- Windows terminal -->
540
+ <div class="os-content" data-os-content="windows">
541
+ <div class="qs-terminal glow-border reveal">
542
+ <div class="qs-chrome">
543
+ <div class="terminal-dots">
544
+ <span class="td td-r"></span>
545
+ <span class="td td-y"></span>
546
+ <span class="td td-g"></span>
547
+ </div>
548
+ <div class="terminal-title">PowerShell</div>
549
+ <div class="qs-chrome-spacer"></div>
550
+ </div>
551
+ <div class="qs-body">
473
552
  <div class="qs-line">
474
- <span class="qs-comment"># Install Zubo</span>
553
+ <span class="qs-comment"># Install WSL (one-time, requires restart)</span>
475
554
  </div>
476
555
  <div class="qs-line qs-cmd">
477
- <span class="qs-prompt">$</span>
478
- <span class="qs-text">bun add -g zubo</span>
479
- <button class="copy-btn" data-copy="bun add -g zubo" aria-label="Copy command">
556
+ <span class="qs-prompt">&gt;</span>
557
+ <span class="qs-text">wsl --install</span>
558
+ <button class="copy-btn" data-copy="wsl --install" aria-label="Copy command">
480
559
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
481
560
  </button>
482
561
  </div>
483
562
  <div class="qs-line qs-spacer"></div>
484
563
  <div class="qs-line">
485
- <span class="qs-comment"># Meet your new agent</span>
564
+ <span class="qs-comment"># Then open WSL and install Zubo</span>
486
565
  </div>
487
566
  <div class="qs-line qs-cmd">
488
567
  <span class="qs-prompt">$</span>
489
- <span class="qs-text">zubo setup</span>
490
- <button class="copy-btn" data-copy="zubo setup" aria-label="Copy command">
568
+ <span class="qs-text">curl -fsSL https://zubo.bot/install.sh | bash</span>
569
+ <button class="copy-btn" data-copy="curl -fsSL https://zubo.bot/install.sh | bash" aria-label="Copy command">
491
570
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
492
571
  </button>
493
572
  </div>
494
573
  </div>
495
- <!-- npm tab -->
496
- <div class="qs-pane" id="qs-npm">
574
+ </div>
575
+ <p class="qs-note reveal">Runs inside WSL (Windows Subsystem for Linux). Full performance, native experience.</p>
576
+ </div>
577
+
578
+ <!-- Docker terminal -->
579
+ <div class="os-content" data-os-content="docker">
580
+ <div class="qs-terminal glow-border reveal">
581
+ <div class="qs-chrome">
582
+ <div class="terminal-dots">
583
+ <span class="td td-r"></span>
584
+ <span class="td td-y"></span>
585
+ <span class="td td-g"></span>
586
+ </div>
587
+ <div class="terminal-title">Terminal</div>
588
+ <div class="qs-chrome-spacer"></div>
589
+ </div>
590
+ <div class="qs-body">
497
591
  <div class="qs-line">
498
- <span class="qs-comment"># Install Zubo</span>
592
+ <span class="qs-comment"># Clone and start with Docker</span>
499
593
  </div>
500
594
  <div class="qs-line qs-cmd">
501
595
  <span class="qs-prompt">$</span>
502
- <span class="qs-text">npm i -g zubo</span>
503
- <button class="copy-btn" data-copy="npm i -g zubo" aria-label="Copy command">
596
+ <span class="qs-text">git clone https://github.com/apwn/zubo.git &amp;&amp; cd zubo</span>
597
+ <button class="copy-btn" data-copy="git clone https://github.com/apwn/zubo.git && cd zubo" aria-label="Copy command">
504
598
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
505
599
  </button>
506
600
  </div>
507
601
  <div class="qs-line qs-spacer"></div>
508
602
  <div class="qs-line">
509
- <span class="qs-comment"># Meet your new agent</span>
603
+ <span class="qs-comment"># Launch Zubo in a container</span>
510
604
  </div>
511
605
  <div class="qs-line qs-cmd">
512
606
  <span class="qs-prompt">$</span>
513
- <span class="qs-text">zubo setup</span>
514
- <button class="copy-btn" data-copy="zubo setup" aria-label="Copy command">
607
+ <span class="qs-text">docker compose up -d</span>
608
+ <button class="copy-btn" data-copy="docker compose up -d" aria-label="Copy command">
515
609
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
516
610
  </button>
517
611
  </div>
518
612
  </div>
519
613
  </div>
614
+ <p class="qs-note reveal">Works on any OS with Docker. Dashboard at <strong>localhost:8787</strong>.</p>
520
615
  </div>
521
-
522
- <p class="qs-note reveal">Works on macOS &amp; Linux. The installer sets up Bun and everything else for you.</p>
523
616
  </div>
524
617
  </section>
525
618
 
package/site/script.js CHANGED
@@ -3,6 +3,16 @@
3
3
  Vanilla JS, zero dependencies
4
4
  ============================================ */
5
5
 
6
+ /* OS toggle — must be global for inline onclick handlers */
7
+ function switchOS(os) {
8
+ document.querySelectorAll('.os-btn').forEach(function (b) { b.classList.remove('active'); });
9
+ document.querySelectorAll('.os-content').forEach(function (c) { c.classList.remove('active'); });
10
+ var btn = document.querySelector('.os-btn[data-os="' + os + '"]');
11
+ if (btn) btn.classList.add('active');
12
+ var content = document.querySelector('[data-os-content="' + os + '"]');
13
+ if (content) content.classList.add('active');
14
+ }
15
+
6
16
  (function () {
7
17
  'use strict';
8
18
 
@@ -231,14 +241,13 @@
231
241
  });
232
242
 
233
243
  /* ------------------------------------------
234
- 6. Quick Start tab switching
244
+ 6. Quick Start tab switching (within macOS/Linux)
235
245
  ------------------------------------------ */
236
246
  document.querySelectorAll('.qs-tab').forEach(function (tab) {
237
247
  tab.addEventListener('click', function () {
238
- // Deactivate all tabs and panes
239
- document.querySelectorAll('.qs-tab').forEach(function (t) { t.classList.remove('active'); });
240
- document.querySelectorAll('.qs-pane').forEach(function (p) { p.classList.remove('active'); });
241
- // Activate clicked tab and matching pane
248
+ var parent = tab.closest('.os-content') || document;
249
+ parent.querySelectorAll('.qs-tab').forEach(function (t) { t.classList.remove('active'); });
250
+ parent.querySelectorAll('.qs-pane').forEach(function (p) { p.classList.remove('active'); });
242
251
  tab.classList.add('active');
243
252
  var paneId = 'qs-' + tab.getAttribute('data-tab');
244
253
  var pane = document.getElementById(paneId);
package/site/style.css CHANGED
@@ -986,6 +986,44 @@ h1, h2, h3, h4,
986
986
  color: #c4b5fd;
987
987
  }
988
988
  .qs-chrome-spacer { flex: 1; }
989
+
990
+ /* OS toggle */
991
+ .os-toggle {
992
+ display: flex;
993
+ justify-content: center;
994
+ gap: 6px;
995
+ margin-bottom: 24px;
996
+ }
997
+ .os-btn {
998
+ display: inline-flex;
999
+ align-items: center;
1000
+ gap: 8px;
1001
+ background: rgba(255,255,255,0.04);
1002
+ border: 1px solid var(--border);
1003
+ border-radius: 10px;
1004
+ padding: 10px 20px;
1005
+ font-family: var(--font);
1006
+ font-size: 14px;
1007
+ font-weight: 500;
1008
+ color: var(--text-muted);
1009
+ cursor: pointer;
1010
+ transition: all 0.25s var(--ease);
1011
+ }
1012
+ .os-btn:hover {
1013
+ color: var(--text-secondary);
1014
+ border-color: rgba(124, 58, 237, 0.2);
1015
+ background: rgba(255,255,255,0.06);
1016
+ }
1017
+ .os-btn.active {
1018
+ background: rgba(124, 58, 237, 0.12);
1019
+ border-color: rgba(124, 58, 237, 0.35);
1020
+ color: #c4b5fd;
1021
+ }
1022
+ .os-btn svg { opacity: 0.7; }
1023
+ .os-btn.active svg { opacity: 1; }
1024
+ .os-content { display: none; }
1025
+ .os-content.active { display: block; }
1026
+
989
1027
  .qs-body {
990
1028
  padding: 28px 28px 32px;
991
1029
  font-family: var(--mono);
@@ -1,5 +1,11 @@
1
1
  const API = "https://gmail.googleapis.com/gmail/v1/users/me";
2
2
 
3
+ /** RFC 2047 encode a header value if it contains non-ASCII characters (e.g. emojis) */
4
+ function mimeEncode(value: string): string {
5
+ if (/^[\x20-\x7E]*$/.test(value)) return value;
6
+ return `=?UTF-8?B?${Buffer.from(value, "utf-8").toString("base64")}?=`;
7
+ }
8
+
3
9
  async function getToken(): Promise<string> {
4
10
  const Zubo = (globalThis as any).Zubo;
5
11
  if (Zubo?.getGoogleToken) return Zubo.getGoogleToken();
@@ -80,7 +86,7 @@ export default async function (input: Record<string, unknown>): Promise<string>
80
86
  case "send": {
81
87
  if (!to) return JSON.stringify({ error: "to is required" });
82
88
  if (!subject) return JSON.stringify({ error: "subject is required" });
83
- const raw = Buffer.from(`To: ${to}\r\nSubject: ${subject}\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n${body || ""}`).toString("base64url");
89
+ const raw = Buffer.from(`To: ${to}\r\nSubject: ${mimeEncode(subject)}\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n${body || ""}`).toString("base64url");
84
90
  const res = await fetch(`${API}/messages/send`, {
85
91
  method: "POST", headers,
86
92
  body: JSON.stringify({ raw }),
@@ -106,7 +112,7 @@ export default async function (input: Record<string, unknown>): Promise<string>
106
112
  const replyTo = getHeader("From");
107
113
  const subj = getHeader("Subject").startsWith("Re:") ? getHeader("Subject") : `Re: ${getHeader("Subject")}`;
108
114
  const msgId = getHeader("Message-ID");
109
- const raw = Buffer.from(`To: ${replyTo}\r\nSubject: ${subj}\r\nIn-Reply-To: ${msgId}\r\nReferences: ${msgId}\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n${body}`).toString("base64url");
115
+ const raw = Buffer.from(`To: ${replyTo}\r\nSubject: ${mimeEncode(subj)}\r\nIn-Reply-To: ${msgId}\r\nReferences: ${msgId}\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n${body}`).toString("base64url");
110
116
  const res = await fetch(`${API}/messages/send`, {
111
117
  method: "POST", headers,
112
118
  body: JSON.stringify({ raw, threadId: origData.threadId }),
@@ -37,11 +37,17 @@ function validateUrl(raw: string): void {
37
37
  export default async function (input: Record<string, unknown>): Promise<string> {
38
38
  const url = input.url as string;
39
39
  const method = ((input.method as string) || "GET").toUpperCase();
40
- const headers = (input.headers as Record<string, string>) || {};
40
+ const rawHeaders = (input.headers as Record<string, string>) || {};
41
41
  const body = input.body as string | undefined;
42
42
 
43
43
  validateUrl(url);
44
44
 
45
+ // Strip non-ASCII characters from header values (HTTP/1.1 requires ASCII)
46
+ const headers: Record<string, string> = {};
47
+ for (const [key, value] of Object.entries(rawHeaders)) {
48
+ headers[key] = String(value).replace(/[^\x20-\x7E]/g, "");
49
+ }
50
+
45
51
  const opts: RequestInit = {
46
52
  method,
47
53
  headers: {