diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a10d64..d9596a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 4dfd91a..c66d989 100644 --- a/README.md +++ b/README.md @@ -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://: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. | @@ -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. diff --git a/cptr/app.py b/cptr/app.py index 4c09e0c..4ba9d82 100644 --- a/cptr/app.py +++ b/cptr/app.py @@ -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 diff --git a/cptr/env.py b/cptr/env.py index dc178f3..cc3cf26 100644 --- a/cptr/env.py +++ b/cptr/env.py @@ -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")) diff --git a/cptr/frontend/src/lib/apis/admin.ts b/cptr/frontend/src/lib/apis/admin.ts index f3c71b8..4a6869c 100644 --- a/cptr/frontend/src/lib/apis/admin.ts +++ b/cptr/frontend/src/lib/apis/admin.ts @@ -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; @@ -142,6 +142,11 @@ export interface ToolServer { description: string; headers: Record | null; enabled: boolean; + // Stdio MCP fields + command?: string; + args?: string[]; + env?: Record | null; + cwd?: string | null; } export const listToolServers = async (): Promise => { diff --git a/cptr/frontend/src/lib/apis/git.ts b/cptr/frontend/src/lib/apis/git.ts index b40a2b3..382bf18 100644 --- a/cptr/frontend/src/lib/apis/git.ts +++ b/cptr/frontend/src/lib/apis/git.ts @@ -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 })); diff --git a/cptr/frontend/src/lib/apis/index.ts b/cptr/frontend/src/lib/apis/index.ts index 77c1994..c4d81d5 100644 --- a/cptr/frontend/src/lib/apis/index.ts +++ b/cptr/frontend/src/lib/apis/index.ts @@ -23,7 +23,7 @@ export async function fetchJSON(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(); } diff --git a/cptr/frontend/src/lib/components/Admin/CreateConnectionModal.svelte b/cptr/frontend/src/lib/components/Admin/CreateConnectionModal.svelte index 2f94378..a4fbfba 100644 --- a/cptr/frontend/src/lib/components/Admin/CreateConnectionModal.svelte +++ b/cptr/frontend/src/lib/components/Admin/CreateConnectionModal.svelte @@ -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(''); @@ -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" > - + diff --git a/cptr/frontend/src/lib/components/Admin/EditConnectionModal.svelte b/cptr/frontend/src/lib/components/Admin/EditConnectionModal.svelte index ae57bee..c54d31e 100644 --- a/cptr/frontend/src/lib/components/Admin/EditConnectionModal.svelte +++ b/cptr/frontend/src/lib/components/Admin/EditConnectionModal.svelte @@ -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" > - + diff --git a/cptr/frontend/src/lib/components/Admin/ToolServers.svelte b/cptr/frontend/src/lib/components/Admin/ToolServers.svelte index 4c45f06..34a31b2 100644 --- a/cptr/frontend/src/lib/components/Admin/ToolServers.svelte +++ b/cptr/frontend/src/lib/components/Admin/ToolServers.svelte @@ -23,7 +23,7 @@ // Form state let formId = $state(''); - let formType = $state<'openapi' | 'mcp'>('openapi'); + let formType = $state<'openapi' | 'mcp' | 'mcp_stdio'>('openapi'); let formUrl = $state(''); let formPath = $state('openapi.json'); let formAuthType = $state('bearer'); @@ -31,6 +31,10 @@ let formName = $state(''); let formDescription = $state(''); let formHeaders = $state(''); + // Stdio fields + let formCommand = $state(''); + let formArgs = $state(''); + let formCwd = $state(''); let saving = $state(false); let verifying = $state(false); @@ -59,6 +63,9 @@ formName = ''; formDescription = ''; formHeaders = ''; + formCommand = ''; + formArgs = ''; + formCwd = ''; verifyResult = null; showModal = true; @@ -75,13 +82,25 @@ formName = s.name; formDescription = s.description; formHeaders = s.headers ? JSON.stringify(s.headers, null, 2) : ''; + formCommand = s.command || ''; + formArgs = (s.args || []).join(' '); + formCwd = s.cwd || ''; verifyResult = null; showModal = true; } async function handleSubmit() { - if (!formId.trim() || !formUrl.trim()) { + const isStdio = formType === 'mcp_stdio'; + if (!formId.trim()) { + toast.error($t('toolServers.fieldsRequired')); + return; + } + if (isStdio && !formCommand.trim()) { + toast.error($t('toolServers.commandRequired')); + return; + } + if (!isStdio && !formUrl.trim()) { toast.error($t('toolServers.fieldsRequired')); return; } @@ -110,6 +129,7 @@ name: formName.trim() || (() => { + if (formType === 'mcp_stdio') return formCommand.trim().split('/').pop() || 'stdio'; try { return new URL(formUrl.trim()).hostname; } catch { @@ -117,7 +137,10 @@ } })(), description: formDescription.trim(), - headers: parsedHeaders || null + headers: parsedHeaders || null, + command: formCommand.trim(), + args: formArgs.trim() ? formArgs.trim().split(/\s+/) : [], + cwd: formCwd.trim() || null }; if (editServer) { if (formKey.trim()) data.key = formKey.trim(); @@ -205,17 +228,17 @@ > - {s.type === 'mcp' ? 'MCP' : 'API'} + {s.type === 'mcp' ? 'MCP' : s.type === 'mcp_stdio' ? 'STDIO' : 'API'} - {s.name || s.url} + {s.name || s.command || s.url} + @@ -305,20 +329,59 @@ class="block w-full bg-transparent text-[13px] text-gray-700 dark:text-gray-300 placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none py-0.5" /> - - - + + {#if formType !== 'mcp_stdio'} + + + {/if} + + + {#if formType === 'mcp_stdio'} + + + + + + + {/if} {#if formType === 'openapi'} @@ -335,35 +398,37 @@ /> {/if} - -
-
- - -
- {#if formAuthType === 'bearer'} -
+ + {#if formType !== 'mcp_stdio'} +
+
{$t('toolServers.auth')} - +
- {/if} -
+ {#if formAuthType === 'bearer'} +
+ + +
+ {/if} +
+ {/if}