An open API service indexing awesome lists of open source software.

https://github.com/clouvet/sprite-mobile

A PWA chat UI for accessing Claude Code running in YOLO mode on a Sprite
https://github.com/clouvet/sprite-mobile

Last synced: about 2 months ago
JSON representation

A PWA chat UI for accessing Claude Code running in YOLO mode on a Sprite

Awesome Lists containing this project

README

          

# Sprite Mobile

> **⚠️ This project is a work in progress and is subject to change at any time. Features, APIs, and behavior may be modified or removed without notice.**
>
> Features added to Sprites will likely make some of the hacks described below redundant, and hopefully a lot of this, especially setup, configuration, and orchestration, will be simplified in the near future.
>
> This is a personal project, not an official Fly.io product.

sprite-mobile gives you a progressive web app chat UI for accessing Claude Code running in YOLO mode on a [Sprite](https://sprites.dev), an ideal vibe-coding interface on your phone. It allows input by text, voice, and image, persists sessions across clients, and seamlessly networks with your other sprites through Tailscale.

## Table of Contents

- [Sprite Setup](#sprite-setup)
- [Sprite Orchestration](#sprite-orchestration)
- [Prerequisites](#prerequisites)
- [Claude Code Integration](#claude-code-integration)
- [Features](#features)
- [Distributed Tasks](#distributed-tasks)
- [Access Model](#access-model)
- [Quick Start](#quick-start)
- [Environment Variables](#environment-variables)
- [Architecture](#architecture)
- [Services](#services)
- [Data Storage](#data-storage)
- [API Endpoints](#api-endpoints)
- [WebSocket](#websocket)
- [Keepalive](#keepalive)
- [Session Lifecycle](#session-lifecycle)
- [CLI Session Attachment](#cli-session-attachment)
- [Configuration](#configuration)
- [Security](#security)
- [Troubleshooting](#troubleshooting)
- [License](#license)

## Sprite Setup

To set up a fresh Sprite with all dependencies, authentication, and services, download and run the setup script:

```bash
curl -fsSL https://raw.githubusercontent.com/clouvet/sprite-mobile/refs/heads/main/scripts/sprite-setup.sh -o sprite-setup.sh && chmod +x sprite-setup.sh && ./sprite-setup.sh
```

The script will:
1. Install Sprites CLI and authenticate
2. Configure hostname, git user, URLs, and repo (auto-detects public URL from sprite metadata)
3. Authenticate Claude CLI
4. Authenticate GitHub CLI
5. Install Fly.io CLI
6. Install and configure Tailscale
7. Set up Tailscale Serve (HTTPS for PWA support)
8. Install and configure claude-hub (WebSocket hub for multi-client sync)
9. Clone and run sprite-mobile
10. Set up Sprite Network credentials (optional - enables automatic discovery of other sprites in your org)
11. Start the Tailnet Gate (public entry point that embeds Tailscale URL via iframe)
12. Create CLAUDE.md with sprite environment instructions

The script is idempotent and can be safely re-run.

The app is installed to `~/.sprite-mobile` (hidden directory). On each service start, it attempts to auto-update via `git pull` so all sprites receive updates when they wake up.

**Note:** During authentication:
- Claude CLI may start a new Claude session after completing. Just type `exit` or press `Ctrl+C` to exit and continue.

## Sprite Orchestration

Once you have one sprite-mobile sprite set up, it can automatically create and configure new sprites with a single command. This is useful for scaling your sprite fleet or letting Claude Code create new sprites on demand.

### Prerequisites

For fully automated sprite creation, you need:

1. **~/.sprite-config file** - Created automatically during initial setup
2. **Tailscale reusable auth key** - Must be saved in your `~/.sprite-config`
3. **Authenticated CLI tools** - Claude, GitHub, Fly.io, and Sprite CLI

**One-Time Setup: Tailscale Reusable Auth Key**

Create a reusable auth key and add it to your `~/.sprite-config`:

1. Go to https://login.tailscale.com/admin/settings/keys
2. Click "Generate auth key"
3. Check "Reusable"
4. Copy the key and add it to `~/.sprite-config`:
```bash
TAILSCALE_AUTH_KEY=tskey-auth-xxxxx
```

### Creating a New Sprite (One Command)

From any existing sprite-mobile sprite:

```bash
~/.sprite-mobile/scripts/create-sprite.sh my-new-sprite
```

**That's it!** This single command will:

1. Create a new sprite with the given name
2. Make its URL public
3. Transfer your `.sprite-config` to the new sprite (excluding sprite-specific URLs)
4. Download and run the full setup script non-interactively
5. Verify all services are running

**Example output:**

```
Creating and Configuring Sprite
Target sprite: my-new-sprite

Step 1: Creating sprite...
Created sprite: my-new-sprite

Step 2: Making URL public...
Public URL: https://my-new-sprite.sprites.app

Step 3: Transferring configuration...
Transferred ~/.sprite-config (excluded sprite-specific URLs)

Step 4: Downloading setup script...
Downloaded sprite-setup.sh

Step 5: Running setup script (this may take 3-5 minutes)...
[Setup runs automatically with your credentials]

Setup Complete!
```

### What Gets Transferred

The script transfers your `~/.sprite-config` which includes:
- Git configuration (user.name, user.email)
- Claude CLI OAuth token
- GitHub CLI token
- Fly.io API token
- Sprite API token
- Tailscale reusable auth key
- Sprite Network credentials

The following are **unique per sprite** and NOT transferred:
- `SPRITE_PUBLIC_URL` - Stripped during transfer, set correctly for the new sprite
- `TAILSCALE_SERVE_URL` - Stripped during transfer, generated during setup
- Hostname - Set to the sprite name automatically

### How It Works

The `create-sprite.sh` script uses a defense-in-depth approach:

1. **Filters sprite-specific values** during config transfer:
```bash
# Strip SPRITE_PUBLIC_URL and TAILSCALE_SERVE_URL
grep -v '^SPRITE_PUBLIC_URL=' ~/.sprite-config | \
grep -v '^TAILSCALE_SERVE_URL=' > filtered-config
```

2. **Passes correct values** to setup script:
```bash
sprite exec -- ./sprite-setup.sh --name 'my-new-sprite' --url 'https://my-new-sprite.sprites.app' all
```

This ensures the new sprite always gets the correct public URL and hostname, even if the source config contained different values.

### Manual Alternative

If you prefer manual control or need to customize the process:

```bash
# 1. Create sprite
sprite create my-new-sprite

# 2. Make URL public and get the URL
sprite url update --auth public -s my-new-sprite
PUBLIC_URL=$(sprite api /v1/sprites/my-new-sprite | grep -o '"url"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: *"\([^"]*\)".*/\1/')

# 3. Transfer config (excluding sprite-specific URLs)
grep -v '^SPRITE_PUBLIC_URL=' ~/.sprite-config | grep -v '^TAILSCALE_SERVE_URL=' | \
sprite -s my-new-sprite exec -- cat > ~/.sprite-config

# 4. Download and run setup
sprite -s my-new-sprite exec -- bash -c "
curl -fsSL https://gist.githubusercontent.com/clouvet/901dabc09e62648fa394af65ad004d04/raw/sprite-setup.sh -o ~/sprite-setup.sh
chmod +x ~/sprite-setup.sh
~/sprite-setup.sh --name my-new-sprite --url '$PUBLIC_URL' all
"
```

### Letting Claude Create Sprites

With orchestration configured, you can simply tell Claude Code:

> "Create a new sprite-mobile sprite called test-sprite"

Claude will use `create-sprite.sh` to handle the entire process automatically.

## Prerequisites

This app is designed to run on a Sprite from [sprites.dev](https://sprites.dev). Sprites come with:

- **Bun** runtime pre-installed
- **Claude Code** CLI pre-installed and authenticated

If running elsewhere, you'll need to install these manually and authenticate Claude Code with `claude login`.

## Claude Code Integration

sprite-mobile includes a comprehensive Claude skill that provides context about the architecture, service management, development workflows, and sprite orchestration. When working with Claude Code on a sprite-mobile sprite, Claude automatically has access to this skill.

**The skill covers:**
- Architecture (tailnet-gate + sprite-mobile integration)
- Service management (restart procedures, logs, status)
- Development workflows (service worker cache versioning)
- Creating and managing other sprite-mobile sprites
- Configuration management and replication
- API endpoints and WebSocket protocol
- Common troubleshooting tasks

**Location:** `.claude/skills/sprite-mobile.md`

This means you can ask Claude questions like:
- "How do I restart the sprite-mobile service?"
- "Create a new sprite-mobile sprite called test-sprite"
- "What's the service worker cache version and when should I bump it?"
- "How does the tailnet-gate work?"

Claude will have full context about sprite-mobile without needing to read through documentation or search for files.

## Features

- **Multiple Chat Sessions**: Create and manage multiple independent chat sessions, each with its own Claude Code process
- **Persistent History**: Messages are saved to disk and survive server restarts
- **Session Resume**: Reconnecting to a session resumes the existing Claude conversation
- **CLI Session Attachment**: Attach to existing Claude CLI sessions started in the terminal and import their history
- **Image Support**: Upload and send images to Claude for analysis (auto-resized for API limits)
- **Real-time Streaming**: Responses stream in real-time via WebSocket
- **Activity Indicators**: See exactly what Claude is doing (reading files, running commands, searching)
- **Multi-client Support**: Multiple browser tabs can connect to the same session
- **Auto-naming**: Chat sessions are automatically named based on conversation content
- **Smart Auto-focus**: Input field auto-focuses on desktop after Claude responds (disabled on mobile to avoid keyboard popup)
- **Voice Input**: Tap the microphone button to dictate messages (uses Web Speech API, works on iOS Safari and Android Chrome)
- **Dynamic Branding**: Header displays the sprite's hostname with a neon green πŸ‘Ύ
- **Tailscale Integration**: HTTPS access via Tailscale Serve, embedded in iframe from public URL
- **Tailnet Gate**: Public URL wakes sprite and embeds Tailscale URL in iframe (if authorized)
- **Deep Linking**: URL hash syncs bidirectionally between parent and iframe for shareable session links
- **PWA Support**: Installable as a Progressive Web App, works offline (requires HTTPS via Tailscale Serve)
- **Auto-update**: Pulls latest code when the service starts
- **Sprite Network**: Automatic discovery of other sprites in your Fly.io organization via shared Tigris bucket
- **Distributed Tasks**: Assign work across sprites, track progress, and automatically process task queues
- **Network Restart**: Run `scripts/restart-others.sh` to restart sprite-mobile on all other network sprites after pulling updates

## Distributed Tasks

> **⚠️ Experimental Feature**: Distributed tasks is a new feature that has not been thoroughly tested. Use with caution and expect potential issues or behavior changes.

sprite-mobile includes a distributed task management system that allows sprites in your network to assign work to each other, track progress, and automatically process queued tasks. This enables collaborative workflows across your sprite fleet.

### Overview

The distributed tasks feature enables:
- **Task assignment from chat**: Ask Claude to assign tasks to specific sprites
- **Round-robin distribution**: Distribute multiple tasks across available sprites automatically
- **Automatic sprite wake-up**: Target sprites are automatically awakened using `sprite exec`
- **Sequential processing**: Each sprite processes tasks one at a time from its queue
- **Auto-sessions**: Tasks automatically create Claude Code sessions with the task description
- **Progress tracking**: Monitor what each sprite is working on across your network

### Usage

#### Assigning Tasks from Chat

Simply tell Claude to assign work to another sprite:

```
"Assign carnivorous-slobbius to implement feature X"
"Assign eternalii-famishus to fix the bug in module Y"
```

Claude will create the task and automatically wake the target sprite. The target sprite will:
1. Receive the task in its queue
2. Create a new Claude Code session with the task description
3. Work on the task autonomously
4. For git repository work:
- Create a feature branch (feat/* or fix/*)
- Complete the work and commit changes
- Push the branch to remote
- Create a pull request using gh CLI
- Include the PR URL in the completion summary
5. Report completion back to Tigris
6. Automatically pick up the next queued task

#### Distributing Multiple Tasks

For bulk work, use round-robin distribution:

```
"Distribute these 4 tasks across available sprites"
```

This automatically spreads the workload evenly across sprites in your network.

#### Monitoring Task Status

Check on your sprite network's progress:

```
"What are other sprites working on?"
```

Claude will query the distributed task status and show:
- Current tasks in progress
- Queued tasks per sprite
- Recent completions

#### Using the Tasks UI

The web interface includes a Tasks button (πŸ“‹) in the header that opens a modal showing:
- **My Tasks**: Your current task and queue
- **All Sprites Status**: What each sprite in the network is working on
- **Task History**: Complete task history with status-based color coding

#### Git Workflow for Repository Tasks

When a distributed task involves work on a git repository, Claude automatically follows this workflow:

1. **Create Feature Branch**: Before starting work, create a descriptive branch
- Format: `feat/brief-description` for new features
- Format: `fix/brief-description` for bug fixes
- Example: `git checkout -b feat/add-user-authentication`

2. **Complete Work**: Implement the changes as described in the task

3. **Commit Changes**: Stage and commit with a clear message
- Example: `git add . && git commit -m "Add user authentication feature"`

4. **Push Branch**: Push to remote repository
- Example: `git push -u origin feat/add-user-authentication`

5. **Create Pull Request**: Use gh CLI to create a PR
- Example: `gh pr create --title "Add user authentication" --body "Implements feature X"`

6. **Report Completion**: Include the PR URL in the task completion summary
- Example: "Completed task. Created PR: https://github.com/org/repo/pull/123"

**Note**: If a task doesn't involve a git repository or the repository isn't configured for remote pushes, the git workflow is skipped and the task completes normally.

### API Endpoints

The distributed tasks system provides these API endpoints:

| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/distributed-tasks` | Create a new task |
| POST | `/api/distributed-tasks/distribute` | Distribute tasks round-robin |
| POST | `/api/distributed-tasks/check` | Check for new tasks in queue |
| POST | `/api/distributed-tasks/complete` | Mark a task complete |
| POST | `/api/distributed-tasks/:id/cancel` | Cancel a task (aborts if in progress, removes from queue if pending) |
| POST | `/api/distributed-tasks/:id/reassign` | Reassign a task to a different sprite (updates queue accordingly) |
| GET | `/api/distributed-tasks` | List all tasks |
| GET | `/api/distributed-tasks/mine` | Get tasks for this sprite |
| GET | `/api/distributed-tasks/status` | Get status of all sprites |
| GET | `/api/distributed-tasks/:id` | Get a specific task by ID |
| PATCH | `/api/distributed-tasks/:id` | Update a task's status/fields |

**Cancellation:**
```bash
# Cancel a task by ID
curl -X POST http://localhost:8081/api/distributed-tasks/{task-id}/cancel \
-H "Content-Type: application/json" \
-d '{"reason": "Optional cancellation reason"}'
```

**Reassignment:**
```bash
# Reassign a task to a different sprite
curl -X POST http://localhost:8081/api/distributed-tasks/{task-id}/reassign \
-H "Content-Type: application/json" \
-d '{"newAssignedTo": "target-sprite-name"}'
```

### Data Model

Tasks are stored in your shared Tigris bucket with the following structure:

**Task Object** (`tasks/{task-id}.json`):
```json
{
"id": "uuid",
"assignedTo": "sprite-name",
"assignedBy": "sprite-name",
"status": "pending|in_progress|completed|failed|cancelled",
"title": "Task title",
"description": "Full task description",
"createdAt": "ISO date",
"startedAt": "ISO date",
"completedAt": "ISO date",
"cancelledAt": "ISO date",
"sessionId": "session-id",
"result": {
"summary": "What was accomplished",
"success": true
},
"cancellationReason": "Optional reason for cancellation",
"reassignmentHistory": [
{
"from": "old-sprite",
"to": "new-sprite",
"at": "ISO date",
"reason": "Optional reason"
}
]
}
```

**Task Queue** (`task-queues/{sprite-name}.json`):
```json
{
"spriteName": "sprite-name",
"queuedTasks": ["task-id-1", "task-id-2"],
"currentTask": "task-id-3",
"lastUpdated": "ISO date"
}
```

### Example Workflow

Here's a complete example of distributed tasks in action:

1. **User on sprite `eternalii-famishus`**: "Assign carnivorous-slobbius to implement features A, B, and C"
2. **eternalii-famishus's Claude**: Creates 3 tasks in Tigris and wakes carnivorous-slobbius with `sprite exec`
3. **carnivorous-slobbius**: Receives tasks, creates a new Claude Code session for task A
4. **carnivorous-slobbius**: Completes task A, reports back to Tigris, automatically starts task B
5. **User on sprite `canis-latrans`**: "What are other sprites working on?"
6. **canis-latrans's Claude**: Queries task status and responds: "carnivorous-slobbius is working on 'Implement feature B' with 1 task queued"

### Prerequisites

Distributed tasks requires:
- **Sprite Network configuration**: Shared Tigris bucket credentials
- **sprite-exec access**: Ability to execute commands on target sprites
- **Multiple sprites**: At least 2 sprites in your network for cross-sprite assignment

The sprite network is automatically configured during initial setup if you provide Tigris credentials.

## Access Model

Sprite Mobile uses Tailscale for secure access without passwords or tokens:

```
Public URL (https://sprite.sprites.app)
β”‚
β–Ό
Tailnet Gate (port 8080)
β”‚
β”œβ”€β”€ Embed iframe with Tailscale HTTPS URL
β”‚ β”‚
β”‚ β”œβ”€β”€ Iframe loads? ──→ Show sprite-mobile interface
β”‚ β”‚ (WebSocket keeps sprite awake)
β”‚ β”‚
β”‚ └── Iframe fails (4s timeout)? ──→ Show "Unauthorized" πŸ‘Ύ 🚫
β”‚
└── Hash syncing ──→ Deep linking to specific sessions
```

**Three access paths:**

| Path | URL | Auth | HTTPS | PWA |
|------|-----|------|-------|-----|
| Public | `https://sprite.sprites.app` | Tailnet Gate | Yes | Via iframe |
| Tailscale Serve | `https://my-sprite.ts.net` | Tailnet only | Yes | Yes |
| Tailscale IP | `http://100.x.x.x:8081` | Tailnet only | No | No |

**Recommended**: Bookmark the public URL. It wakes the sprite and embeds the Tailscale HTTPS URL in an iframe (with hash syncing for deep linking). A WebSocket keepalive keeps the sprite awake while the page is open.

## Quick Start

If you prefer to set things up manually:

```bash
git clone sprite-mobile
cd sprite-mobile
bun install
bun start
```

The server runs on port 8081 by default. Override with the `PORT` environment variable.

Open `http://localhost:8081` in a browser to access the chat interface.

## Environment Variables

### Configuration File

All environment variables are managed through `~/.sprite-config`, which serves as the single source of truth. Both bash and zsh automatically source this file.

**Format:**
```bash
# ~/.sprite-config
GH_TOKEN=ghp_xxxxx
CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-xxxxx
FLY_API_TOKEN=fm2_xxxxx
SPRITE_API_TOKEN=your-org-name/org/id/token
SPRITE_PUBLIC_URL=https://my-sprite.sprites.app
TAILSCALE_SERVE_URL=https://my-sprite.tailxxxxx.ts.net
SPRITE_MOBILE_REPO=https://github.com/org/sprite-mobile
```

### Key Variables

| Variable | Description | Example |
|----------|-------------|---------|
| `PORT` | Server port | `8081` |
| `USE_GO_HUB` | Enable claude-hub for multi-client sync (default: `true`) | `true` |
| `GO_HUB_URL` | WebSocket URL for claude-hub | `ws://localhost:9090` |
| `SPRITE_PUBLIC_URL` | Public URL for waking sprite | `https://my-sprite.sprites.app` |
| `TAILSCALE_SERVE_URL` | Tailscale HTTPS URL | `https://my-sprite.ts.net` |
| `SPRITE_HOSTNAME` | Hostname for sprite network registration | `my-sprite` |
| `SPRITE_NETWORK_CREDS` | Path to Tigris credentials file | `~/.sprite-network/credentials.json` |
| `SPRITE_NETWORK_ORG` | Fly.io org for sprite network | `my-org` |
| `GH_TOKEN` | GitHub Personal Access Token | `ghp_xxxxx` |
| `CLAUDE_CODE_OAUTH_TOKEN` | Claude Code OAuth token | `sk-ant-oat01-xxxxx` |
| `FLY_API_TOKEN` | Fly.io API token | `fm2_xxxxx` |
| `SPRITE_API_TOKEN` | Sprite CLI API token | `your-org-name/org/id/token` |

These are automatically configured by the setup script and stored in `~/.sprite-config`.

## Architecture

### Overview

sprite-mobile uses a multi-service architecture for reliable multi-client Claude Code session management:

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Public Internet β”‚
β”‚ https://my-sprite.sprites.app β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ tailnet-gate β”‚ Port 8080
β”‚ (Public entry) β”‚ β€’ Wakes sprite
β”‚ β”‚ β€’ Embeds Tailscale URL in iframe
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β€’ WebSocket keepalive
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Tailscale HTTPS β”‚
β”‚ my-sprite.ts.net β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ sprite-mobile β”‚ Port 8081
β”‚ (Web UI + API) β”‚ β€’ Serves PWA interface
β”‚ β”‚ β€’ Proxies WebSocket to claude-hub
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β€’ Maintains UI metadata
β”‚
β”‚ WebSocket Proxy
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ claude-hub β”‚ Port 9090
β”‚ (Session Mgr) β”‚ β€’ Spawns/manages Claude processes
β”‚ β”‚ β€’ Multi-client sync
β”‚ β”‚ β€’ State machine (IDLE/WEB/TERMINAL)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β€’ Terminal session detection
β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ β”‚ β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”
β”‚ Claude Process β”‚ β”‚ File β”‚ β”‚ Terminal β”‚
β”‚ (headless) β”‚ β”‚Watcher β”‚ β”‚ Session β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ~/.claude/ β”‚
β”‚ projects/ β”‚
β”‚ {cwd}/{uuid} β”‚
β”‚ .jsonl β”‚
β”‚ β”‚
β”‚ (Source of Truth) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

**Key architectural principles:**
- **Claude's `.jsonl` files are the source of truth** - All message history lives here
- **sprite-mobile is a proxy** - Forwards WebSocket traffic to claude-hub
- **claude-hub manages process lifecycle** - State machine handles web/terminal transitions
- **Multi-client sync** - Multiple browsers and terminal sessions share the same Claude session
- **No time-based cleanup** - Sessions persist until explicitly terminated

### Services

After setup, these services run on your sprite:

| Service | Port | Description |
|---------|------|-------------|
| `tailnet-gate` | 8080 | Public entry point, embeds Tailscale URL in iframe with WebSocket keepalive |
| `sprite-mobile` | 8081 | Main app server, proxies WebSocket connections to claude-hub |
| `claude-hub` | 9090 | WebSocket hub for multi-client Claude Code session synchronization |
| `tailscaled` | - | Tailscale daemon |

### Data Storage

**Session data (source of truth):**
- `~/.claude/projects/{cwdDir}/{sessionId}.jsonl` - All message history in Claude's native format
- This is the authoritative source for all conversation data

**sprite-mobile lightweight metadata** (`data/` directory):
- `sessions.json` - UI metadata only (session names, timestamps, preview text)
- `sprites.json` - Saved Sprite profiles for network discovery
- `uploads/{sessionId}/` - Uploaded images per session

**Architecture:**
When `USE_GO_HUB=true` (the default), sprite-mobile acts as a WebSocket proxy to claude-hub. Messages flow:
```
Web Client β†’ sprite-mobile (proxy) β†’ claude-hub β†’ Claude process β†’ ~/.claude/projects/*.jsonl
```

Claude's `.jsonl` files are the source of truth. Sprite-mobile only maintains lightweight metadata for the UI (session list, previews, timestamps).

### API Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/config` | Get public configuration |
| GET | `/api/sessions` | List all sessions |
| POST | `/api/sessions` | Create new session |
| PATCH | `/api/sessions/:id` | Update session name/cwd |
| DELETE | `/api/sessions/:id` | Delete session |
| GET | `/api/sessions/:id/messages` | Get message history |
| POST | `/api/sessions/:id/regenerate-title` | Regenerate session title |
| GET | `/api/claude-sessions` | Discover Claude CLI sessions from `~/.claude/projects/` |
| POST | `/api/upload?session={id}` | Upload an image |
| GET | `/api/uploads/:sessionId/:filename` | Retrieve uploaded image |
| GET | `/api/sprites` | List saved Sprite profiles |
| POST | `/api/sprites` | Add a Sprite profile |
| PATCH | `/api/sprites/:id` | Update a Sprite profile |
| DELETE | `/api/sprites/:id` | Remove a Sprite profile |
| GET | `/api/network/status` | Check if sprite network is configured |
| GET | `/api/network/sprites` | Discover sprites in the network |
| POST | `/api/network/heartbeat` | Manual heartbeat trigger |
| DELETE | `/api/network/sprites/:hostname` | Remove a sprite from the network |
| POST | `/api/distributed-tasks` | Create a new task |
| POST | `/api/distributed-tasks/distribute` | Distribute tasks round-robin |
| POST | `/api/distributed-tasks/check` | Check for new tasks |
| POST | `/api/distributed-tasks/complete` | Mark task complete |
| POST | `/api/distributed-tasks/:id/cancel` | Cancel a task by ID |
| POST | `/api/distributed-tasks/:id/reassign` | Reassign a task to a different sprite |
| GET | `/api/distributed-tasks` | List all tasks |
| GET | `/api/distributed-tasks/mine` | Get this sprite's tasks |
| GET | `/api/distributed-tasks/status` | Get all sprites status |
| GET | `/api/distributed-tasks/:id` | Get a specific task by ID |
| PATCH | `/api/distributed-tasks/:id` | Update a task's status/fields |

### WebSocket

Connect to `/ws?session={sessionId}` to interact with a chat session.

**With claude-hub (default):**
- sprite-mobile acts as a transparent WebSocket proxy
- Messages flow: `Web Client ↔ sprite-mobile (proxy) ↔ claude-hub ↔ Claude process`
- Multiple clients can connect to the same session (synced in real-time)
- Terminal sessions and web clients share the same session seamlessly

**Incoming messages (from claude-hub via proxy):**
- `{ type: "system", subtype: "init", session_id: "..." }` - Session initialized with Claude's UUID
- `{ type: "history", messages: [...] }` - Message history on connect
- `{ type: "assistant", message: {...} }` - Streaming assistant response
- `{ type: "result", ... }` - Response complete
- `{ type: "user_message", message: {...} }` - User message from another client
- `{ type: "processing", isProcessing: true/false }` - Processing state
- `{ type: "system", message: "..." }` - System notifications (e.g., "Switched to terminal mode")

**Outgoing messages (to server):**
```json
{
"type": "user",
"content": "Your message here",
"imageId": "optional-image-id",
"imageFilename": "optional-filename",
"imageMediaType": "image/png"
}
```

**Session ID synchronization:**
- Web clients start with a temporary UUID
- claude-hub spawns Claude, which generates its own session UUID
- `init` message updates the frontend to use Claude's UUID
- URL hash, session metadata, and `.jsonl` files all sync to Claude's UUID

### Keepalive

Two WebSocket endpoints keep the Sprite awake:

1. **Public Gate Keepalive** (`/keepalive` on port 8080): The tailnet-gate opens a WebSocket connection to the sprite's http_port (8080) to keep it awake while the public URL is open. This ensures the sprite doesn't suspend before the Tailscale connection is established.

2. **App Keepalive** (`/ws/keepalive` on port 8081): The sprite-mobile app itself opens a WebSocket to keep the sprite awake while the app is in use.

Both use persistent WebSocket connections because sprites stay awake as long as there's an active connection to their http_port or any running service.

## Session Lifecycle

**With claude-hub (default `USE_GO_HUB=true`):**

1. **Creation**: Browser connects to `ws://localhost:8081/ws?session={id}`
2. **Proxy**: sprite-mobile proxies connection to `ws://localhost:9090/ws?session={id}`
3. **Session State Machine**: claude-hub manages session state
- `IDLE` β†’ `WEB_ONLY` (first web client connects, spawns headless Claude process)
- `WEB_ONLY` ⇄ `TERMINAL_ONLY` (terminal session detected/exits)
- `WEB_ONLY`/`TERMINAL_ONLY` β†’ `IDLE` (all clients disconnect)
4. **Process Lifecycle**: Claude processes persist even after all clients disconnect
- No time-based cleanup or 30-minute timeouts
- Process stays alive until explicitly interrupted or sprite restarts
- Session files in `~/.claude/projects/` preserve full history
5. **Reconnection**: Resuming a session rejoins the existing process with full history

**⚠️ Important: Sprite Sleep Behavior**

If you close your browser while Claude is working:
- WebSocket disconnects β†’ claude-hub keeps Claude process alive
- **BUT** sprite goes to sleep (no HTTP connections to port 8081)
- All processes freeze until sprite wakes up

**Workarounds for long-running tasks:**
- Keep browser tab open (even in background)
- Open terminal session: `sprite exec -s ` keeps sprite awake
- Use distributed tasks to assign work to another sprite

## CLI Session Attachment

Sprite Mobile can attach to existing Claude CLI sessions that were started in the terminal. This allows you to:

1. Continue conversations that you started on the command line
2. Import the full conversation history into the web interface
3. Switch between CLI and web interface seamlessly

**How it works:**

- Click the terminal icon in the sidebar (next to "New Chat")
- The app discovers sessions from `~/.claude/projects/` directory
- Select a session to import its history and resume the conversation
- The session continues with the same Claude session ID, maintaining full context

**Session discovery format:**

Claude CLI stores sessions in `~/.claude/projects/{cwd}/{sessionId}.jsonl`. The app:
- Scans all working directories under the projects folder
- Parses `.jsonl` files to extract message history
- Shows the first user message as a preview
- Filters out empty sessions and internal tool messages

**Important caveat (with claude-hub):**

claude-hub manages transitions between web and terminal sessions using a state machine:

- **Web-only mode**: claude-hub runs a headless Claude process
- **Terminal detected**: claude-hub kills the headless process and monitors the terminal session
- **Terminal exits**: claude-hub spawns headless process again

**Potential race condition:**
- During the transition window (terminal detection β†’ killing headless), both processes could theoretically write to the same `.jsonl` file
- claude-hub detects terminal sessions via file watching (fsnotify), not continuous polling
- **Best practice**: Avoid actively using both terminal and web interface simultaneously on the same session to prevent potential file corruption during state transitions

## Configuration

Sessions can specify a working directory (`cwd`) that Claude Code operates in. This defaults to the user's home directory.

## Security

### Intended Use: Personal Tool

**sprite-mobile is designed as a personal tool for individual use, not for shared or public deployment.** Each person should run their own instance(s) on their own Sprite(s). This significantly simplifies the security model:

- No multi-user authentication needed
- No per-user permissions or isolation
- Tailscale network membership IS the authentication

### Important Security Considerations

⚠️ **Beware. If you wouldn't let someone into your Tailnet then you probably shouldn't let them anywhere near this app. Do not expose this app to the public internet or share your tailnet with untrusted users.** Anyone with access to the app has full control over your Claude Code sessions and can execute arbitrary commands on your Sprite. They'll also have whatever access you're scoped to for Fly, Sprites, and the GitHub cli. 😡

### Access Control

Access is controlled via Tailscale:
- **Tailnet membership is the auth** - No passwords or tokens needed
- **Public URL embeds via iframe** - The tailnet gate embeds the Tailscale URL in an iframe; if it fails to load within 4 seconds, shows "Unauthorized"
- **Not on tailnet = Unauthorized** - Users outside your tailnet see a blocked page with πŸ‘Ύ 🚫
- **Trust model**: Anyone on your tailnet can use the app. Only add trusted devices/users to your tailnet.

### Claude Code Permissions

This app runs Claude Code with `--dangerously-skip-permissions`, which allows Claude to execute commands without confirmation prompts. This is appropriate for:
- Personal use where you trust your own prompts
- A Sprite environment where the sandbox provides isolation
- "YOLO mode" vibe-coding workflows

Be aware that Claude has full access to the Sprite's filesystem and can run arbitrary commands. This is the intended behavior for a personal coding assistant.

## Troubleshooting

### Chrome Certificate Error

If Chrome shows `ERR_CERTIFICATE_TRANSPARENCY_REQUIRED` when accessing the Tailscale URL:
- Wait a few minutes for certificate propagation
- Try hard refresh (Cmd+Shift+R)
- Clear site data in DevTools
- Try incognito mode
- Safari is more lenient and may work immediately

### Tailscale Serve Not Working

Check the serve status:
```bash
tailscale serve status
```

Restart if needed:
```bash
tailscale serve --bg 8081
```

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.