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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.4.4] - 2026-06-15

### Added

- 🧙 **Setup wizard.** A friendly first-run guide walks you through picking a folder and connecting your AI. Pops up automatically after sign-up.
- 🔌 **Local tool servers (stdio).** You can now connect MCP tool servers that run as local commands — not just over HTTP. Add the command and arguments from the Tool Servers tab.
- 📄 **Read documents.** The AI can now open and read PDFs, Word docs, Excel spreadsheets, PowerPoint files, and more.
- 💬 **Send input to running commands.** The AI can now type into running processes — answering prompts, interacting with REPLs, or sending Ctrl-C.
- ↩️ **Undo last commit.** Changed your mind? Undo the last commit from the Git history and get your changes back in staging.
- 🚀 **Publish branches.** Push a new branch for the first time with one click. The button says "Publish" when there's no upstream yet.
- 🔗 **View on GitHub / GitLab.** A new link in the Git panel takes you straight to your repo on the web.
- 📋 **Commit actions menu.** Click the dots on any commit to copy its hash. On the latest commit you can also undo it.
- 🔗 **Share cptr.** Quick links on the About page to share cptr on X, Reddit, LinkedIn, or copy the URL.

### Changed

- ⚡ **Smoother command execution.** Commands now run in a real terminal (PTY) by default. You can choose how long to wait for output before moving on.
- 🧠 **Better reasoning model support.** Models like o3 and o4-mini now keep their chain of thought across tool calls, giving more accurate results.
- 📝 **Updated README.** New sections on accessing cptr from your phone and a list of compatible terminal agents.
- 🔧 **Clearer error messages.** Validation errors now show what actually went wrong instead of a generic status code.

### Fixed

- 🔒 **Gateway connections working again.** Fixed an error that could break the gateway models endpoint.

## [0.4.3] - 2026-06-14

### Changed
Expand Down
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,35 @@ cptr run

Or with [uv](https://docs.astral.sh/uv/): `uvx cptr@latest run`

Opens in your browser. From other devices:
Opens in your browser at `http://localhost:8000`.

### Access from your phone

Same Wi-Fi? Bind to all interfaces:

```bash
cptr run --host 0.0.0.0
```

Open `http://<your-computer-ip>:8000` on your phone.

Not on the same network? Use a tunnel:

- **[Tailscale](https://tailscale.com)** creates a private mesh network between your devices. Recommended.
- **[Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/)** gives you a permanent URL through Cloudflare's edge.
- **[ngrok](https://ngrok.com)** gives you a public URL in one command.

Or skip networking entirely and connect a [messaging bot](#messaging-bots) instead.

## What you get

| | |
|---|---|
| 📁 **File browser** | Navigate, create, rename, upload, drag and drop. Icons by type, sizes at a glance. |
| ⌨️ **Terminal** | Full PTY-backed shell in the browser. Anything you'd run at your desk. |
| ⌨️ **Terminal** | Full shell in the browser. Run your tools, your scripts, or your favourite coding agent. |
| 🔀 **Git** | Stage, commit, diff, branch, push. Visual changes view. No command line required. |
| ✏️ **Editor** | Syntax-highlighted editing with tabs. Open multiple files side by side. |
| 🗂️ **Tabs** | Open terminals, files, chats, and tools in separate tabs. Rearrange or split your layout. |
| 📂 **Workspaces** | Multiple projects, one instance. Switch without losing your place. |
| 🔍 **Search** | Find files by name, search across file contents and chat history. ⌘K to find anything. |
| 📱 **Mobile-first** | Not a desktop UI made smaller. Built for the screen in your pocket. |
Expand All @@ -63,6 +78,8 @@ Bring your own API key. Works with OpenAI, Anthropic, Ollama, or any OpenAI-comp
| 🔌 **Tool servers** | Connect external tools via MCP or OpenAPI. |
| 🧠 **Context compaction** | Long conversations are automatically summarised to stay fast. |

Already have a favourite terminal agent? Claude Code, Codex, Gemini CLI, Cursor, Grok, OpenCode, Kilo Code, and Pi all plug straight in. Use the subscription you already pay for.

## Messaging bots

Connect the AI to your chat apps. Full tool access, streaming responses, conversations synced back to the web UI.
Expand Down
7 changes: 7 additions & 0 deletions cptr/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ async def shutdown():
await shutdown_browser()
except Exception:
pass
# Clean up stdio MCP server processes
try:
from cptr.utils.mcp.stdio_manager import stdio_manager

await stdio_manager.disconnect_all()
except Exception:
pass


# Auth middleware
Expand Down
8 changes: 8 additions & 0 deletions cptr/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@
CHAT_TOOL_COMMAND_MAX_CHARS = int(os.environ.get("CHAT_TOOL_COMMAND_MAX_CHARS", "8000"))
CHAT_COMPACT_TOKEN_THRESHOLD = int(os.environ.get("CHAT_COMPACT_TOKEN_THRESHOLD", "80000"))

# ── Execute timeout ─────────────────────────────────────────
# Default wait (seconds) for run_command / check_task when the caller
# doesn't pass an explicit wait value. None = return immediately.
EXECUTE_TIMEOUT: float | None = None
_execute_timeout = os.environ.get("CPTR_EXECUTE_TIMEOUT")
if _execute_timeout is not None:
EXECUTE_TIMEOUT = float(_execute_timeout)

# ── AI stream settings ──────────────────────────────────────
STREAM_CONNECT_TIMEOUT_SECONDS = float(os.environ.get("CPTR_STREAM_CONNECT_TIMEOUT", "30"))
STREAM_READ_TIMEOUT_SECONDS = float(os.environ.get("CPTR_STREAM_READ_TIMEOUT", "300"))
Expand Down
7 changes: 6 additions & 1 deletion cptr/frontend/src/lib/apis/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export const updateModelConfig = (

export interface ToolServer {
id: string;
type: 'openapi' | 'mcp';
type: 'openapi' | 'mcp' | 'mcp_stdio';
url: string;
path: string;
auth_type: string;
Expand All @@ -142,6 +142,11 @@ export interface ToolServer {
description: string;
headers: Record<string, string> | null;
enabled: boolean;
// Stdio MCP fields
command?: string;
args?: string[];
env?: Record<string, string> | null;
cwd?: string | null;
}

export const listToolServers = async (): Promise<ToolServer[]> => {
Expand Down
8 changes: 7 additions & 1 deletion cptr/frontend/src/lib/apis/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ export const gitCommit = (root: string, message: string) =>

export const gitPull = (root: string) => fetchJSON('/api/git/pull', jsonBody({ root }));

export const gitPush = (root: string) => fetchJSON('/api/git/push', jsonBody({ root }));
export const gitPush = (
root: string,
{ force = false, set_upstream = false, branch }: { force?: boolean; set_upstream?: boolean; branch?: string } = {}
) => fetchJSON('/api/git/push', jsonBody({ root, force, set_upstream, branch }));

export const gitUncommit = (root: string) =>
fetchJSON('/api/git/uncommit', jsonBody({ root }));

export const createGitBranch = (root: string, name: string) =>
fetchJSON('/api/git/branch', jsonBody({ root, name }));
Expand Down
2 changes: 1 addition & 1 deletion cptr/frontend/src/lib/apis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function fetchJSON<T = unknown>(path: string, init?: RequestInit):
const res = await fetchHandler(path, init);
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new ApiError(res.status, data.error || res.statusText);
throw new ApiError(res.status, data.detail || data.error || res.statusText);
}
return res.json();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
let { onclose, oncreated }: Props = $props();

let formName = $state('');
let formProvider = $state<'anthropic' | 'openai'>('anthropic');
let formProvider = $state<'openai' | 'anthropic'>('openai');
let formApiType = $state<'chat_completions' | 'responses'>('chat_completions');
let formBaseUrl = $state('');
let formApiKey = $state('');
Expand Down Expand Up @@ -94,8 +94,8 @@
bind:value={formProvider}
class="block w-full bg-transparent text-[13px] text-gray-700 dark:text-gray-300 outline-none py-0.5 cursor-pointer"
>
<option value="anthropic">Anthropic</option>
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
</select>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@
bind:value={formProvider}
class="block w-full bg-transparent text-[13px] text-gray-700 dark:text-gray-300 outline-none py-0.5 cursor-pointer"
>
<option value="anthropic">Anthropic</option>
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
</select>
</div>
</div>
Expand Down
Loading
Loading