Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/stable-session-reconnect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"crosscode": patch
---

Stabilize the CLI session identity and tunnel URLs so a returning mobile client (e.g. after the PC sleeps or the network drops) can always reconnect instead of being stranded by a freshly generated QR/URL.

- Persist `sessionToken` (opencode password + QR token) and `projectId` in `~/.crosscode/config.json` so they stay constant across CLI restarts. This makes the managed tunnel URL (`connect.crosscode.site/t/<projectId>`) and the QR token stable.
- For free, unauthenticated users, use a persistent **named cloudflared tunnel** (`crosscode-<projectId>`) which keeps a fixed `*.cfargotunnel.com` hostname across restarts and reconnects. Falls back to the ephemeral quick tunnel (URL still changes on restart) when `cloudflared tunnel login` hasn't been run.
- Print the QR code exactly once per identity; tunnel reconnects no longer regenerate it.
- Auto-restart a dropped named cloudflared tunnel without changing its URL.
237 changes: 220 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,249 @@
# CrossCode

Control your PC's [OpenCode](https://opencode.ai) the terminal AI coding agentfrom your phone, from anywhere in the world. Free, open source, and private: your code never touches CrossCode's servers, because there are none.
Control your PC's [OpenCode](https://opencode.ai) (the terminal AI coding agent) from your phone, from anywhere in the world. Free, open source, and private. Your code never touches CrossCode's servers, because there are none.

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![npm](https://img.shields.io/npm/v/crosscode)](https://www.npmjs.com/package/crosscode)
[![Node.js >=20](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)

## Quick Start
## What is CrossCode?

### 1. On your PC
CrossCode streams a live OpenCode session from your computer to your phone over a secure tunnel. You get real-time chat, streaming responses, inline diffs, tool-call approvals, a Git panel, voice input, push notifications, and more. Your code is never uploaded to a cloud.

Open a terminal in your project directory and run:
There are two ways to connect:

| Mode | Tunnel | Account needed? | URL |
|---|---|---|---|
| Free | Cloudflare or ngrok | No | Random, changes each session |
| Dedicated | CrossCode Tunnel | Yes (free or paid login) | Stable `*.connect.crosscode.site` |

We recommend logging in. It gives you a stable, dedicated URL so you don't have to re-scan a new QR code every time you restart.

## Prerequisites

- A computer (Windows, macOS, or Linux) with [OpenCode](https://opencode.ai) installed
- Node.js 20 or newer (only needed for the CLI wrapper; OpenCode itself is a standalone binary)
- A phone with the CrossCode app ([Android](https://crosscode.site/download); iOS coming soon)
- For free tunnels: `cloudflared` (the CLI will guide you if it's missing) or `ngrok`

---

## Install OpenCode (by operating system)

CrossCode needs a running `opencode serve` on your machine. Install OpenCode first.

### macOS

```bash
# Recommended (always up to date)
brew install anomalyco/tap/opencode

# Or the official install script
curl -fsSL https://opencode.ai/install | bash
```

### Linux

```bash
# Debian/Ubuntu or any distro with Homebrew
brew install anomalyco/tap/opencode

# Arch (stable)
sudo pacman -S opencode

# Arch (latest from AUR)
paru -S opencode-bin

# Or the official install script (any distro)
curl -fsSL https://opencode.ai/install | bash
```

### Windows

Windows has two good options.

**Option A - WSL2 (recommended for the best experience)**

```powershell
# 1. Install WSL2, then open your WSL terminal and run:
curl -fsSL https://opencode.ai/install | bash
```

**Option B - Native Windows**

```powershell
# Chocolatey
choco install opencode

# or Scoop
scoop install opencode

# or npm
npm install -g opencode-ai
```

If you use WSL, run CrossCode inside WSL too (see the Windows workflow below). Windows and WSL have separate PATHs and home directories.

Verify the install on any OS:

```bash
opencode --version
```

---

## Install the CrossCode CLI (by operating system)

The CLI (`crosscode`) does all the setup for you. It starts `opencode serve`, opens a tunnel, and prints a QR code.

### macOS or Linux

```bash
# Run instantly (no install needed)
npx crosscode

# Or install globally
npm install -g crosscode
```

### Windows (native)

```powershell
# Run instantly
npx crosscode

# Or install globally
npm install -g crosscode
```

### Windows (WSL2)

Run the same commands inside your WSL terminal. Node.js must be installed in WSL:

```bash
# Install Node.js in WSL if you don't have it
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs

# Then run CrossCode
npx crosscode
```

Tip: `npx crosscode` always uses the latest version. If you installed it globally, run `npm update -g crosscode` to upgrade.

---

## Step-by-Step: From Zero to Connected

Follow these in order. OS-specific install commands are in the sections above.

### 1. Install OpenCode on your computer
Pick your OS in the Install OpenCode section and run the command. Verify with `opencode --version`.

### 2. Log in for a dedicated tunnel (recommended)
Logging in gives you a stable, dedicated URL at `*.connect.crosscode.site`. You can reconnect from your phone without re-scanning a fresh QR code every session.

```bash
npx crosscode login
```

1. Your browser opens `https://crosscode.site/login`
2. Log in with your email (one-time code) on the dashboard and copy your API key
3. Paste the API key back into the terminal

Your credentials are saved to `~/.crosscode/config.json`. Check status anytime with `npx crosscode status`, or clear it with `npx crosscode logout`.

Skipping login? That's fine. You'll use the free Cloudflare or ngrok tunnel instead. Make sure `cloudflared` or `ngrok` is installed (the CLI will tell you if it's missing and how to install it). Your tunnel URL will change each time you start.

### 3. Start CrossCode in your project directory
Open a terminal in the folder you want to work in and run:

```bash
npx crosscode
```

This starts `opencode serve`, opens a tunnel, and prints a QR code in your terminal. (Make sure [OpenCode](https://opencode.ai) is installed first.)
This will:
- Start `opencode serve` in the current directory
- Open your tunnel (dedicated if logged in, otherwise Cloudflare or ngrok)
- Print a QR code in your terminal

Keep this terminal open while you use your phone.

### 4. Install the mobile app and connect
1. Install CrossCode on your phone ([Android](https://crosscode.site/download); iOS coming soon)
2. Open the app and scan the QR code shown in your terminal
3. You're connected. Start chatting with your agent

### 5. Stay connected across restarts (optional)
Because the dedicated tunnel gives you a stable URL, you can reopen the app later and reconnect to the same session without scanning again. Just make sure `npx crosscode` is running on your PC.

That's the whole setup. Your code never leaves your machines. CrossCode only relays the live session traffic.

---

## Tunnel Options

| Condition | Tunnel Used |
|---|---|
| Logged in (any tier) | CrossCode Tunnel (`*.connect.crosscode.site`) |
| `--ngrok` flag | ngrok |
| `--cloudflared` flag | cloudflared |
| Not logged in, no flag | cloudflared (fallback) |
| CrossCode tunnel fails | cloudflared (automatic fallback) |

```bash
npx crosscode # auto-selected tunnel (dedicated if logged in)
npx crosscode --cloudflared # force Cloudflare tunnel
npx crosscode --ngrok # force ngrok tunnel
```

If you use `--ngrok`, set your auth token first: `ngrok config add-authtoken <TOKEN>`.

### Installing cloudflared (free tier)

The free tunnel needs `cloudflared`. If it's missing, the CLI will detect it and guide you. You can also install it ahead of time:

```bash
# macOS or Linux (Homebrew)
brew install cloudflared

# Debian/Ubuntu
curl -fsSL https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o /tmp/cloudflared.deb
sudo dpkg -i /tmp/cloudflared.deb

# Windows (native)
winget install cloudflare.cloudflared
# or: choco install cloudflared
```

---

## Interactive Controls

### 2. On your phone
While the tunnel is running:

1. Install the CrossCode app — [Android](https://crosscode.site/download) (iOS coming soon)
2. Open the app and scan the QR code
3. Start chatting with your agent
| Key | Action |
|---|---|
| `l` | Toggle log viewer (last 20 lines from CrossCode, tunnel, and OpenCode) |
| `Ctrl+C` | Graceful shutdown that kills all child processes and exits |

That's it. No account, no configuration, no shared network.
---

## How It Works

Your phone connects to the OpenCode instance running on your machine through a secure tunnel, and streams everything back in real timeprompts, streaming responses, tool-call approvals, and file diffs.
Your phone connects to the OpenCode instance running on your machine through a secure tunnel. It streams everything back in real time: prompts, streaming responses, tool-call approvals, and file diffs. Your source code stays on your computer. The relay only forwards the encrypted session stream.

## Documentation

- [CLI Reference](docs/cli.md) all commands, flags, and configuration
- [Development](docs/development.md) set up and build the project locally
- [Features](docs/features.md) what CrossCode can do
- [Tunnel Server](docs/tunnel-server/README.md) self-hosted relay for paid users
- [CLI Reference](docs/cli.md) - all commands, flags, tunnel options, config, and environment variables
- [Development](docs/development.md) - set up and build the project locally
- [Features](docs/features.md) - what CrossCode can do
- [Tunnel Server](docs/tunnel-server/README.md) - self-hosted relay for paid users

## Contributing

Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md).
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md).

## License

MIT — see [LICENSE](LICENSE).
MIT. See [LICENSE](LICENSE).
6 changes: 6 additions & 0 deletions apps/mobile/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# @crosscode/mobile

## Unreleased

### Added
- Android home screen widgets (Small / Medium / Large) showing connection status, active session, per-project usage, and a 7-day output-token bar chart. Powered by `react-native-android-widget`.
- Daily usage history tracking (`dailyHistory`) in `opencode-stats.store` to support widget charts.

## 1.0.0
37 changes: 37 additions & 0 deletions apps/mobile/app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,43 @@ module.exports = ({ config }) => {
"expo-image",
"expo-router",
"expo-status-bar",
[
"react-native-android-widget",
{
widgets: [
{
name: "SmallWidget",
label: "CrossCode Status",
description: "Connection status and active session",
minWidth: "110dp",
minHeight: "40dp",
targetCellWidth: 2,
targetCellHeight: 1,
resizeMode: "horizontal",
},
{
name: "MediumWidget",
label: "CrossCode Session",
description: "Session status, today's usage and 7-day chart",
minWidth: "250dp",
minHeight: "110dp",
targetCellWidth: 4,
targetCellHeight: 2,
resizeMode: "horizontal|vertical",
},
{
name: "LargeWidget",
label: "CrossCode Projects",
description: "Per-project usage with mini charts",
minWidth: "250dp",
minHeight: "180dp",
targetCellWidth: 4,
targetCellHeight: 3,
resizeMode: "horizontal|vertical",
},
],
},
],
[
"expo-build-properties",
{
Expand Down
12 changes: 7 additions & 5 deletions apps/mobile/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -382,11 +382,13 @@ export default function HomeScreen() {
<View className="flex-1 bg-background" style={{ paddingTop: insets.top }}>
<View className="px-6 pt-4 pb-3 gap-4">
<View className="flex-row items-center justify-between">
<Image
source={theme === "dark" ? require("@/assets/branding-dark-mode.png") : require("@/assets/branding-light-mode.png")}
className="h-6 w-6"
resizeMode="contain"
/>
<Pressable onPress={() => router.push("/about")} hitSlop={8} className="active:opacity-60">
<Image
source={theme === "dark" ? require("@/assets/branding-dark-mode.png") : require("@/assets/branding-light-mode.png")}
className="h-6 w-6"
resizeMode="contain"
/>
</Pressable>
<View className="flex-row items-center gap-1">
<Pressable onPress={() => router.push("/notifications")} className="p-2">
<Bell size={22} color={THEME[theme].mutedForeground} />
Expand Down
29 changes: 29 additions & 0 deletions apps/mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@ import * as SplashScreen from "expo-splash-screen"

import { NAV_THEME } from "@/lib/theme"
import { useNotificationRouting } from "@/lib/notifications"
import { registerCrossCodeWidgets, refreshWidgets } from "@/lib/widget-task"
import { useSettings } from "@/store/settings.store"
import { useSessions } from "@/store/sessions.store"
import { useConnections } from "@/store/connection.store"
import { useOpencodeStats } from "@/store/opencode-stats.store"
import { useQuestions } from "@/store/questions.store"

SplashScreen.preventAutoHideAsync()

registerCrossCodeWidgets()

export default function RootLayout() {
const router = useRouter()
const segments = useSegments()
Expand All @@ -49,6 +56,28 @@ export default function RootLayout() {
}
}, [fontsLoaded, hasCompletedOnboarding, router, segments, settingsHydrated])

React.useEffect(() => {
let timer: ReturnType<typeof setTimeout> | undefined
const schedule = () => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
refreshWidgets()
}, 800)
}
const unsubSessions = useSessions.subscribe(schedule)
const unsubConnections = useConnections.subscribe(schedule)
const unsubStats = useOpencodeStats.subscribe(schedule)
const unsubQuestions = useQuestions.subscribe(schedule)
refreshWidgets()
return () => {
if (timer) clearTimeout(timer)
unsubSessions()
unsubConnections()
unsubStats()
unsubQuestions()
}
}, [])

React.useEffect(() => {
if (fontsLoaded && settingsHydrated) {
SplashScreen.hideAsync()
Expand Down
Loading
Loading