docs: lowercase internal docs + migrate website docs to public-docs/ (#634)

* docs: rename to lowercase + drop leftover plans

Rename docs in docs/ to lowercase kebab-case for consistency and update
all references in CLAUDE.md, CONTRIBUTING.md, CHANGELOG.md,
packages/server/CLAUDE.md, and inter-doc links.

Drop two leftover design plan docs:
- docs/ATTACHMENT_BASED_REVIEW_CONTEXT_PLAN.md
- docs/plan-approval-normalization.md

* docs: drop stale uppercase entries from case-insensitive rename

* feat(website): power /docs from public-docs/ markdown tree

Move website docs out of TSX route components and into a root-level
public-docs/ directory of plain markdown files with frontmatter
(title, description, nav, order).

- Add packages/website/src/docs.ts loader using import.meta.glob with
  ?raw to compile the markdown into the bundle at build time.
- Replace the 9 hand-written docs/*.tsx routes with a single $.tsx
  catch-all that renders any slug via react-markdown.
- Drive the docs sidebar nav from frontmatter order/nav.
- Auto-discover docs routes in vite.config.ts so the sitemap stays in
  sync without manual edits.

* fix(website): bind dev server to 0.0.0.0 so port collisions trigger fallback

`host: "127.0.0.1"` (or unset) lets macOS coexist with another process
holding an IPv6 dual-stack `*:8082` socket, so Vite never sees
EADDRINUSE and silently binds alongside it. Forcing IPv4 wildcard
makes the conflict real, and Vite's default `strictPort: false`
falls through to the next free port.

* fix(website): restore docs page styling after markdown migration

Add a .docs-prose class that mirrors the styling the original
docs/*.tsx components hand-rolled (h1/h2/h3 sizes, paragraph/list
spacing, link colors, code blocks, callout-style blockquotes).

ReactMarkdown was emitting unstyled HTML because the previous
wrapper class only had inline-code rules — headings and code
blocks fell back to user-agent defaults.
This commit is contained in:
Mohamed Boudra
2026-04-30 17:27:15 +08:00
committed by GitHub
parent 5d9dc0c4d5
commit dec47d72d9
49 changed files with 1395 additions and 2572 deletions
+81
View File
@@ -0,0 +1,81 @@
interface DocFrontmatter {
title: string;
description: string;
nav: string;
order: number;
}
export interface Doc {
slug: string;
href: string;
frontmatter: DocFrontmatter;
content: string;
}
function parseFrontmatter(raw: string): { data: Record<string, string>; content: string } {
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (!match) return { data: {}, content: raw };
const data: Record<string, string> = {};
for (const line of match[1].split("\n")) {
const colonIdx = line.indexOf(":");
if (colonIdx === -1) continue;
const key = line.slice(0, colonIdx).trim();
const value = line
.slice(colonIdx + 1)
.trim()
.replace(/^["']|["']$/g, "");
data[key] = value;
}
return { data, content: match[2] };
}
const docModules = import.meta.glob("../../../public-docs/**/*.md", {
eager: true,
query: "?raw",
import: "default",
}) as Record<string, string>;
function pathToSlug(path: string): string {
const after = path.split("/public-docs/")[1] ?? path;
const noExt = after.replace(/\.md$/, "");
return noExt === "index" ? "" : noExt;
}
function loadDocs(): Doc[] {
const docs: Doc[] = [];
for (const [path, raw] of Object.entries(docModules)) {
const { data, content } = parseFrontmatter(raw);
const slug = pathToSlug(path);
const href = slug === "" ? "/docs" : `/docs/${slug}`;
const order = Number.parseInt(data.order ?? "999", 10);
docs.push({
slug,
href,
frontmatter: {
title: data.title ?? "",
description: data.description ?? "",
nav: data.nav ?? data.title ?? slug,
order: Number.isFinite(order) ? order : 999,
},
content,
});
}
docs.sort((a, b) => a.frontmatter.order - b.frontmatter.order);
return docs;
}
let cached: Doc[] | undefined;
export function getDocs(): Doc[] {
if (!cached) cached = loadDocs();
return cached;
}
export function getDoc(slug: string): Doc | undefined {
return getDocs().find((d) => d.slug === slug);
}
+17 -185
View File
@@ -20,15 +20,7 @@ import { Route as BlogRouteImport } from "./routes/blog";
import { Route as IndexRouteImport } from "./routes/index";
import { Route as DocsIndexRouteImport } from "./routes/docs/index";
import { Route as BlogIndexRouteImport } from "./routes/blog/index";
import { Route as DocsWorktreesRouteImport } from "./routes/docs/worktrees";
import { Route as DocsVoiceRouteImport } from "./routes/docs/voice";
import { Route as DocsUpdatesRouteImport } from "./routes/docs/updates";
import { Route as DocsSkillsRouteImport } from "./routes/docs/skills";
import { Route as DocsSecurityRouteImport } from "./routes/docs/security";
import { Route as DocsProvidersRouteImport } from "./routes/docs/providers";
import { Route as DocsConfigurationRouteImport } from "./routes/docs/configuration";
import { Route as DocsCliRouteImport } from "./routes/docs/cli";
import { Route as DocsBestPracticesRouteImport } from "./routes/docs/best-practices";
import { Route as DocsSplatRouteImport } from "./routes/docs/$";
import { Route as BlogSplatRouteImport } from "./routes/blog/$";
const PrivacyRoute = PrivacyRouteImport.update({
@@ -86,49 +78,9 @@ const BlogIndexRoute = BlogIndexRouteImport.update({
path: "/",
getParentRoute: () => BlogRoute,
} as any);
const DocsWorktreesRoute = DocsWorktreesRouteImport.update({
id: "/worktrees",
path: "/worktrees",
getParentRoute: () => DocsRoute,
} as any);
const DocsVoiceRoute = DocsVoiceRouteImport.update({
id: "/voice",
path: "/voice",
getParentRoute: () => DocsRoute,
} as any);
const DocsUpdatesRoute = DocsUpdatesRouteImport.update({
id: "/updates",
path: "/updates",
getParentRoute: () => DocsRoute,
} as any);
const DocsSkillsRoute = DocsSkillsRouteImport.update({
id: "/skills",
path: "/skills",
getParentRoute: () => DocsRoute,
} as any);
const DocsSecurityRoute = DocsSecurityRouteImport.update({
id: "/security",
path: "/security",
getParentRoute: () => DocsRoute,
} as any);
const DocsProvidersRoute = DocsProvidersRouteImport.update({
id: "/providers",
path: "/providers",
getParentRoute: () => DocsRoute,
} as any);
const DocsConfigurationRoute = DocsConfigurationRouteImport.update({
id: "/configuration",
path: "/configuration",
getParentRoute: () => DocsRoute,
} as any);
const DocsCliRoute = DocsCliRouteImport.update({
id: "/cli",
path: "/cli",
getParentRoute: () => DocsRoute,
} as any);
const DocsBestPracticesRoute = DocsBestPracticesRouteImport.update({
id: "/best-practices",
path: "/best-practices",
const DocsSplatRoute = DocsSplatRouteImport.update({
id: "/$",
path: "/$",
getParentRoute: () => DocsRoute,
} as any);
const BlogSplatRoute = BlogSplatRouteImport.update({
@@ -148,15 +100,7 @@ export interface FileRoutesByFullPath {
"/opencode": typeof OpencodeRoute;
"/privacy": typeof PrivacyRoute;
"/blog/$": typeof BlogSplatRoute;
"/docs/best-practices": typeof DocsBestPracticesRoute;
"/docs/cli": typeof DocsCliRoute;
"/docs/configuration": typeof DocsConfigurationRoute;
"/docs/providers": typeof DocsProvidersRoute;
"/docs/security": typeof DocsSecurityRoute;
"/docs/skills": typeof DocsSkillsRoute;
"/docs/updates": typeof DocsUpdatesRoute;
"/docs/voice": typeof DocsVoiceRoute;
"/docs/worktrees": typeof DocsWorktreesRoute;
"/docs/$": typeof DocsSplatRoute;
"/blog/": typeof BlogIndexRoute;
"/docs/": typeof DocsIndexRoute;
}
@@ -169,15 +113,7 @@ export interface FileRoutesByTo {
"/opencode": typeof OpencodeRoute;
"/privacy": typeof PrivacyRoute;
"/blog/$": typeof BlogSplatRoute;
"/docs/best-practices": typeof DocsBestPracticesRoute;
"/docs/cli": typeof DocsCliRoute;
"/docs/configuration": typeof DocsConfigurationRoute;
"/docs/providers": typeof DocsProvidersRoute;
"/docs/security": typeof DocsSecurityRoute;
"/docs/skills": typeof DocsSkillsRoute;
"/docs/updates": typeof DocsUpdatesRoute;
"/docs/voice": typeof DocsVoiceRoute;
"/docs/worktrees": typeof DocsWorktreesRoute;
"/docs/$": typeof DocsSplatRoute;
"/blog": typeof BlogIndexRoute;
"/docs": typeof DocsIndexRoute;
}
@@ -193,15 +129,7 @@ export interface FileRoutesById {
"/opencode": typeof OpencodeRoute;
"/privacy": typeof PrivacyRoute;
"/blog/$": typeof BlogSplatRoute;
"/docs/best-practices": typeof DocsBestPracticesRoute;
"/docs/cli": typeof DocsCliRoute;
"/docs/configuration": typeof DocsConfigurationRoute;
"/docs/providers": typeof DocsProvidersRoute;
"/docs/security": typeof DocsSecurityRoute;
"/docs/skills": typeof DocsSkillsRoute;
"/docs/updates": typeof DocsUpdatesRoute;
"/docs/voice": typeof DocsVoiceRoute;
"/docs/worktrees": typeof DocsWorktreesRoute;
"/docs/$": typeof DocsSplatRoute;
"/blog/": typeof BlogIndexRoute;
"/docs/": typeof DocsIndexRoute;
}
@@ -218,15 +146,7 @@ export interface FileRouteTypes {
| "/opencode"
| "/privacy"
| "/blog/$"
| "/docs/best-practices"
| "/docs/cli"
| "/docs/configuration"
| "/docs/providers"
| "/docs/security"
| "/docs/skills"
| "/docs/updates"
| "/docs/voice"
| "/docs/worktrees"
| "/docs/$"
| "/blog/"
| "/docs/";
fileRoutesByTo: FileRoutesByTo;
@@ -239,15 +159,7 @@ export interface FileRouteTypes {
| "/opencode"
| "/privacy"
| "/blog/$"
| "/docs/best-practices"
| "/docs/cli"
| "/docs/configuration"
| "/docs/providers"
| "/docs/security"
| "/docs/skills"
| "/docs/updates"
| "/docs/voice"
| "/docs/worktrees"
| "/docs/$"
| "/blog"
| "/docs";
id:
@@ -262,15 +174,7 @@ export interface FileRouteTypes {
| "/opencode"
| "/privacy"
| "/blog/$"
| "/docs/best-practices"
| "/docs/cli"
| "/docs/configuration"
| "/docs/providers"
| "/docs/security"
| "/docs/skills"
| "/docs/updates"
| "/docs/voice"
| "/docs/worktrees"
| "/docs/$"
| "/blog/"
| "/docs/";
fileRoutesById: FileRoutesById;
@@ -366,67 +270,11 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof BlogIndexRouteImport;
parentRoute: typeof BlogRoute;
};
"/docs/worktrees": {
id: "/docs/worktrees";
path: "/worktrees";
fullPath: "/docs/worktrees";
preLoaderRoute: typeof DocsWorktreesRouteImport;
parentRoute: typeof DocsRoute;
};
"/docs/voice": {
id: "/docs/voice";
path: "/voice";
fullPath: "/docs/voice";
preLoaderRoute: typeof DocsVoiceRouteImport;
parentRoute: typeof DocsRoute;
};
"/docs/updates": {
id: "/docs/updates";
path: "/updates";
fullPath: "/docs/updates";
preLoaderRoute: typeof DocsUpdatesRouteImport;
parentRoute: typeof DocsRoute;
};
"/docs/skills": {
id: "/docs/skills";
path: "/skills";
fullPath: "/docs/skills";
preLoaderRoute: typeof DocsSkillsRouteImport;
parentRoute: typeof DocsRoute;
};
"/docs/security": {
id: "/docs/security";
path: "/security";
fullPath: "/docs/security";
preLoaderRoute: typeof DocsSecurityRouteImport;
parentRoute: typeof DocsRoute;
};
"/docs/providers": {
id: "/docs/providers";
path: "/providers";
fullPath: "/docs/providers";
preLoaderRoute: typeof DocsProvidersRouteImport;
parentRoute: typeof DocsRoute;
};
"/docs/configuration": {
id: "/docs/configuration";
path: "/configuration";
fullPath: "/docs/configuration";
preLoaderRoute: typeof DocsConfigurationRouteImport;
parentRoute: typeof DocsRoute;
};
"/docs/cli": {
id: "/docs/cli";
path: "/cli";
fullPath: "/docs/cli";
preLoaderRoute: typeof DocsCliRouteImport;
parentRoute: typeof DocsRoute;
};
"/docs/best-practices": {
id: "/docs/best-practices";
path: "/best-practices";
fullPath: "/docs/best-practices";
preLoaderRoute: typeof DocsBestPracticesRouteImport;
"/docs/$": {
id: "/docs/$";
path: "/$";
fullPath: "/docs/$";
preLoaderRoute: typeof DocsSplatRouteImport;
parentRoute: typeof DocsRoute;
};
"/blog/$": {
@@ -452,28 +300,12 @@ const BlogRouteChildren: BlogRouteChildren = {
const BlogRouteWithChildren = BlogRoute._addFileChildren(BlogRouteChildren);
interface DocsRouteChildren {
DocsBestPracticesRoute: typeof DocsBestPracticesRoute;
DocsCliRoute: typeof DocsCliRoute;
DocsConfigurationRoute: typeof DocsConfigurationRoute;
DocsProvidersRoute: typeof DocsProvidersRoute;
DocsSecurityRoute: typeof DocsSecurityRoute;
DocsSkillsRoute: typeof DocsSkillsRoute;
DocsUpdatesRoute: typeof DocsUpdatesRoute;
DocsVoiceRoute: typeof DocsVoiceRoute;
DocsWorktreesRoute: typeof DocsWorktreesRoute;
DocsSplatRoute: typeof DocsSplatRoute;
DocsIndexRoute: typeof DocsIndexRoute;
}
const DocsRouteChildren: DocsRouteChildren = {
DocsBestPracticesRoute: DocsBestPracticesRoute,
DocsCliRoute: DocsCliRoute,
DocsConfigurationRoute: DocsConfigurationRoute,
DocsProvidersRoute: DocsProvidersRoute,
DocsSecurityRoute: DocsSecurityRoute,
DocsSkillsRoute: DocsSkillsRoute,
DocsUpdatesRoute: DocsUpdatesRoute,
DocsVoiceRoute: DocsVoiceRoute,
DocsWorktreesRoute: DocsWorktreesRoute,
DocsSplatRoute: DocsSplatRoute,
DocsIndexRoute: DocsIndexRoute,
};
+8 -15
View File
@@ -1,28 +1,21 @@
import { createFileRoute, Link, Outlet } from "@tanstack/react-router";
import { getDocs } from "~/docs";
import "~/styles.css";
export const Route = createFileRoute("/docs")({
component: DocsLayout,
});
const navigation = [
{ name: "Getting started", href: "/docs" },
{ name: "Updates", href: "/docs/updates" },
{ name: "Voice", href: "/docs/voice" },
{ name: "Git worktrees", href: "/docs/worktrees" },
{ name: "CLI", href: "/docs/cli" },
{ name: "Skills", href: "/docs/skills" },
{ name: "Providers", href: "/docs/providers" },
{ name: "Configuration", href: "/docs/configuration" },
{ name: "Security", href: "/docs/security" },
{ name: "Best practices", href: "/docs/best-practices" },
];
const ACTIVE_OPTIONS_EXACT = { exact: true };
const MOBILE_ACTIVE_PROPS = { className: "text-foreground" };
const DESKTOP_ACTIVE_PROPS = { className: "bg-muted text-foreground" };
function DocsLayout() {
const navigation = getDocs().map((doc) => ({
name: doc.frontmatter.nav,
href: doc.href,
}));
return (
<div className="min-h-screen bg-background">
{/* Mobile header */}
@@ -31,7 +24,7 @@ function DocsLayout() {
<img src="/logo.svg" alt="Paseo" className="w-6 h-6" />
<span className="text-lg font-medium">Paseo</span>
</Link>
<nav className="flex gap-4 mt-4">
<nav className="flex gap-4 mt-4 flex-wrap">
{navigation.map((item) => (
<Link
key={item.href}
@@ -67,7 +60,7 @@ function DocsLayout() {
))}
</nav>
</aside>
<main className="flex-1 p-6 md:p-12 max-w-3xl prose">
<main className="flex-1 p-6 md:p-12 max-w-3xl docs-prose">
<Outlet />
</main>
</div>
+28
View File
@@ -0,0 +1,28 @@
import { createFileRoute } from "@tanstack/react-router";
import ReactMarkdown from "react-markdown";
import { getDoc } from "~/docs";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/docs/$")({
head: ({ params }) => {
const slug = params._splat ?? "";
const doc = getDoc(slug);
if (!doc) return { meta: pageMeta("Not Found - Paseo Docs", "Doc not found.") };
return {
meta: pageMeta(`${doc.frontmatter.title} - Paseo Docs`, doc.frontmatter.description),
};
},
component: DocsPage,
});
function DocsPage() {
const { _splat } = Route.useParams();
const slug = _splat ?? "";
const doc = getDoc(slug);
if (!doc) {
return <p className="text-muted-foreground">Doc not found.</p>;
}
return <ReactMarkdown>{doc.content}</ReactMarkdown>;
}
@@ -1,136 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/docs/best-practices")({
head: () => ({
meta: pageMeta(
"Best Practices - Paseo Docs",
"Tips for getting the most out of Paseo and mobile-first agent workflows.",
),
}),
component: BestPractices,
});
function BestPractices() {
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-medium font-title mb-4">Best Practices</h1>
<p className="text-white/60 leading-relaxed">
What I&apos;ve learned from using Paseo daily. Not rules, just patterns that have worked
for me.
</p>
</div>
<section className="space-y-4">
<h2 className="text-xl font-medium">Agents replace typing, not thinking</h2>
<p className="text-white/60 leading-relaxed">
Your role has changed. You&apos;re no longer the one writing code line by line.
You&apos;re the one making decisions: what to build, how it should work, what the
architecture looks like. The agent executes, but you direct.
</p>
<p className="text-white/60 leading-relaxed">
You can&apos;t just say &quot;implement feature X&quot; and walk away. You still have to
do the hard part: deciding what to build, how it fits into the system, what trade-offs to
make. Thinking is not optional. At least for now, agents replace the typing, not the
thinking.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Verification loops</h2>
<p className="text-white/60 leading-relaxed">
The agent needs a way to verify its work. TDD is one implementation of this pattern: get
the agent to write a failing test, verify it fails for the right reasons, then tell it to
make the test pass. The agent can loop on its own because it knows what &quot;done&quot;
means.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Invest in tooling</h2>
<p className="text-white/60 leading-relaxed">
It&apos;s not just test runners. For web apps, something like Playwright MCP lets the
agent take screenshots and verify UI changes. For a SaaS app I built a CLI that wraps all
the business logic so the agent could launch jobs, check statuses, and scrape data without
going through the UI.
</p>
<p className="text-white/60 leading-relaxed">
Code is cheap with coding agents. I would have never written that CLI before because it
felt like wasted effort. Now I bootstrap tooling first. It pays off exponentially.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Agents are cheap</h2>
<p className="text-white/60 leading-relaxed">
Don&apos;t be shy about running multiple agents. Paseo lets you launch agents in isolated
worktrees. Kick one off with voice while walking, then kick off another. They work
independently. You get a notification when they&apos;re done.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Use voice extensively</h2>
<p className="text-white/60 leading-relaxed">
It&apos;s much more natural to use voice to communicate ideas and pull them out of your
brain. The agent will parse and organize your thoughts better than if you try to write the
perfect prompt. You don&apos;t need to organize anything. Just talk.
</p>
<p className="text-white/60 leading-relaxed">
Current speech-to-text models are really good. They catch accents, acronyms, technical
terms. And even when they don&apos;t, the LLM will infer what you meant.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Understand the type of work</h2>
<p className="text-white/60 leading-relaxed">
Sometimes you need to plan: design a spec, verify it, get the agent to follow through.
Maybe it takes a couple of agents to work through it. Other times it&apos;s
conversational: kick off a single agent and start talking, asking questions. Match your
approach to the task.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Iterate and refactor often</h2>
<p className="text-white/60 leading-relaxed">
Don&apos;t expect perfect. Expect working. Make it work, make it correct, make it
beautiful. Each iteration gets you closer. With tests, refactoring is cheap.
</p>
<p className="text-white/60 leading-relaxed">
I don&apos;t let myself add too many features before stopping to refactor. Sometimes I
kick off an agent and have it trace code paths, explain dependencies, show me how modules
connect. I make mental notes during code review and circle back.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Use agents to check agents</h2>
<p className="text-white/60 leading-relaxed">
If an agent implements something and you ask it to review its own work, it will never find
issues. Launch a separate agent with a fresh context to review the first agent&apos;s
code. It will catch things the first agent missed or glossed over. An agent might say
it&apos;s done when it&apos;s not. Another agent can detect that.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Learn your agents&apos; quirks</h2>
<p className="text-white/60 leading-relaxed">
People argue about which model is better. That&apos;s the wrong question. Each model has
strengths and weaknesses. Knowing them is more useful than chasing benchmarks. Benchmarks
don&apos;t mean anything. You need to try the models yourself to form an opinion.
</p>
<p className="text-white/60 leading-relaxed">
I use Claude Code as my main driver because it&apos;s quick and uses tools well. But
sometimes it jumps to conclusions and gives up too easily. Codex is frustratingly slow but
goes deep, doesn&apos;t stop, and is methodical. It&apos;s also stubborn and too serious.
These aren&apos;t good or bad traits, just differences you learn to work around. Use the
right model for the job.
</p>
</section>
</div>
);
}
-257
View File
@@ -1,257 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/docs/cli")({
head: () => ({
meta: pageMeta(
"CLI - Paseo Docs",
"Paseo CLI reference: manage agents, daemons, permissions, and worktrees from your terminal.",
),
}),
component: CLI,
});
function Code({ children }: { children: React.ReactNode }) {
return (
<div className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto">
{children}
</div>
);
}
function CLI() {
return (
<div className="space-y-10">
<div>
<h1 className="text-3xl font-medium font-title mb-4">CLI</h1>
<p className="text-white/60 leading-relaxed">
The Paseo CLI lets you manage agents from your terminal. It&apos;s the same interface
exposed by the daemon&apos;s API, so anything you can do in the app you can do from the
command line.
</p>
</div>
{/* Agent orchestration callout */}
<section className="space-y-4">
<div className="bg-primary/10 border border-primary/30 rounded-lg p-4 text-white/80">
<strong>Agent orchestration:</strong> You can tell coding agents to use the Paseo CLI to
spawn and manage other agents. This enables multi-agent workflows where one agent
delegates subtasks to others and waits for results.
</div>
</section>
{/* Quick reference */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Quick reference</h2>
<Code>
<pre className="text-white/80">{`paseo run "fix the tests" # Start an agent
paseo ls # List running agents
paseo attach <id> # Stream agent output
paseo send <id> "also fix linting" # Send follow-up task
paseo logs <id> # View agent timeline
paseo stop <id> # Stop an agent`}</pre>
</Code>
</section>
{/* Running agents */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Running agents</h2>
<p className="text-white/60 leading-relaxed">
Use <code className="font-mono">paseo run</code> to start a new agent with a task:
</p>
<Code>
<pre className="text-white/80">{`paseo run "implement user authentication"
paseo run --provider codex "refactor the API layer"
paseo run --detach "run the full test suite" # background
paseo run --worktree feature-x "implement feature X"
paseo run --output-schema schema.json "extract release notes"
paseo run --output-schema '{"type":"object","properties":{"summary":{"type":"string"}},"required":["summary"]}' "summarize release notes"`}</pre>
</Code>
<p className="text-white/60 leading-relaxed">
The <code className="font-mono">--worktree</code> flag creates the agent in an isolated
git worktree, useful for parallel feature development.
</p>
<p className="text-white/60 leading-relaxed">
Use <code className="font-mono">--output-schema</code> to return only matching JSON
output. You can pass a schema file path or an inline JSON schema object. This mode cannot
be used with <code className="font-mono">--detach</code>.
</p>
<p className="text-white/60 leading-relaxed">
By default, <code className="font-mono">paseo run</code> waits for completion. Use{" "}
<code className="font-mono">--detach</code> to run in the background.
</p>
</section>
{/* Listing agents */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Listing agents</h2>
<Code>
<pre className="text-white/80">{`paseo ls # Running agents in current directory
paseo ls -a # Include completed/stopped agents
paseo ls -g # All directories
paseo ls -a -g --json # Full list as JSON`}</pre>
</Code>
</section>
{/* Streaming output */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Streaming output</h2>
<p className="text-white/60 leading-relaxed">
Use <code className="font-mono">paseo attach</code> to stream an agent&apos;s output in
real-time:
</p>
<Code>
<pre className="text-white/80">{`paseo attach abc123 # Attach to agent (Ctrl+C to detach)`}</pre>
</Code>
<p className="text-white/60 leading-relaxed">
Agent IDs can be shortened — <code className="font-mono">abc</code> works if it&apos;s
unambiguous.
</p>
</section>
{/* Sending messages */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Sending messages</h2>
<p className="text-white/60 leading-relaxed">
Send follow-up tasks to a running or idle agent:
</p>
<Code>
<pre className="text-white/80">{`paseo send <id> "now run the tests"
paseo send <id> --image screenshot.png "what's wrong here?"
paseo send <id> --no-wait "queue this task"`}</pre>
</Code>
</section>
{/* Viewing logs */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Viewing logs</h2>
<Code>
<pre className="text-white/80">{`paseo logs <id> # Full timeline
paseo logs <id> -f # Follow (streaming)
paseo logs <id> --tail 10 # Last 10 entries
paseo logs <id> --filter tools # Only tool calls`}</pre>
</Code>
</section>
{/* Waiting for agents */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Waiting for agents</h2>
<p className="text-white/60 leading-relaxed">
Block until an agent finishes its current task:
</p>
<Code>
<pre className="text-white/80">{`paseo wait <id>
paseo wait <id> --timeout 60 # 60 second timeout`}</pre>
</Code>
<p className="text-white/60 leading-relaxed">
Useful in scripts or when one agent needs to wait for another.
</p>
</section>
{/* Permissions */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Permissions</h2>
<p className="text-white/60 leading-relaxed">
Agents may request permission for certain actions. Manage these from the CLI:
</p>
<Code>
<pre className="text-white/80">{`paseo permit ls # List pending requests
paseo permit allow <id> # Allow all pending for agent
paseo permit deny <id> --all # Deny all pending`}</pre>
</Code>
</section>
{/* Agent modes */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Agent modes</h2>
<p className="text-white/60 leading-relaxed">
Change an agent&apos;s operational mode (provider-specific):
</p>
<Code>
<pre className="text-white/80">{`paseo agent mode <id> --list # Show available modes
paseo agent mode <id> bypass # Set bypass mode
paseo agent mode <id> plan # Set plan mode`}</pre>
</Code>
</section>
{/* Daemon management */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Daemon management</h2>
<Code>
<pre className="text-white/80">{`paseo daemon start # Start the daemon
paseo daemon status # Check status
paseo daemon stop # Stop the daemon`}</pre>
</Code>
<p className="text-white/60 leading-relaxed">
Use <code className="font-mono">PASEO_HOME</code> to run multiple isolated daemon
instances.
</p>
</section>
{/* Multi-agent workflows */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Multi-agent workflows</h2>
<p className="text-white/60 leading-relaxed">
The CLI is designed to be used by agents themselves. You can instruct an agent to spawn
sub-agents for parallel work:
</p>
<Code>
<pre className="text-white/80">{`# Agent A spawns Agent B and waits for it
paseo run --detach "implement the API" --name api-agent
paseo wait api-agent
paseo logs api-agent --tail 5`}</pre>
</Code>
<p className="text-white/60 leading-relaxed">Simple implement + verify loop:</p>
<Code>
<pre className="text-white/80">{`# Requires jq
while true; do
paseo run --provider codex "make the tests pass" >/dev/null
verdict=$(paseo run --provider claude --output-schema '{"type":"object","properties":{"criteria_met":{"type":"boolean"}},"required":["criteria_met"],"additionalProperties":false}' "ensure tests all pass")
if echo "$verdict" | jq -e '.criteria_met == true' >/dev/null; then
echo "criteria met"
break
fi
done`}</pre>
</Code>
<p className="text-white/60 leading-relaxed">
This pattern enables hierarchical task decomposition — a lead agent can break down work,
delegate to specialists, and synthesize results.
</p>
</section>
{/* Output formats */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Output formats</h2>
<p className="text-white/60 leading-relaxed">
Most commands support multiple output formats for scripting:
</p>
<Code>
<pre className="text-white/80">{`paseo ls --json # JSON output
paseo ls --format yaml # YAML output
paseo ls -q # IDs only (quiet)`}</pre>
</Code>
</section>
{/* Global options */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Global options</h2>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
<code className="font-mono">--host &lt;host:port&gt;</code> — connect to a different
daemon
</li>
<li>
<code className="font-mono">--json</code> — JSON output
</li>
<li>
<code className="font-mono">-q, --quiet</code> — minimal output
</li>
<li>
<code className="font-mono">--no-color</code> — disable colors
</li>
</ul>
</section>
</div>
);
}
@@ -1,261 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/docs/configuration")({
head: () => ({
meta: pageMeta(
"Configuration - Paseo Docs",
"Configure Paseo via config.json, environment variables, and CLI overrides.",
),
}),
component: Configuration,
});
const CUSTOM_PROVIDERS_URL = "https://github.com/getpaseo/paseo/blob/main/docs/CUSTOM-PROVIDERS.md";
function Configuration() {
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-medium font-title mb-4">Configuration</h1>
<p className="text-white/60 leading-relaxed">
Paseo loads configuration from a single JSON file in your Paseo home directory, with
optional environment variable and CLI overrides.
</p>
</div>
<section className="space-y-4">
<h2 className="text-xl font-medium">Where config lives</h2>
<p className="text-white/60 leading-relaxed">
By default, Paseo uses <code className="font-mono">~/.paseo</code> as its home directory.
The configuration file is:
</p>
<div className="bg-card border border-border rounded-lg p-4 font-mono text-sm">
<span className="text-muted-foreground select-none">$ </span>
<span>~/.paseo/config.json</span>
</div>
<p className="text-white/60 leading-relaxed">
You can change the home directory by setting <code className="font-mono">PASEO_HOME</code>{" "}
or passing <code className="font-mono">--home</code> to{" "}
<code className="font-mono">paseo daemon start</code>.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Precedence</h2>
<p className="text-white/60 leading-relaxed">Paseo merges configuration in this order:</p>
<ol className="text-white/60 space-y-2 list-decimal list-inside">
<li>Defaults</li>
<li>
<code className="font-mono">config.json</code>
</li>
<li>Environment variables</li>
<li>CLI flags</li>
</ol>
<p className="text-white/60 leading-relaxed">
Lists append across sources (for example, <code className="font-mono">hostnames</code> and
<code className="font-mono">cors.allowedOrigins</code>).
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Example</h2>
<p className="text-white/60 leading-relaxed">
Minimal example that configures listening address, hostnames, and MCP:
</p>
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
{`{
"$schema": "https://paseo.sh/schemas/paseo.config.v1.json",
"version": 1,
"daemon": {
"listen": "127.0.0.1:6767",
"hostnames": ["localhost", ".localhost"],
"mcp": { "enabled": true }
}
}`}
</pre>
<p className="text-white/60 leading-relaxed">
<code className="font-mono">daemon.hostnames</code> is the primary field. The old{" "}
<code className="font-mono">daemon.allowedHosts</code> name still works as a deprecated
alias for backward compatibility.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Agent providers</h2>
<p className="text-white/60 leading-relaxed">
Agent providers — both the first-class ones Paseo ships with and custom entries you add
under <code className="font-mono">agents.providers</code> — are documented on their own
page.
</p>
<p className="text-white/60 leading-relaxed">
See{" "}
<a href="/docs/providers" className="underline hover:text-white/80">
Providers
</a>{" "}
for first-class providers, how to point Claude at Anthropic-compatible endpoints (Z.AI,
Alibaba/Qwen), multiple profiles, custom binaries, ACP agents, and the{" "}
<code className="font-mono">additionalModels</code> merge behavior. The full field
reference lives on GitHub at{" "}
<a
href={CUSTOM_PROVIDERS_URL}
target="_blank"
rel="noreferrer"
className="underline hover:text-white/80"
>
docs/CUSTOM-PROVIDERS.md
</a>
.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Voice</h2>
<p className="text-white/60 leading-relaxed">
Voice is configured through <code className="font-mono">features.dictation</code> and{" "}
<code className="font-mono">features.voiceMode</code>, with provider credentials under{" "}
<code className="font-mono">providers</code>.
</p>
<p className="text-white/60 leading-relaxed">
For voice philosophy, architecture, and complete local/OpenAI setup examples, see{" "}
<a href="/docs/voice" className="underline hover:text-white/80">
Voice docs
</a>
.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Logging</h2>
<p className="text-white/60 leading-relaxed">
Daemon logging uses separate console and file sinks by default:
</p>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
Console: <code className="font-mono">info</code> and above
</li>
<li>
File (<code className="font-mono">$PASEO_HOME/daemon.log</code>):{" "}
<code className="font-mono">trace</code> and above
</li>
<li>
File rotation: <code className="font-mono">10m</code> max file size,{" "}
<code className="font-mono">2</code> retained files total (active + 1 rotated)
</li>
</ul>
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
{`{
"log": {
"console": {
"level": "info",
"format": "pretty"
},
"file": {
"level": "trace",
"path": "daemon.log",
"rotate": {
"maxSize": "10m",
"maxFiles": 2
}
}
}
}`}
</pre>
<p className="text-white/60 leading-relaxed">
Legacy fields <code className="font-mono">log.level</code> and{" "}
<code className="font-mono">log.format</code> are still supported and map to the new
destination settings.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Common env vars</h2>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
<code className="font-mono">PASEO_HOME</code> — set Paseo home directory
</li>
<li>
<code className="font-mono">PASEO_LISTEN</code> — override{" "}
<code className="font-mono">daemon.listen</code>
</li>
<li>
<code className="font-mono">PASEO_HOSTNAMES</code> — override/extend{" "}
<code className="font-mono">daemon.hostnames</code>
</li>
<li>
<code className="font-mono">PASEO_ALLOWED_HOSTS</code> — deprecated alias for{" "}
<code className="font-mono">PASEO_HOSTNAMES</code>
</li>
<li>
<code className="font-mono">PASEO_LOG_CONSOLE_LEVEL</code> — override{" "}
<code className="font-mono">log.console.level</code>
</li>
<li>
<code className="font-mono">PASEO_LOG_FILE_LEVEL</code> — override{" "}
<code className="font-mono">log.file.level</code>
</li>
<li>
<code className="font-mono">PASEO_LOG_FILE_PATH</code> — override{" "}
<code className="font-mono">log.file.path</code>
</li>
<li>
<code className="font-mono">PASEO_LOG_FILE_ROTATE_SIZE</code> — override{" "}
<code className="font-mono">log.file.rotate.maxSize</code>
</li>
<li>
<code className="font-mono">PASEO_LOG_FILE_ROTATE_COUNT</code> — override{" "}
<code className="font-mono">log.file.rotate.maxFiles</code>
</li>
<li>
<code className="font-mono">PASEO_LOG</code>,{" "}
<code className="font-mono">PASEO_LOG_FORMAT</code> — legacy log overrides (still
supported)
</li>
<li>
<code className="font-mono">OPENAI_API_KEY</code> — override OpenAI provider key
</li>
<li>
<code className="font-mono">PASEO_VOICE_LLM_PROVIDER</code> — override voice LLM
provider (<code className="font-mono">claude</code>,{" "}
<code className="font-mono">codex</code>, <code className="font-mono">opencode</code>)
</li>
<li>
<code className="font-mono">PASEO_DICTATION_STT_PROVIDER</code>,{" "}
<code className="font-mono">PASEO_VOICE_STT_PROVIDER</code>,{" "}
<code className="font-mono">PASEO_VOICE_TTS_PROVIDER</code> — override voice provider
selection (<code className="font-mono">local</code> or{" "}
<code className="font-mono">openai</code>)
</li>
<li>
<code className="font-mono">PASEO_LOCAL_MODELS_DIR</code> — control local model
directory
</li>
<li>
<code className="font-mono">PASEO_DICTATION_LOCAL_STT_MODEL</code> — override local
dictation STT model
</li>
<li>
<code className="font-mono">PASEO_VOICE_LOCAL_STT_MODEL</code>,{" "}
<code className="font-mono">PASEO_VOICE_LOCAL_TTS_MODEL</code> — override local voice
STT/TTS models
</li>
<li>
<code className="font-mono">PASEO_VOICE_LOCAL_TTS_SPEAKER_ID</code>,{" "}
<code className="font-mono">PASEO_VOICE_LOCAL_TTS_SPEED</code> — optional local voice
TTS tuning
</li>
</ul>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Schema</h2>
<p className="text-white/60 leading-relaxed">
For editor autocomplete/validation, set <code className="font-mono">$schema</code> to:
</p>
<div className="bg-card border border-border rounded-lg p-4 font-mono text-sm">
<span>https://paseo.sh/schemas/paseo.config.v1.json</span>
</div>
</section>
</div>
);
}
+14 -163
View File
@@ -1,170 +1,21 @@
import { createFileRoute } from "@tanstack/react-router";
import ReactMarkdown from "react-markdown";
import { getDoc } from "~/docs";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/docs/")({
head: () => ({
meta: pageMeta(
"Getting Started - Paseo Docs",
"Learn how to set up and use Paseo to manage your coding agents from anywhere.",
),
}),
component: GettingStarted,
head: () => {
const doc = getDoc("");
if (!doc) return { meta: pageMeta("Docs - Paseo", "Paseo documentation.") };
return {
meta: pageMeta(`${doc.frontmatter.title} - Paseo Docs`, doc.frontmatter.description),
};
},
component: DocsIndex,
});
function GettingStarted() {
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-medium font-title mb-4">Getting Started</h1>
<p className="text-white/60 leading-relaxed">
Paseo has three main pieces: the daemon is the local server that manages your agents, the
app is the client you use from mobile, web, or desktop, and the CLI is the terminal
interface that can also launch the daemon.
</p>
</div>
<section className="space-y-4">
<h2 className="text-xl font-medium">Prerequisites</h2>
<p className="text-white/60">
Paseo manages existing agent CLIs. Install at least one agent and make sure it already
works with your credentials before you set up Paseo.
</p>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
<a
href="https://docs.anthropic.com/en/docs/claude-code"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-white/80"
>
Claude Code
</a>
</li>
<li>
<a
href="https://github.com/openai/codex"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-white/80"
>
Codex
</a>
</li>
<li>
<a
href="https://github.com/anomalyco/opencode"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-white/80"
>
OpenCode
</a>
</li>
</ul>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Desktop App</h2>
<p className="text-white/60">
Download the desktop app from{" "}
<a
href="https://paseo.sh/download"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-white/80"
>
paseo.sh/download
</a>{" "}
or the{" "}
<a
href="https://github.com/getpaseo/paseo/releases"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-white/80"
>
GitHub releases page
</a>
. Open it and you&apos;re done.
</p>
<p className="text-white/60">
The desktop app bundles and manages its own daemon automatically, so you do not need a
separate CLI install on that machine unless you want it.
</p>
<p className="text-white/60">
On first launch, you may briefly see a startup screen while the local server starts and
the app connects to it. After that, connect from your phone by scanning the QR code in
Settings if you want mobile access.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">CLI / Server</h2>
<p className="text-white/60">
Use this path for headless setups, servers, or remote machines where you want the daemon
running without the desktop app.
</p>
<div className="bg-card border border-border rounded-lg p-4 font-mono text-sm">
<span className="text-muted-foreground select-none">$ </span>
<span>npm install -g @getpaseo/cli</span>
</div>
<div className="bg-card border border-border rounded-lg p-4 font-mono text-sm">
<span className="text-muted-foreground select-none">$ </span>
<span>paseo</span>
</div>
<p className="text-white/60">
Paseo prints a QR code in the terminal. Scan it from the mobile app, or enter the daemon
address manually from another client.
</p>
<p className="text-white/60">
Configuration and local state live under <code>PASEO_HOME</code>.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Voice Setup</h2>
<p className="text-white/60">
Paseo includes first-class voice support with a local-first architecture and configurable
speech providers.
</p>
<p className="text-white/60">
For architecture, local model behavior, and provider configuration, see the Voice docs
page.
</p>
<a href="/docs/voice" className="underline hover:text-white/80">
Voice docs
</a>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Next</h2>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
<a href="/docs/updates" className="underline hover:text-white/80">
Updates
</a>
</li>
<li>
<a href="/docs/voice" className="underline hover:text-white/80">
Voice
</a>
</li>
<li>
<a href="/docs/providers" className="underline hover:text-white/80">
Providers
</a>
</li>
<li>
<a href="/docs/configuration" className="underline hover:text-white/80">
Configuration
</a>
</li>
<li>
<a href="/docs/security" className="underline hover:text-white/80">
Security
</a>
</li>
</ul>
</section>
</div>
);
function DocsIndex() {
const doc = getDoc("");
if (!doc) return <p className="text-muted-foreground">Doc not found.</p>;
return <ReactMarkdown>{doc.content}</ReactMarkdown>;
}
@@ -1,346 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/docs/providers")({
head: () => ({
meta: pageMeta(
"Providers - Paseo Docs",
"First-class agent providers in Paseo, and how to configure custom providers, ACP agents, and profiles.",
),
}),
component: Providers,
});
const CUSTOM_PROVIDERS_URL = "https://github.com/getpaseo/paseo/blob/main/docs/CUSTOM-PROVIDERS.md";
function Providers() {
return (
<div className="space-y-10">
<div>
<h1 className="text-3xl font-medium font-title mb-4">Providers</h1>
<p className="text-white/60 leading-relaxed">
A provider is an agent CLI that Paseo knows how to launch, stream, and control. Paseo
ships with first-class providers for the major coding agents, and lets you add your own
through <code className="font-mono">config.json</code> — either by pointing an existing
provider at a different API, adding extra profiles, or plugging in any{" "}
<a
href="https://agentclientprotocol.com"
target="_blank"
rel="noreferrer"
className="underline hover:text-white/80"
>
ACP
</a>
-compatible agent.
</p>
</div>
<section className="space-y-4">
<h2 className="text-xl font-medium">First-class providers</h2>
<p className="text-white/60 leading-relaxed">
These work out of the box once the underlying CLI is installed and authenticated. Paseo
discovers them automatically, wires up modes, and exposes them in the app and CLI.
</p>
<ul className="text-white/60 space-y-3 list-disc list-inside">
<li>
<code className="font-mono">claude</code> — Anthropic&apos;s Claude Code. Multi-tool
assistant with MCP support, streaming, and deep reasoning.
</li>
<li>
<code className="font-mono">codex</code> — OpenAI&apos;s Codex workspace agent with
sandbox controls and optional network access.
</li>
<li>
<code className="font-mono">opencode</code> — Open-source coding assistant with
multi-provider model support.
</li>
<li>
<code className="font-mono">copilot</code> — GitHub Copilot via ACP, with dynamic modes
and session support.
</li>
<li>
<code className="font-mono">pi</code> — Minimal terminal-based coding agent with
multi-provider LLM support.
</li>
</ul>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Custom providers</h2>
<p className="text-white/60 leading-relaxed">
Everything beyond the defaults lives under{" "}
<code className="font-mono">agents.providers</code> in{" "}
<code className="font-mono">~/.paseo/config.json</code>. You can:
</p>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
<strong>Extend</strong> a first-class provider to point at a different API (Z.AI,
Alibaba/Qwen, a proxy, a self-hosted endpoint).
</li>
<li>
<strong>Add profiles</strong> — multiple entries against the same underlying provider
with different credentials or curated model lists.
</li>
<li>
<strong>Override the binary</strong> — run a nightly build, a wrapper script, or a
Docker image instead of the installed CLI.
</li>
<li>
<strong>Add ACP agents</strong> — Gemini CLI, Hermes, or any agent speaking the Agent
Client Protocol over stdio.
</li>
<li>
<strong>Disable</strong> a provider you don&apos;t use.
</li>
</ul>
<p className="text-white/60 leading-relaxed">
Provider IDs must be lowercase alphanumeric with hyphens (
<code className="font-mono">/^[a-z][a-z0-9-]*$/</code>). Every custom entry needs{" "}
<code className="font-mono">extends</code> (a first-class provider ID or{" "}
<code className="font-mono">&quot;acp&quot;</code>) and a{" "}
<code className="font-mono">label</code>.
</p>
<p className="text-white/60 leading-relaxed">
The examples below are a quick tour. The full, up-to-date reference is on GitHub:{" "}
<a
href={CUSTOM_PROVIDERS_URL}
target="_blank"
rel="noreferrer"
className="underline hover:text-white/80"
>
docs/CUSTOM-PROVIDERS.md
</a>
.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Extending a first-class provider</h2>
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
{`{
"agents": {
"providers": {
"my-claude": {
"extends": "claude",
"label": "My Claude",
"env": {
"ANTHROPIC_API_KEY": "sk-ant-...",
"ANTHROPIC_BASE_URL": "https://my-proxy.example.com/v1"
}
}
}
}
}`}
</pre>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Z.AI (GLM) coding plan</h2>
<p className="text-white/60 leading-relaxed">
Z.AI exposes GLM models through an Anthropic-compatible endpoint. Point{" "}
<code className="font-mono">ANTHROPIC_BASE_URL</code> at their API and use{" "}
<code className="font-mono">ANTHROPIC_AUTH_TOKEN</code> for the key. Third-party endpoints
don&apos;t support Anthropic&apos;s server-side tools, so disable{" "}
<code className="font-mono">WebSearch</code>.
</p>
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
{`{
"agents": {
"providers": {
"zai": {
"extends": "claude",
"label": "ZAI",
"env": {
"ANTHROPIC_AUTH_TOKEN": "<your-zai-api-key>",
"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic",
"API_TIMEOUT_MS": "3000000"
},
"disallowedTools": ["WebSearch"],
"models": [
{ "id": "glm-5-turbo", "label": "GLM 5 Turbo", "isDefault": true },
{ "id": "glm-5.1", "label": "GLM 5.1" }
]
}
}
}
}`}
</pre>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Alibaba Cloud (Qwen) coding plan</h2>
<p className="text-white/60 leading-relaxed">
Alibaba&apos;s coding plan routes Claude Code to Qwen models via an Anthropic-compatible
API. Subscription keys look like <code className="font-mono">sk-sp-...</code> and must be
created in the Singapore region.
</p>
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
{`{
"agents": {
"providers": {
"qwen": {
"extends": "claude",
"label": "Qwen (Alibaba)",
"env": {
"ANTHROPIC_AUTH_TOKEN": "sk-sp-<coding-plan-key>",
"ANTHROPIC_BASE_URL": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic"
},
"disallowedTools": ["WebSearch"],
"models": [
{ "id": "qwen3.5-plus", "label": "Qwen 3.5 Plus", "isDefault": true },
{ "id": "qwen3-coder-next", "label": "Qwen 3 Coder Next" }
]
}
}
}
}`}
</pre>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Multiple profiles</h2>
<p className="text-white/60 leading-relaxed">
Create as many entries as you want against the same first-class provider. Each one shows
up as a separate option in the app with its own credentials and models.
</p>
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
{`{
"agents": {
"providers": {
"claude-work": {
"extends": "claude",
"label": "Claude (Work)",
"env": { "ANTHROPIC_API_KEY": "sk-ant-work-..." }
},
"claude-personal": {
"extends": "claude",
"label": "Claude (Personal)",
"env": { "ANTHROPIC_API_KEY": "sk-ant-personal-..." }
}
}
}
}`}
</pre>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Custom binary</h2>
<p className="text-white/60 leading-relaxed">
<code className="font-mono">command</code> is an array — first element is the binary, the
rest are arguments. It fully replaces the default launch command for that provider.
</p>
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
{`{
"agents": {
"providers": {
"claude": {
"command": ["/opt/claude-nightly/claude"]
}
}
}
}`}
</pre>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">ACP providers</h2>
<p className="text-white/60 leading-relaxed">
Any agent that speaks{" "}
<a
href="https://agentclientprotocol.com"
target="_blank"
rel="noreferrer"
className="underline hover:text-white/80"
>
ACP
</a>{" "}
over stdio can be added with <code className="font-mono">extends: &quot;acp&quot;</code>{" "}
and a <code className="font-mono">command</code>. Paseo spawns the process, sends an{" "}
<code className="font-mono">initialize</code> JSON-RPC request, and the agent reports its
capabilities, modes, and models at runtime.
</p>
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
{`{
"agents": {
"providers": {
"gemini": {
"extends": "acp",
"label": "Google Gemini",
"command": ["gemini", "--acp"]
},
"hermes": {
"extends": "acp",
"label": "Hermes",
"command": ["hermes", "acp"]
}
}
}
}`}
</pre>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Adding or relabeling models</h2>
<p className="text-white/60 leading-relaxed">
<code className="font-mono">models</code> replaces the model list entirely.{" "}
<code className="font-mono">additionalModels</code> merges with runtime-discovered models
(ACP) or with <code className="font-mono">models</code> — use it to add an extra entry or
relabel a discovered one without redeclaring the full list. An entry with the same{" "}
<code className="font-mono">id</code> as a discovered model updates it in place.
</p>
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
{`{
"agents": {
"providers": {
"gemini": {
"extends": "acp",
"label": "Google Gemini",
"command": ["gemini", "--acp"],
"additionalModels": [
{ "id": "experimental-model", "label": "Experimental", "isDefault": true },
{ "id": "gemini-2.5-pro", "label": "Gemini 2.5 Pro (preferred)" }
]
}
}
}
}`}
</pre>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Disabling a provider</h2>
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
{`{
"agents": {
"providers": {
"copilot": { "enabled": false }
}
}
}`}
</pre>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Full reference</h2>
<p className="text-white/60 leading-relaxed">
For the complete field reference (<code className="font-mono">extends</code>,{" "}
<code className="font-mono">label</code>, <code className="font-mono">command</code>,{" "}
<code className="font-mono">env</code>, <code className="font-mono">models</code>,{" "}
<code className="font-mono">additionalModels</code>,{" "}
<code className="font-mono">disallowedTools</code>,{" "}
<code className="font-mono">enabled</code>, <code className="font-mono">order</code>),
model and thinking-option schemas, and deeper examples for each plan, see{" "}
<a
href={CUSTOM_PROVIDERS_URL}
target="_blank"
rel="noreferrer"
className="underline hover:text-white/80"
>
docs/CUSTOM-PROVIDERS.md
</a>{" "}
on GitHub.
</p>
</section>
</div>
);
}
@@ -1,289 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/docs/security")({
head: () => ({
meta: pageMeta(
"Security - Paseo Docs",
"Security model for Paseo: architecture overview, connection methods, relay encryption, and best practices.",
),
}),
component: Security,
});
function Callout({ children }: { children: React.ReactNode }) {
return (
<div className="bg-primary/10 border border-primary/30 rounded-lg p-4 text-white/80">
{children}
</div>
);
}
function Security() {
return (
<div className="space-y-10">
<div>
<h1 className="text-3xl font-medium font-title mb-4">Security</h1>
<p className="text-white/60 leading-relaxed">
Paseo follows a client-server architecture, similar to Docker. The daemon runs on your
machine and manages your coding agents. Clients (the mobile app, CLI, or web interface)
connect to the daemon to monitor and control those agents.
</p>
<p className="text-white/60 leading-relaxed mt-3">
Your code never leaves your machine. Paseo is a local-first tool that connects directly to
your development environment.
</p>
</div>
{/* Architecture Overview */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Architecture</h2>
<p className="text-white/60 leading-relaxed">
The Paseo daemon can run anywhere you want to execute agents: your laptop, a Mac Mini, a
VPS, or a Docker container. The daemon listens for connections and manages agent
lifecycles.
</p>
<p className="text-white/60 leading-relaxed">
Clients connect to the daemon over WebSocket. There are two ways to establish this
connection:
</p>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
<strong className="text-white/80">Relay connection (recommended)</strong> — The daemon
connects outbound to our relay server, and clients meet it there. No open ports
required.
</li>
<li>
<strong className="text-white/80">Direct connection</strong> — The daemon listens on a
network address and clients connect directly
</li>
</ul>
</section>
{/* Relay Connection */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Relay connections (recommended)</h2>
<p className="text-white/60 leading-relaxed">
The relay is the simplest way to connect from your phone. It requires no VPN setup, no
port forwarding, and no firewall configuration. The daemon can stay bound to localhost or
a socket file — it connects <em>outbound</em> to the relay, and your phone meets it there.
</p>
<Callout>
<strong>The relay is designed to be untrusted.</strong> All traffic between your phone and
daemon is end-to-end encrypted. The relay server cannot read your messages, see your code,
or modify traffic without detection. Even if the relay is compromised, your data remains
protected.
</Callout>
<h3 className="text-lg font-medium mt-6">How it works</h3>
<ol className="text-white/60 space-y-2 list-decimal list-inside">
<li>
The daemon generates a persistent ECDH keypair and stores it in{" "}
<code className="font-mono">$PASEO_HOME/daemon-keypair.json</code>
</li>
<li>
When you scan the QR code or click the pairing link, your phone receives the
daemon&apos;s public key
</li>
<li>
Your phone sends a handshake message with its own public key. The daemon will not accept
any commands until this handshake completes.
</li>
<li>
Both sides perform an ECDH key exchange to derive a shared secret. All subsequent
messages are encrypted with AES-256-GCM.
</li>
</ol>
<p className="text-white/60 leading-relaxed mt-3">
The relay sees only: IP addresses, timing, message sizes, and session IDs. It cannot read
message contents, forge messages, or derive encryption keys from observing the handshake.
</p>
<h3 className="text-lg font-medium mt-6">Why the relay can&apos;t attack you</h3>
<p className="text-white/60 leading-relaxed">
The daemon requires a valid cryptographic handshake before processing any commands. A
compromised relay cannot:
</p>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
<strong className="text-white/80">Send commands</strong> — Without your phone&apos;s
private key, it cannot complete the handshake
</li>
<li>
<strong className="text-white/80">Read your traffic</strong> — All messages are
encrypted with AES-256-GCM after the handshake
</li>
<li>
<strong className="text-white/80">Forge messages</strong> — GCM provides authenticated
encryption; tampered messages are rejected
</li>
<li>
<strong className="text-white/80">Replay old messages</strong> — Each session derives
fresh encryption keys
</li>
</ul>
<h3 className="text-lg font-medium mt-6">Trust model</h3>
<p className="text-white/60 leading-relaxed">
The QR code or pairing link is the trust anchor. It contains the daemon&apos;s public key,
which is required to establish the encrypted connection. Treat it like a password —
don&apos;t share it publicly.
</p>
<p className="text-white/60 leading-relaxed">
If you believe a pairing offer has been compromised, restart the daemon to generate a new
session ID and rotate the relay pairing.
</p>
</section>
{/* Direct Connection */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Direct connections</h2>
<p className="text-white/60 leading-relaxed">
By default, the daemon listens on <code className="font-mono">127.0.0.1:6767</code>{" "}
(localhost only). This is safe for local CLI usage but not reachable from your phone or
other devices.
</p>
<h3 className="text-lg font-medium mt-6">Socket file (CLI only)</h3>
<p className="text-white/60 leading-relaxed">
For maximum isolation, you can configure the daemon to listen on a Unix socket file
instead of a TCP port. This prevents any network access entirely — only processes on the
same machine can connect. The CLI supports this mode, but the mobile app and web interface
require a network connection.
</p>
<h3 className="text-lg font-medium mt-6">VPN access</h3>
<p className="text-white/60 leading-relaxed">
If you prefer direct connections over the relay, you can use a VPN like{" "}
<a
href="https://tailscale.com"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-white/80"
>
Tailscale
</a>
. Tailscale creates a private network between your devices, so you can access your daemon
without exposing it to the public internet.
</p>
<p className="text-white/60 leading-relaxed">To set this up:</p>
<ol className="text-white/60 space-y-2 list-decimal list-inside">
<li>
Install Tailscale on your machine and phone and join them to the same{" "}
<a
href="https://tailscale.com/kb/1136/tailnet"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-white/80"
>
tailnet
</a>
</li>
<li>
Configure the daemon to listen on your Tailscale IP (e.g.,{" "}
<code className="font-mono">100.x.y.z:6767</code>)
</li>
<li>
Add your Tailscale hostname to <code className="font-mono">hostnames</code> and{" "}
<code className="font-mono">cors.allowedOrigins</code>
</li>
<li>
Add the daemon as a direct connection in the Paseo app using the Tailscale address
</li>
</ol>
<h3 className="text-lg font-medium mt-6">Binding to 0.0.0.0</h3>
<div className="bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-4 text-white/80">
<strong>Warning:</strong> Binding to <code className="font-mono">0.0.0.0</code> makes the
daemon reachable on all network interfaces, including public Wi-Fi and local networks.
This can expose your daemon to unauthorized access. If you must bind to all interfaces,
ensure you have proper firewall rules and review your{" "}
<code className="font-mono">hostnames</code> configuration.
</div>
</section>
{/* DNS Rebinding Protection */}
<section className="space-y-4">
<h2 className="text-xl font-medium">DNS rebinding protection</h2>
<p className="text-white/60 leading-relaxed">
<strong className="text-white/80">CORS is not a complete security boundary.</strong> It
controls which browser origins can make requests, but does not prevent a malicious website
from resolving its domain to your local machine (DNS rebinding).
</p>
<p className="text-white/60 leading-relaxed">
Paseo uses a host allowlist to validate the <code className="font-mono">Host</code> header
on incoming requests. Requests with unrecognized hosts are rejected.
</p>
<p className="text-white/60 leading-relaxed">
Configure via <code className="font-mono">daemon.hostnames</code> in{" "}
<code className="font-mono">config.json</code>:
</p>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
Default (<code className="font-mono">[]</code>): allow{" "}
<code className="font-mono">localhost</code>,{" "}
<code className="font-mono">*.localhost</code>, and all IP addresses
</li>
<li>
<code className="font-mono">[&apos;.example.com&apos;]</code>: allow{" "}
<code className="font-mono">example.com</code> and any subdomain, plus defaults
</li>
<li>
<code className="font-mono">true</code>: allow any host (not recommended)
</li>
</ul>
</section>
{/* Agent Authentication */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Agent authentication</h2>
<p className="text-white/60 leading-relaxed">
Paseo wraps agent CLIs (Claude Code, Codex, OpenCode) but does not manage their
authentication. Each agent provider handles its own credentials:
</p>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
<strong className="text-white/80">Claude Code</strong> — authenticates via
Anthropic&apos;s OAuth flow, stored in <code className="font-mono">~/.claude/</code>
</li>
<li>
<strong className="text-white/80">Codex</strong> — uses your OpenAI API key or OAuth
session
</li>
<li>
<strong className="text-white/80">OpenCode</strong> — configured via provider-specific
API keys
</li>
</ul>
<p className="text-white/60 leading-relaxed">
Paseo never stores or transmits provider API keys. Agents run in your user context with
your existing credentials.
</p>
</section>
{/* Best Practices Summary */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Recommendations</h2>
<ul className="text-white/60 space-y-3 list-disc list-inside">
<li>
<strong className="text-white/80">Use the relay</strong> for mobile access — it&apos;s
the simplest option and all traffic is end-to-end encrypted
</li>
<li>
<strong className="text-white/80">Treat the QR code like a password</strong> — anyone
with the pairing offer can connect to your daemon
</li>
<li>
<strong className="text-white/80">Never bind to 0.0.0.0</strong> unless you understand
the implications and have proper firewall rules
</li>
<li>
<strong className="text-white/80">Keep your daemon updated</strong> — security
improvements are released regularly
</li>
</ul>
</section>
</div>
);
}
-165
View File
@@ -1,165 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/docs/skills")({
head: () => ({
meta: pageMeta(
"Orchestration Skills - Paseo Docs",
"Paseo orchestration skills: teach coding agents to spawn, coordinate, and manage other agents using slash commands.",
),
}),
component: Skills,
});
function Code({ children }: { children: React.ReactNode }) {
return (
<div className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto">
{children}
</div>
);
}
function Skills() {
return (
<div className="space-y-10">
<div>
<h1 className="text-3xl font-medium font-title mb-4">Orchestration Skills</h1>
<p className="text-white/60 leading-relaxed">
Paseo ships orchestration skills that teach coding agents (Claude Code, Codex) how to use
the Paseo CLI to spawn, coordinate, and manage other agents. Skills are slash commands
your agent can invoke — they provide the prompts, context, and workflows so agents know
how to orchestrate without you writing boilerplate. Install them from the desktop
app&apos;s Integrations settings or via the CLI.
</p>
</div>
{/* Installation */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Installation</h2>
<p className="text-white/60 leading-relaxed">Two ways to install:</p>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
<strong>Desktop app:</strong> Settings → Integrations → Install
</li>
<li>
<strong>Manual:</strong>{" "}
<code className="font-mono">npx skills add getpaseo/paseo</code> — this installs to{" "}
<code className="font-mono">~/.agents/skills/</code> and sets up symlinks for each
agent.
</li>
</ul>
</section>
{/* /paseo */}
<section className="space-y-4">
<h2 className="text-xl font-medium">
<code className="font-mono">/paseo</code> — CLI Reference
</h2>
<p className="text-white/60 leading-relaxed">
The foundational skill. Loaded automatically by other skills. Contains the full Paseo CLI
command reference so agents know how to run commands.
</p>
<p className="text-white/60 leading-relaxed">
Not typically invoked directly by users — it&apos;s a reference that other skills depend
on.
</p>
</section>
{/* /paseo-handoff */}
<section className="space-y-4">
<h2 className="text-xl font-medium">
<code className="font-mono">/paseo-handoff</code> — Task Handoff
</h2>
<p className="text-white/60 leading-relaxed">
Hands off your current task to another agent with full context. The receiving agent gets a
comprehensive prompt with: task description, relevant files, what&apos;s been tried,
decisions made, and acceptance criteria.
</p>
<p className="text-white/60 leading-relaxed">
Default provider is Codex. Can specify Claude (sonnet/opus). Supports{" "}
<code className="font-mono">--worktree</code> for isolated git branches.
</p>
<Code>
<pre className="text-white/80">{`/paseo-handoff hand off the auth fix to codex in a worktree
/paseo-handoff hand this to claude opus for review`}</pre>
</Code>
</section>
{/* /paseo-loop */}
<section className="space-y-4">
<h2 className="text-xl font-medium">
<code className="font-mono">/paseo-loop</code> — Iterative Loops
</h2>
<p className="text-white/60 leading-relaxed">
Runs an agent in a loop with automatic verification until an exit condition is met. Worker
runs, verifier checks, repeat until done or max iterations. Supports different providers
for worker vs verifier (e.g., Codex implements, Claude verifies).
</p>
<p className="text-white/60 leading-relaxed">
Stop conditions: <code className="font-mono">--max-iterations</code>,{" "}
<code className="font-mono">--max-time</code>, or verification passes.
</p>
<Code>
<pre className="text-white/80">{`/paseo-loop fix the failing tests, verify with npm test, max 5 iterations
/paseo-loop use codex to implement, claude sonnet to verify, loop until tests pass`}</pre>
</Code>
</section>
{/* /paseo-orchestrator */}
<section className="space-y-4">
<h2 className="text-xl font-medium">
<code className="font-mono">/paseo-orchestrator</code> — Team Orchestration
</h2>
<p className="text-white/60 leading-relaxed">
Builds and manages a team of agents coordinating through a shared chat room. You describe
the work, it sets up roles, launches agents, and coordinates through chat. Uses a
heartbeat schedule to check progress.
</p>
<p className="text-white/60 leading-relaxed">
Cross-provider: typically Codex for implementation, Claude for review.
</p>
<Code>
<pre className="text-white/80">{`/paseo-orchestrator spin up a team to implement the database migration, codex implements, claude reviews`}</pre>
</Code>
</section>
{/* /paseo-chat */}
<section className="space-y-4">
<h2 className="text-xl font-medium">
<code className="font-mono">/paseo-chat</code> — Chat Rooms
</h2>
<p className="text-white/60 leading-relaxed">
Use persistent chat rooms for asynchronous agent coordination. Create rooms, post
messages, read history, wait for replies. Supports @mentions for specific agents or
@everyone.
</p>
<p className="text-white/60 leading-relaxed">
Typically used by the orchestrator skill, but can be used directly.
</p>
<Code>
<pre className="text-white/80">{`/paseo-chat create a room called "backend-refactor" for coordinating the API changes
/paseo-chat post to backend-refactor: "API endpoints are done, ready for review"`}</pre>
</Code>
</section>
{/* /paseo-committee */}
<section className="space-y-4">
<h2 className="text-xl font-medium">
<code className="font-mono">/paseo-committee</code> — Committee Planning
</h2>
<p className="text-white/60 leading-relaxed">
Forms a committee of two high-reasoning agents (Claude Opus + GPT 5.4) to analyze a
problem before implementing. Both agents reason in parallel, then plans are merged. Useful
when stuck, looping, or facing a hard architectural decision.
</p>
<p className="text-white/60 leading-relaxed">
Agents are prevented from editing code — they only produce a plan.
</p>
<Code>
<pre className="text-white/80">{`/paseo-committee why are the websocket connections dropping under load?
/paseo-committee plan the auth system migration`}</pre>
</Code>
</section>
</div>
);
}
@@ -1,86 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/docs/updates")({
head: () => ({
meta: pageMeta(
"Updates - Paseo Docs",
"How to update Paseo daemon and apps across web, desktop, and mobile.",
),
}),
component: UpdatesDocs,
});
function UpdatesDocs() {
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-medium font-title mb-4">Updates</h1>
<p className="text-white/60 leading-relaxed">
Keep your daemon and apps current to get the latest fixes and features.
</p>
</div>
<section className="space-y-4">
<h2 className="text-xl font-medium">Version compatibility</h2>
<p className="text-white/60">
For now, daemon and app versions should be kept in lockstep. If your daemon is version X,
make sure your clients are also version X.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Update the daemon</h2>
<p className="text-white/60">Install the latest CLI/daemon package globally:</p>
<div className="bg-card border border-border rounded-lg p-4 font-mono text-sm">
<span className="text-muted-foreground select-none">$ </span>
<span>npm install -g @getpaseo/cli@latest</span>
</div>
<p className="text-white/60">Then restart the daemon:</p>
<div className="bg-card border border-border rounded-lg p-4 font-mono text-sm">
<span className="text-muted-foreground select-none">$ </span>
<span>paseo daemon restart</span>
</div>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Web app</h2>
<p className="text-white/60">
<a
href="https://app.paseo.sh"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-white/80"
>
app.paseo.sh
</a>{" "}
is always up to date. No manual update needed.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Desktop app</h2>
<p className="text-white/60">
Download the latest desktop build from the GitHub releases page and install it over your
current version.
</p>
<a
href="https://github.com/getpaseo/paseo/releases"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-white/80"
>
Paseo releases
</a>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Mobile apps</h2>
<p className="text-white/60">
Mobile apps are available on the App Store and Play Store. Update through your respective
store. Store versions may lag behind the latest release due to review processes.
</p>
</section>
</div>
);
}
-152
View File
@@ -1,152 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/docs/voice")({
head: () => ({
meta: pageMeta(
"Voice - Paseo Docs",
"Paseo voice architecture, local-first model execution, and provider configuration.",
),
}),
component: VoiceDocs,
});
function VoiceDocs() {
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-medium font-title mb-4">Voice</h1>
<p className="text-white/60 leading-relaxed">
Paseo has first-class voice support for dictation and realtime conversations with your
coding environment.
</p>
</div>
<section className="space-y-4">
<h2 className="text-xl font-medium">Philosophy</h2>
<p className="text-white/60 leading-relaxed">
Voice is local-first. You can run speech fully on-device, or choose OpenAI for speech
features. For voice reasoning/orchestration, Paseo reuses agent providers already
installed and authenticated on your machine.
</p>
<p className="text-white/60 leading-relaxed">
This keeps credentials and execution in your environment and avoids introducing a separate
cloud-only voice stack.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Architecture</h2>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
Speech I/O: STT and TTS providers per feature (<code className="font-mono">local</code>{" "}
or <code className="font-mono">openai</code>)
</li>
<li>Local speech runtime: ONNX models executed on CPU by default</li>
<li>
Voice LLM orchestration: hidden agent session using your configured provider (
<code className="font-mono">claude</code>, <code className="font-mono">codex</code>, or{" "}
<code className="font-mono">opencode</code>)
</li>
<li>Tooling path: MCP stdio bridge for voice tools and agent control</li>
</ul>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Local Speech</h2>
<p className="text-white/60 leading-relaxed">
Local speech defaults to model IDs{" "}
<code className="font-mono">parakeet-tdt-0.6b-v3-int8</code> (STT) and{" "}
<code className="font-mono">kokoro-en-v0_19</code> (TTS, speaker 0 / voice 00).
</p>
<p className="text-white/60 leading-relaxed">
Missing models are downloaded at daemon startup into{" "}
<code className="font-mono">$PASEO_HOME/models/local-speech</code>. Downloads happen only
for missing files.
</p>
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
{`{
"version": 1,
"features": {
"dictation": { "stt": { "provider": "local", "model": "parakeet-tdt-0.6b-v3-int8" } },
"voiceMode": {
"llm": { "provider": "claude", "model": "haiku" },
"stt": { "provider": "local", "model": "parakeet-tdt-0.6b-v3-int8" },
"tts": { "provider": "local", "model": "kokoro-en-v0_19", "speakerId": 0 }
}
},
"providers": {
"local": {
"modelsDir": "~/.paseo/models/local-speech"
}
}
}`}
</pre>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">OpenAI Speech Option</h2>
<p className="text-white/60 leading-relaxed">
You can switch dictation, voice STT, and voice TTS to OpenAI by setting provider fields to{" "}
<code className="font-mono">openai</code> and providing{" "}
<code className="font-mono">OPENAI_API_KEY</code>.
</p>
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
{`{
"version": 1,
"features": {
"dictation": { "stt": { "provider": "openai" } },
"voiceMode": {
"stt": { "provider": "openai" },
"tts": { "provider": "openai" }
}
},
"providers": {
"openai": { "apiKey": "..." }
}
}`}
</pre>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Environment Variables</h2>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
<code className="font-mono">OPENAI_API_KEY</code> — OpenAI speech credentials
</li>
<li>
<code className="font-mono">PASEO_VOICE_LLM_PROVIDER</code> — voice agent provider
override
</li>
<li>
<code className="font-mono">PASEO_LOCAL_MODELS_DIR</code> — local model storage
directory
</li>
<li>
<code className="font-mono">PASEO_DICTATION_LOCAL_STT_MODEL</code> — local dictation STT
model ID
</li>
<li>
<code className="font-mono">PASEO_VOICE_LOCAL_STT_MODEL</code>,{" "}
<code className="font-mono">PASEO_VOICE_LOCAL_TTS_MODEL</code> — local voice STT/TTS
model IDs
</li>
<li>
<code className="font-mono">PASEO_VOICE_LOCAL_TTS_SPEAKER_ID</code>,{" "}
<code className="font-mono">PASEO_VOICE_LOCAL_TTS_SPEED</code> — optional local voice
TTS tuning
</li>
</ul>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Operational Notes</h2>
<p className="text-white/60 leading-relaxed">
Realtime voice can launch and control agents. Treat voice prompts with the same care as
direct agent instructions, especially when specifying working directories or destructive
operations.
</p>
</section>
</div>
);
}
@@ -1,258 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/docs/worktrees")({
head: () => ({
meta: pageMeta(
"Git Worktrees - Paseo Docs",
"Run agents in isolated git worktrees with setup hooks, scripts, and long-running services.",
),
}),
component: Worktrees,
});
function Code({ children }: { children: React.ReactNode }) {
return (
<div className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto">
{children}
</div>
);
}
function Worktrees() {
return (
<div className="space-y-10">
<div>
<h1 className="text-3xl font-medium font-title mb-4">Git Worktrees</h1>
<p className="text-white/60 leading-relaxed">
Each agent runs in its own git worktree — a separate directory on a separate branch — so
parallel agents never step on each other. You configure setup, scripts, and long-running
services through a <code className="font-mono">paseo.json</code> file at your repo root.
</p>
</div>
{/* Layout & workflow */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Layout and workflow</h2>
<p className="text-white/60 leading-relaxed">
Worktrees live under <code className="font-mono">$PASEO_HOME/worktrees/</code>, grouped by
a hash of the source checkout path. Each worktree gets a random slug; the branch name is
chosen when you first launch an agent.
</p>
<Code>
<pre className="text-white/80">{`~/.paseo/worktrees/
└── 1vnnm9k3/ # hash of source checkout path
├── tidy-fox/ # worktree slug (branch set on first agent)
└── bold-owl/`}</pre>
</Code>
<ol className="text-white/60 space-y-2 list-decimal list-inside">
<li>Create a worktree — Paseo runs your setup hooks</li>
<li>Launch an agent — a branch is created or assigned</li>
<li>Review the diff against the base branch</li>
<li>Merge or archive — archive runs teardown and removes the directory</li>
</ol>
</section>
{/* paseo.json */}
<section className="space-y-4">
<h2 className="text-xl font-medium">paseo.json</h2>
<p className="text-white/60 leading-relaxed">
Drop a <code className="font-mono">paseo.json</code> in your repo root. Paseo reads it
from the committed version of the base branch you picked, so uncommitted changes in other
branches don&apos;t apply.
</p>
<Code>
<pre className="text-white/80">{`{
"worktree": {
"setup": "npm ci",
"teardown": "rm -rf .cache"
},
"scripts": {
"test": { "command": "npm test" },
"web": { "command": "npm run dev", "type": "service", "port": 3000 }
}
}`}</pre>
</Code>
</section>
{/* Setup & teardown */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Setup and teardown</h2>
<p className="text-white/60 leading-relaxed">
<code className="font-mono">setup</code> runs once after the worktree is created. A fresh
worktree has no installed dependencies and no ignored files (like{" "}
<code className="font-mono">.env</code>), so use setup to install and copy what you need.
<code className="font-mono">teardown</code> runs during archive, before the directory is
removed.
</p>
<Code>
<pre className="text-white/80">{`{
"worktree": {
"setup": "npm ci\\ncp \\"$PASEO_SOURCE_CHECKOUT_PATH/.env\\" .env\\nnpm run db:migrate",
"teardown": "npm run db:drop || true"
}
}`}</pre>
</Code>
<p className="text-white/60 leading-relaxed">
Both fields accept a multiline shell script or an array of commands; commands run
sequentially either way.
</p>
<p className="text-white/60 leading-relaxed">
Commands run with the worktree as <code className="font-mono">cwd</code>. Use{" "}
<code className="font-mono">$PASEO_SOURCE_CHECKOUT_PATH</code> to reach files in the
original checkout (untracked config, local caches, etc).
</p>
</section>
{/* Scripts & services */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Scripts and services</h2>
<p className="text-white/60 leading-relaxed">
<code className="font-mono">scripts</code> are named commands you can run inside a
worktree on demand. Mark one as a <em>service</em> and Paseo supervises it as a
long-running process, assigns it a port, and routes HTTP traffic to it through the
daemon&apos;s reverse proxy.
</p>
<h3 className="text-lg font-medium mt-4">Plain scripts</h3>
<Code>
<pre className="text-white/80">{`{
"scripts": {
"test": { "command": "npm test" },
"lint": { "command": "npm run lint" },
"generate": { "command": "npm run codegen" }
}
}`}</pre>
</Code>
<h3 className="text-lg font-medium mt-6">Services</h3>
<Code>
<pre className="text-white/80">{`{
"scripts": {
"web": {
"type": "service",
"command": "npm run dev -- --port $PASEO_PORT",
"port": 3000
},
"api": {
"type": "service",
"command": "npm run api -- --port $PASEO_PORT"
}
}
}`}</pre>
</Code>
<p className="text-white/60 leading-relaxed">
Omit <code className="font-mono">port</code> to let Paseo auto-assign one. Bind your
process to <code className="font-mono">$PASEO_PORT</code> rather than hard-coding — each
worktree gets a distinct port so multiple copies of the same service coexist.
</p>
<h3 className="text-lg font-medium mt-6">Reverse proxy</h3>
<p className="text-white/60 leading-relaxed">
Every service is reachable through the daemon at a deterministic hostname:
</p>
<Code>
<pre className="text-white/80">{`http://<script>.<branch>.<project>.localhost:<daemon-port>
# on the default branch, the branch label is dropped:
http://<script>.<project>.localhost:<daemon-port>`}</pre>
</Code>
<p className="text-white/60 leading-relaxed">
<code className="font-mono">*.localhost</code> resolves to{" "}
<code className="font-mono">127.0.0.1</code> on modern systems, so these URLs work out of
the box. The proxy supports WebSocket upgrades.
</p>
<h3 className="text-lg font-medium mt-6">Service-to-service</h3>
<p className="text-white/60 leading-relaxed">
Services launched from the same workspace see each other&apos;s ports and proxy URLs.
Given <code className="font-mono">web</code> and <code className="font-mono">api</code>{" "}
above, each process gets:
</p>
<Code>
<pre className="text-white/80">{`PASEO_PORT=3000 # this service's port
PASEO_URL=http://web.my-app.localhost:6767 # this service's proxy URL
PASEO_SERVICE_API_PORT=51732
PASEO_SERVICE_API_URL=http://api.my-app.localhost:6767
PASEO_SERVICE_WEB_PORT=3000
PASEO_SERVICE_WEB_URL=http://web.my-app.localhost:6767`}</pre>
</Code>
<p className="text-white/60 leading-relaxed">
Script names are upper-cased and non-alphanumerics become{" "}
<code className="font-mono">_</code>. Point your frontend at{" "}
<code className="font-mono">$PASEO_SERVICE_API_URL</code> instead of hard-coding a port.
</p>
</section>
{/* Terminals */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Terminals</h2>
<p className="text-white/60 leading-relaxed">
Open terminals automatically when a worktree is created. Useful for tailing logs or
leaving a REPL ready to go.
</p>
<Code>
<pre className="text-white/80">{`{
"worktree": {
"terminals": [
{ "name": "logs", "command": "tail -f dev.log" },
{ "name": "shell", "command": "bash" }
]
}
}`}</pre>
</Code>
</section>
{/* Env vars */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Environment variables</h2>
<p className="text-white/60 leading-relaxed">
Setup, teardown, scripts, and services all see:
</p>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
<code className="font-mono">$PASEO_SOURCE_CHECKOUT_PATH</code> — the original repo root
</li>
<li>
<code className="font-mono">$PASEO_WORKTREE_PATH</code> — the worktree directory
</li>
<li>
<code className="font-mono">$PASEO_BRANCH_NAME</code> — the worktree&apos;s branch
</li>
<li>
<code className="font-mono">$PASEO_WORKTREE_PORT</code> — legacy per-worktree port
(prefer <code className="font-mono">$PASEO_PORT</code> inside services)
</li>
</ul>
<p className="text-white/60 leading-relaxed">Services additionally get:</p>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
<code className="font-mono">$PASEO_PORT</code> — this service&apos;s assigned port
</li>
<li>
<code className="font-mono">$PASEO_URL</code> — this service&apos;s proxy URL
</li>
<li>
<code className="font-mono">$PASEO_SERVICE_&lt;NAME&gt;_PORT</code> /{" "}
<code className="font-mono">_URL</code> — peer service ports and URLs
</li>
<li>
<code className="font-mono">$HOST</code> — <code className="font-mono">127.0.0.1</code>{" "}
for local-only daemons, <code className="font-mono">0.0.0.0</code> when the daemon binds
all interfaces
</li>
</ul>
</section>
{/* CLI */}
<section className="space-y-4">
<h2 className="text-xl font-medium">CLI</h2>
<Code>
<pre className="text-white/80">{`paseo run --worktree feature-auth --base main "implement auth"
paseo worktree ls
paseo worktree archive feature-auth`}</pre>
</Code>
</section>
</div>
);
}
+128
View File
@@ -84,6 +84,134 @@
color: rgba(250, 250, 250, 0.8);
}
/* Docs page prose styles — mirrors the original docs/*.tsx components */
.docs-prose {
color: rgba(255, 255, 255, 0.6);
line-height: 1.625;
}
.docs-prose > :first-child {
margin-top: 0;
}
.docs-prose h1 {
color: var(--color-foreground);
font-size: 1.875rem;
line-height: 1.25;
font-weight: 500;
margin-top: 0;
margin-bottom: 1rem;
}
.docs-prose h2 {
color: var(--color-foreground);
font-size: 1.25rem;
line-height: 1.4;
font-weight: 500;
margin-top: 2rem;
margin-bottom: 1rem;
}
.docs-prose h3 {
color: var(--color-foreground);
font-size: 1.125rem;
line-height: 1.4;
font-weight: 500;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
.docs-prose p {
margin-top: 1rem;
margin-bottom: 1rem;
}
.docs-prose a {
color: var(--color-foreground);
text-decoration: underline;
text-underline-offset: 2px;
}
.docs-prose a:hover {
color: rgba(255, 255, 255, 0.8);
}
.docs-prose ul,
.docs-prose ol {
margin-top: 1rem;
margin-bottom: 1rem;
padding-left: 1.5rem;
}
.docs-prose ul {
list-style: disc;
}
.docs-prose ol {
list-style: decimal;
}
.docs-prose li {
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}
.docs-prose strong {
color: rgba(255, 255, 255, 0.8);
font-weight: 600;
}
.docs-prose blockquote {
background-color: color-mix(in srgb, var(--color-primary) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--color-primary) 30%, transparent);
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1rem;
margin-bottom: 1rem;
color: rgba(255, 255, 255, 0.8);
}
.docs-prose blockquote > :first-child {
margin-top: 0;
}
.docs-prose blockquote > :last-child {
margin-bottom: 0;
}
.docs-prose pre {
background-color: var(--color-card);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1rem;
overflow-x: auto;
margin-top: 1rem;
margin-bottom: 1rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.875rem;
color: var(--color-foreground);
}
.docs-prose pre code {
background: transparent;
padding: 0;
border-radius: 0;
font-size: inherit;
}
.docs-prose :not(pre) > code {
background-color: var(--color-muted);
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.875em;
}
.docs-prose hr {
border-color: var(--color-border);
margin-top: 2rem;
margin-bottom: 2rem;
}
/* Blog post prose styles — adapted from moboudra.com */
.blog-prose {
color: rgba(250, 250, 250, 0.82);