diff --git a/docs/content/docs/cua/reference/agent-sdk/index.mdx b/docs/content/docs/cua/reference/agent-sdk/index.mdx
index ae458d5934..27a291ef95 100644
--- a/docs/content/docs/cua/reference/agent-sdk/index.mdx
+++ b/docs/content/docs/cua/reference/agent-sdk/index.mdx
@@ -7,7 +7,7 @@ description: Python API reference for building computer-use agents
AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
Generated by: npx tsx scripts/docs-generators/python-sdk.ts
Source: libs/python/agent/agent
- Version: 0.7.24
+ Version: 0.7.27
*/}
import { Callout } from 'fumadocs-ui/components/callout';
@@ -17,7 +17,7 @@ import { VersionHeader } from '@/components/version-selector';
@@ -1130,7 +1130,7 @@ Called when an LLM API call has completed.
*Inherits from: AsyncCallbackHandler*
-Callback that captures errors and sends them to Sentry/OTEL.
+Callback that captures errors and sends them to OTEL.
Should be added early in the callback chain to catch all errors.
diff --git a/docs/content/docs/cua/reference/cli/index.mdx b/docs/content/docs/cua/reference/cli/index.mdx
index 134505206f..352721ac38 100644
--- a/docs/content/docs/cua/reference/cli/index.mdx
+++ b/docs/content/docs/cua/reference/cli/index.mdx
@@ -7,7 +7,7 @@ description: Python API reference for the Cua command-line interface
AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
Generated by: npx tsx scripts/docs-generators/python-sdk.ts
Source: libs/python/cua-cli/cua_cli
- Version: 0.1.0
+ Version: 0.1.5
*/}
import { Callout } from 'fumadocs-ui/components/callout';
@@ -17,7 +17,7 @@ import { VersionHeader } from '@/components/version-selector';
diff --git a/docs/content/docs/cua/reference/computer-sdk/index.mdx b/docs/content/docs/cua/reference/computer-sdk/index.mdx
index cb1025a3c8..a7b64d911a 100644
--- a/docs/content/docs/cua/reference/computer-sdk/index.mdx
+++ b/docs/content/docs/cua/reference/computer-sdk/index.mdx
@@ -7,7 +7,7 @@ description: Python API reference for controlling virtual machines and computer
AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
Generated by: npx tsx scripts/docs-generators/python-sdk.ts
Source: libs/python/computer/computer
- Version: 0.5.12
+ Version: 0.5.14
*/}
import { Callout } from 'fumadocs-ui/components/callout';
@@ -17,7 +17,7 @@ import { VersionHeader } from '@/components/version-selector';
diff --git a/docs/content/docs/cuabench/reference/api.mdx b/docs/content/docs/cuabench/reference/api.mdx
index 849c840192..a6ebc9abf3 100644
--- a/docs/content/docs/cuabench/reference/api.mdx
+++ b/docs/content/docs/cuabench/reference/api.mdx
@@ -7,7 +7,7 @@ description: Python API reference for the desktop automation benchmarking framew
AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
Generated by: npx tsx scripts/docs-generators/python-sdk.ts
Source: libs/cua-bench/cua_bench
- Version: 0.2.3
+ Version: 0.2.4
*/}
import { Callout } from 'fumadocs-ui/components/callout';
@@ -17,7 +17,7 @@ import { VersionHeader } from '@/components/version-selector';
@@ -3631,7 +3631,7 @@ Run a task with 2-container architecture.
#### TaskRunner.run_task_interactively
```python
-async def run_task_interactively(self, env_type: str, golden_name: Optional[str] = None, env_path: Optional[Path] = None, task_index: int = 0, memory: str = '8G', cpus: str = '8', vnc_port: Optional[int] = None, api_port: Optional[int] = None, auto_allocate_ports: bool = True, cleanup_before: bool = True) -> tuple[str, str, callable, Optional[dict]]
+async def run_task_interactively(self, env_type: str, golden_name: Optional[str] = None, env_path: Optional[Path] = None, task_index: int = 0, setup_config: Optional[dict] = None, memory: str = '8G', cpus: str = '8', vnc_port: Optional[int] = None, api_port: Optional[int] = None, auto_allocate_ports: bool = True, cleanup_before: bool = True) -> tuple[str, str, callable, Optional[dict]]
```
Start an environment container interactively (without agent).
diff --git a/libs/python/agent/agent/adapters/cua_adapter.py b/libs/python/agent/agent/adapters/cua_adapter.py
index 7bfb4e0308..0301903064 100644
--- a/libs/python/agent/agent/adapters/cua_adapter.py
+++ b/libs/python/agent/agent/adapters/cua_adapter.py
@@ -1,6 +1,7 @@
import os
from typing import Any, AsyncIterator, Iterator
+from core.http import cua_version_headers
from litellm import acompletion, completion
from litellm.llms.custom_llm import CustomLLM
from litellm.types.utils import GenericStreamingChunk, ModelResponse
@@ -73,6 +74,11 @@ def completion(self, *args, **kwargs) -> ModelResponse:
params["headers"] = kwargs["headers"]
del kwargs["headers"]
+ # Always include CUA version headers
+ version_hdrs = cua_version_headers()
+ if version_hdrs:
+ params["headers"] = {**version_hdrs, **params.get("headers", {})}
+
# Print dropped parameters
original_keys = set(kwargs.keys())
used_keys = set(params.keys()) # Only these are extracted from kwargs
@@ -130,6 +136,11 @@ async def acompletion(self, *args, **kwargs) -> ModelResponse:
params["headers"] = kwargs["headers"]
del kwargs["headers"]
+ # Always include CUA version headers
+ version_hdrs = cua_version_headers()
+ if version_hdrs:
+ params["headers"] = {**version_hdrs, **params.get("headers", {})}
+
# Print dropped parameters
original_keys = set(kwargs.keys())
used_keys = set(params.keys()) # Only these are extracted from kwargs
@@ -166,6 +177,10 @@ def streaming(self, *args, **kwargs) -> Iterator[GenericStreamingChunk]:
"stream": True,
}
)
+ # Always include CUA version headers
+ version_hdrs = cua_version_headers()
+ if version_hdrs:
+ params["headers"] = {**version_hdrs, **params.get("headers", {})}
# Yield chunks directly from LiteLLM's streaming generator
for chunk in completion(**params): # type: ignore
yield chunk # type: ignore
@@ -182,6 +197,10 @@ async def astreaming(self, *args, **kwargs) -> AsyncIterator[GenericStreamingChu
"stream": True,
}
)
+ # Always include CUA version headers
+ version_hdrs = cua_version_headers()
+ if version_hdrs:
+ params["headers"] = {**version_hdrs, **params.get("headers", {})}
stream = await acompletion(**params) # type: ignore
async for chunk in stream: # type: ignore
yield chunk # type: ignore
diff --git a/libs/python/computer-server/computer_server/main.py b/libs/python/computer-server/computer_server/main.py
index 894d5b9453..28d405ecdc 100644
--- a/libs/python/computer-server/computer_server/main.py
+++ b/libs/python/computer-server/computer_server/main.py
@@ -270,8 +270,10 @@ async def auth(self, container_name: str, api_key: str) -> bool:
logger.info(f"Authenticating with TryCUA API for container: {container_name}")
try:
+ from core.http import cua_version_headers
+
async with aiohttp.ClientSession() as session:
- headers = {"Authorization": f"Bearer {api_key}"}
+ headers = {"Authorization": f"Bearer {api_key}", **cua_version_headers()}
async with session.get(
f"https://www.cua.ai/api/vm/auth?container_name={container_name}",
diff --git a/libs/python/computer/computer/interface/generic.py b/libs/python/computer/computer/interface/generic.py
index ba56082414..ebecda0b19 100644
--- a/libs/python/computer/computer/interface/generic.py
+++ b/libs/python/computer/computer/interface/generic.py
@@ -5,6 +5,7 @@
import aiohttp
import websockets
+from core.http import cua_version_headers
from PIL import Image
from ..logger import Logger, LogLevel
@@ -708,7 +709,7 @@ async def playwright_exec(self, command: str, params: Optional[Dict] = None) ->
url = f"{protocol}://{self.ip_address}:{port}/playwright_exec"
payload = {"command": command, "params": params or {}}
- headers = {"Content-Type": "application/json"}
+ headers = {"Content-Type": "application/json", **cua_version_headers()}
if self.api_key:
headers["X-API-Key"] = self.api_key
if self.vm_name:
@@ -947,7 +948,7 @@ async def _send_command_rest(
payload = {"command": command, "params": params or {}}
# Prepare headers
- headers = {"Content-Type": "application/json"}
+ headers = {"Content-Type": "application/json", **cua_version_headers()}
if self.api_key:
headers["X-API-Key"] = self.api_key
if self.vm_name:
diff --git a/libs/python/computer/computer/providers/cloud/provider.py b/libs/python/computer/computer/providers/cloud/provider.py
index 615fc46b50..aa66ebcee4 100644
--- a/libs/python/computer/computer/providers/cloud/provider.py
+++ b/libs/python/computer/computer/providers/cloud/provider.py
@@ -22,6 +22,7 @@
from urllib.parse import urlparse
import aiohttp
+from core.http import cua_version_headers
DEFAULT_API_BASE = os.getenv("CUA_API_BASE", "https://api.cua.ai")
@@ -54,6 +55,13 @@ def __init__(
# Host caching dictionary: {vm_name: host_string}
self._host_cache: Dict[str, str] = {}
+ def _base_headers(self) -> Dict[str, str]:
+ return {
+ "Authorization": f"Bearer {self.api_key}",
+ "Accept": "application/json",
+ **cua_version_headers(),
+ }
+
@property
def provider_type(self) -> VMProviderType:
return VMProviderType.CLOUD
@@ -75,7 +83,7 @@ async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, An
# Query the API for authoritative VM info
url = f"{self.api_base}/v1/vms/{name}"
- headers = {"Authorization": f"Bearer {self.api_key}", "Accept": "application/json"}
+ headers = self._base_headers()
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as resp:
@@ -112,10 +120,7 @@ async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, An
async def list_vms(self) -> ListVMsResponse:
url = f"{self.api_base}/v1/vms"
- headers = {
- "Authorization": f"Bearer {self.api_key}",
- "Accept": "application/json",
- }
+ headers = self._base_headers()
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as resp:
if resp.status == 200:
@@ -175,10 +180,7 @@ async def run_vm(
) -> Dict[str, Any]:
"""Start a VM via public API. Returns a minimal status."""
url = f"{self.api_base}/v1/vms/{name}/start"
- headers = {
- "Authorization": f"Bearer {self.api_key}",
- "Accept": "application/json",
- }
+ headers = self._base_headers()
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers) as resp:
if resp.status in (200, 201, 202, 204):
@@ -194,10 +196,7 @@ async def run_vm(
async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
"""Stop a VM via public API."""
url = f"{self.api_base}/v1/vms/{name}/stop"
- headers = {
- "Authorization": f"Bearer {self.api_key}",
- "Accept": "application/json",
- }
+ headers = self._base_headers()
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers) as resp:
if resp.status in (200, 202):
@@ -220,10 +219,7 @@ async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, A
async def restart_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
"""Restart a VM via public API."""
url = f"{self.api_base}/v1/vms/{name}/restart"
- headers = {
- "Authorization": f"Bearer {self.api_key}",
- "Accept": "application/json",
- }
+ headers = self._base_headers()
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers) as resp:
if resp.status in (200, 202):
diff --git a/libs/python/computer/computer/providers/cloud/providerv2.py b/libs/python/computer/computer/providers/cloud/providerv2.py
index d5519ac05c..707b6a4a31 100644
--- a/libs/python/computer/computer/providers/cloud/providerv2.py
+++ b/libs/python/computer/computer/providers/cloud/providerv2.py
@@ -18,6 +18,7 @@
import os
import aiohttp
+from core.http import cua_version_headers
DEFAULT_API_BASE = os.getenv("CUA_API_BASE", "https://api.cua.ai")
@@ -48,6 +49,13 @@ def __init__(
self.verbose = verbose
self.api_base = (api_base or DEFAULT_API_BASE).rstrip("/")
+ def _base_headers(self) -> Dict[str, str]:
+ return {
+ "Authorization": f"Bearer {self.api_key}",
+ "Accept": "application/json",
+ **cua_version_headers(),
+ }
+
@property
def provider_type(self) -> VMProviderType:
return VMProviderType.CLOUDV2
@@ -91,7 +99,7 @@ async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, An
# Query the API for authoritative VM info
url = f"{self.api_base}/v1/vms/{name}"
- headers = {"Authorization": f"Bearer {self.api_key}", "Accept": "application/json"}
+ headers = self._base_headers()
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as resp:
@@ -125,10 +133,7 @@ async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, An
async def list_vms(self) -> ListVMsResponse:
url = f"{self.api_base}/v1/vms"
- headers = {
- "Authorization": f"Bearer {self.api_key}",
- "Accept": "application/json",
- }
+ headers = self._base_headers()
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as resp:
if resp.status == 200:
@@ -179,10 +184,7 @@ async def run_vm(
) -> Dict[str, Any]:
"""Start a VM via public API. Returns a minimal status."""
url = f"{self.api_base}/v1/vms/{name}/start"
- headers = {
- "Authorization": f"Bearer {self.api_key}",
- "Accept": "application/json",
- }
+ headers = self._base_headers()
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers) as resp:
if resp.status in (200, 201, 202, 204):
@@ -198,10 +200,7 @@ async def run_vm(
async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
"""Stop a VM via public API."""
url = f"{self.api_base}/v1/vms/{name}/stop"
- headers = {
- "Authorization": f"Bearer {self.api_key}",
- "Accept": "application/json",
- }
+ headers = self._base_headers()
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers) as resp:
if resp.status in (200, 202):
@@ -223,10 +222,7 @@ async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, A
async def restart_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
"""Restart a VM via public API."""
url = f"{self.api_base}/v1/vms/{name}/restart"
- headers = {
- "Authorization": f"Bearer {self.api_key}",
- "Accept": "application/json",
- }
+ headers = self._base_headers()
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers) as resp:
if resp.status in (200, 202):
diff --git a/libs/python/core/core/__init__.py b/libs/python/core/core/__init__.py
index 9716bfd2d2..1af2cfbe08 100644
--- a/libs/python/core/core/__init__.py
+++ b/libs/python/core/core/__init__.py
@@ -1,3 +1,5 @@
"""Core functionality shared across Cua components."""
__version__ = "0.1.8"
+
+from core.http import CUA_CLIENT_VERSION_HEADER, cua_version_headers
diff --git a/libs/python/core/core/http.py b/libs/python/core/core/http.py
new file mode 100644
index 0000000000..b0e628db86
--- /dev/null
+++ b/libs/python/core/core/http.py
@@ -0,0 +1,35 @@
+"""Shared HTTP utilities for CUA SDK requests."""
+
+from functools import lru_cache
+
+CUA_CLIENT_VERSION_HEADER = "X-Cua-Client-Version"
+
+# CUA packages whose versions are included in the header value.
+_CUA_PACKAGES = ("cua-agent", "cua-computer", "cua-core")
+
+
+@lru_cache(maxsize=1)
+def _build_version_string() -> str:
+ """Return a composite version string like ``agent:0.4.0 computer:0.1.0 core:0.1.8``."""
+ from importlib.metadata import PackageNotFoundError, version
+
+ parts: list[str] = []
+ for pkg in _CUA_PACKAGES:
+ try:
+ short = pkg.removeprefix("cua-")
+ parts.append(f"{short}:{version(pkg)}")
+ except PackageNotFoundError:
+ continue
+ return " ".join(parts)
+
+
+def cua_version_headers() -> dict[str, str]:
+ """Return headers dict containing the CUA client version header.
+
+ Only installed CUA packages are included. If none are found the dict is
+ empty so it is always safe to unpack with ``**cua_version_headers()``.
+ """
+ value = _build_version_string()
+ if not value:
+ return {}
+ return {CUA_CLIENT_VERSION_HEADER: value}
diff --git a/libs/python/cua-cli/cua_cli/api/client.py b/libs/python/cua-cli/cua_cli/api/client.py
index 7dee69092c..5b92e9d479 100644
--- a/libs/python/cua-cli/cua_cli/api/client.py
+++ b/libs/python/cua-cli/cua_cli/api/client.py
@@ -7,6 +7,7 @@
from urllib.parse import quote
import aiohttp
+from core.http import cua_version_headers
from cua_cli.auth.store import require_api_key
DEFAULT_API_BASE = "https://api.cua.ai"
@@ -28,6 +29,7 @@ def _headers(self) -> dict[str, str]:
return {
"Authorization": f"Bearer {self.api_key}",
"Accept": "application/json",
+ **cua_version_headers(),
}
async def _request(
diff --git a/libs/python/cua-cli/cua_cli/commands/auth.py b/libs/python/cua-cli/cua_cli/commands/auth.py
index acb8b7de09..744ac27948 100644
--- a/libs/python/cua-cli/cua_cli/commands/auth.py
+++ b/libs/python/cua-cli/cua_cli/commands/auth.py
@@ -6,6 +6,7 @@
from typing import Any, Optional
import aiohttp
+from core.http import cua_version_headers
from cua_cli.auth.browser import authenticate_via_browser
from cua_cli.auth.store import clear_credentials, get_api_key, save_api_key
from cua_cli.utils.async_utils import run_async
@@ -160,7 +161,11 @@ def cmd_status(args: argparse.Namespace) -> int:
async def _fetch():
url = f"{_get_api_base()}/v1/me"
- headers = {"Authorization": f"Bearer {api_key}", "Accept": "application/json"}
+ headers = {
+ "Authorization": f"Bearer {api_key}",
+ "Accept": "application/json",
+ **cua_version_headers(),
+ }
async with aiohttp.ClientSession() as session:
timeout = aiohttp.ClientTimeout(total=10)
async with session.get(url, headers=headers, timeout=timeout) as resp:
diff --git a/libs/python/cua-cli/cua_cli/commands/do.py b/libs/python/cua-cli/cua_cli/commands/do.py
index 6a2d80111c..2a68ba31cf 100644
--- a/libs/python/cua-cli/cua_cli/commands/do.py
+++ b/libs/python/cua-cli/cua_cli/commands/do.py
@@ -336,9 +336,10 @@ async def _send(provider_type: str, name: str, command: str, params: dict) -> di
return await _host_dispatch(command, params)
import aiohttp
+ from core.http import cua_version_headers
api_url = await _get_api_url(provider_type, name)
- headers = {"Content-Type": "application/json"}
+ headers = {"Content-Type": "application/json", **cua_version_headers()}
if provider_type in ("cloud", "cloudv2"):
from cua_cli.auth.store import get_api_key
diff --git a/libs/python/cua-cli/cua_cli/commands/mcp.py b/libs/python/cua-cli/cua_cli/commands/mcp.py
index 8e3ca017da..376b75fc4a 100644
--- a/libs/python/cua-cli/cua_cli/commands/mcp.py
+++ b/libs/python/cua-cli/cua_cli/commands/mcp.py
@@ -442,10 +442,13 @@ async def _send_command(sandbox_name: str, command: str, params: dict) -> dict:
server_url = await _get_server_url(sandbox_name)
api_key = get_api_key()
+ from core.http import cua_version_headers
+
headers = {
"Content-Type": "application/json",
"X-API-Key": api_key,
"X-Container-Name": sandbox_name or default_sandbox,
+ **cua_version_headers(),
}
async with aiohttp.ClientSession() as session:
diff --git a/libs/python/cua-cli/cua_cli/commands/sandbox.py b/libs/python/cua-cli/cua_cli/commands/sandbox.py
index de5e7d276f..defbc711ba 100644
--- a/libs/python/cua-cli/cua_cli/commands/sandbox.py
+++ b/libs/python/cua-cli/cua_cli/commands/sandbox.py
@@ -7,6 +7,7 @@
from urllib.parse import quote
import aiohttp
+from core.http import cua_version_headers
from cua_cli.auth.store import require_api_key
from cua_cli.utils.async_utils import run_async
from cua_cli.utils.output import (
@@ -37,6 +38,7 @@ async def _api_request(
headers = {
"Authorization": f"Bearer {api_key}",
"Accept": "application/json",
+ **cua_version_headers(),
}
if json is not None:
diff --git a/libs/typescript/agent/package.json b/libs/typescript/agent/package.json
index 5d768a67bd..2cebcd76e5 100644
--- a/libs/typescript/agent/package.json
+++ b/libs/typescript/agent/package.json
@@ -38,7 +38,7 @@
"prepublishOnly": "pnpm run build"
},
"dependencies": {
- "@trycua/core": "^0.1.2",
+ "@trycua/core": "workspace:^",
"peerjs": "^1.5.4",
"pino": "^9.7.0"
},
diff --git a/libs/typescript/agent/src/client.ts b/libs/typescript/agent/src/client.ts
index 2ae4da6628..1e7216fe37 100644
--- a/libs/typescript/agent/src/client.ts
+++ b/libs/typescript/agent/src/client.ts
@@ -1,4 +1,5 @@
import { Peer } from 'peerjs';
+import { cuaVersionHeaders } from '@trycua/core';
import type { AgentRequest, AgentResponse, ConnectionType, AgentClientOptions } from './types';
export class AgentClient {
@@ -52,6 +53,7 @@ export class AgentClient {
try {
const headers: Record = {
'Content-Type': 'application/json',
+ ...cuaVersionHeaders('agent', __CUA_VERSION__),
};
if (this.options.apiKey) {
headers['X-API-Key'] = this.options.apiKey;
diff --git a/libs/typescript/agent/src/globals.d.ts b/libs/typescript/agent/src/globals.d.ts
new file mode 100644
index 0000000000..f78c5ad001
--- /dev/null
+++ b/libs/typescript/agent/src/globals.d.ts
@@ -0,0 +1,2 @@
+/** Build-time constant injected by tsdown `define`. */
+declare const __CUA_VERSION__: string;
diff --git a/libs/typescript/agent/tsdown.config.ts b/libs/typescript/agent/tsdown.config.ts
index efbd5ff2ef..2953918b7c 100644
--- a/libs/typescript/agent/tsdown.config.ts
+++ b/libs/typescript/agent/tsdown.config.ts
@@ -1,5 +1,8 @@
+import { readFileSync } from 'node:fs';
import { defineConfig } from 'tsdown';
+const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf-8'));
+
export default defineConfig({
entry: ['src/index.ts'],
format: ['module'],
@@ -9,4 +12,7 @@ export default defineConfig({
// Remove if we don't need to support including the library via '' tags.
// noExternal bundles this list of libraries within the final 'dist'
noExternal: ['peerjs'],
+ define: {
+ __CUA_VERSION__: JSON.stringify(pkg.version),
+ },
});
diff --git a/libs/typescript/computer/package.json b/libs/typescript/computer/package.json
index f8fecc7f8e..a37befce04 100644
--- a/libs/typescript/computer/package.json
+++ b/libs/typescript/computer/package.json
@@ -38,7 +38,7 @@
"prepublishOnly": "pnpm run build"
},
"dependencies": {
- "@trycua/core": "^0.1.2",
+ "@trycua/core": "workspace:^",
"pino": "^9.7.0",
"uuid": "^11.0.0",
"ws": "^8.18.0"
diff --git a/libs/typescript/computer/src/computer/providers/cloud.ts b/libs/typescript/computer/src/computer/providers/cloud.ts
index 3e0d228ea5..9475283026 100644
--- a/libs/typescript/computer/src/computer/providers/cloud.ts
+++ b/libs/typescript/computer/src/computer/providers/cloud.ts
@@ -1,6 +1,7 @@
import { createHash } from 'node:crypto';
import pino from 'pino';
import { v4 as uuidv4 } from 'uuid';
+import { cuaVersionHeaders } from '@trycua/core';
import { type BaseComputerInterface, InterfaceFactory } from '../../interface/index';
import type { CloudComputerConfig, VMProviderType } from '../types';
import { BaseComputer } from './base';
@@ -63,6 +64,10 @@ export class CloudComputer extends BaseComputer {
headers: {
Authorization: `Bearer ${this.apiKey}`,
Accept: 'application/json',
+ ...cuaVersionHeaders(
+ 'computer',
+ typeof __CUA_VERSION__ !== 'undefined' ? __CUA_VERSION__ : ''
+ ),
},
});
diff --git a/libs/typescript/computer/src/globals.d.ts b/libs/typescript/computer/src/globals.d.ts
new file mode 100644
index 0000000000..f78c5ad001
--- /dev/null
+++ b/libs/typescript/computer/src/globals.d.ts
@@ -0,0 +1,2 @@
+/** Build-time constant injected by tsdown `define`. */
+declare const __CUA_VERSION__: string;
diff --git a/libs/typescript/computer/tsdown.config.ts b/libs/typescript/computer/tsdown.config.ts
index b3c70ea9e1..ef25c723b3 100644
--- a/libs/typescript/computer/tsdown.config.ts
+++ b/libs/typescript/computer/tsdown.config.ts
@@ -1,10 +1,16 @@
+import { readFileSync } from 'node:fs';
import { defineConfig } from 'tsdown';
+const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf-8'));
+
export default defineConfig([
{
entry: ['./src/index.ts'],
platform: 'node',
dts: true,
external: ['child_process', 'util'],
+ define: {
+ __CUA_VERSION__: JSON.stringify(pkg.version),
+ },
},
]);
diff --git a/libs/typescript/core/src/globals.d.ts b/libs/typescript/core/src/globals.d.ts
new file mode 100644
index 0000000000..f78c5ad001
--- /dev/null
+++ b/libs/typescript/core/src/globals.d.ts
@@ -0,0 +1,2 @@
+/** Build-time constant injected by tsdown `define`. */
+declare const __CUA_VERSION__: string;
diff --git a/libs/typescript/core/src/http.ts b/libs/typescript/core/src/http.ts
new file mode 100644
index 0000000000..bd1b6e1f95
--- /dev/null
+++ b/libs/typescript/core/src/http.ts
@@ -0,0 +1,17 @@
+/**
+ * Shared HTTP utilities for CUA SDK requests.
+ */
+
+export const CUA_CLIENT_VERSION_HEADER = 'X-Cua-Client-Version';
+
+/**
+ * Build a version header dict for CUA SDK requests.
+ *
+ * @param packageName - short package name, e.g. `'agent'`, `'computer'`
+ * @param version - semver string injected at build time
+ * @returns header record; empty when version is falsy, so always safe to spread.
+ */
+export function cuaVersionHeaders(packageName: string, version: string): Record {
+ if (!version) return {};
+ return { [CUA_CLIENT_VERSION_HEADER]: `${packageName}:${version}` };
+}
diff --git a/libs/typescript/core/src/index.ts b/libs/typescript/core/src/index.ts
index d340bdd3d2..d4d986d0ed 100644
--- a/libs/typescript/core/src/index.ts
+++ b/libs/typescript/core/src/index.ts
@@ -5,3 +5,4 @@
*/
export * from './telemetry';
+export * from './http';
diff --git a/libs/typescript/core/tsdown.config.ts b/libs/typescript/core/tsdown.config.ts
index 36743757ac..ca2dcd7347 100644
--- a/libs/typescript/core/tsdown.config.ts
+++ b/libs/typescript/core/tsdown.config.ts
@@ -1,9 +1,15 @@
+import { readFileSync } from 'node:fs';
import { defineConfig } from 'tsdown';
+const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf-8'));
+
export default defineConfig([
{
entry: ['./src/index.ts'],
platform: 'node',
dts: true,
+ define: {
+ __CUA_VERSION__: JSON.stringify(pkg.version),
+ },
},
]);
diff --git a/libs/typescript/cua-cli/src/commands/sandbox.ts b/libs/typescript/cua-cli/src/commands/sandbox.ts
index 9264fcc139..9551b2646f 100644
--- a/libs/typescript/cua-cli/src/commands/sandbox.ts
+++ b/libs/typescript/cua-cli/src/commands/sandbox.ts
@@ -1,6 +1,6 @@
import type { Argv } from 'yargs';
import { ensureApiKeyInteractive } from '../auth';
-import { http } from '../http';
+import { http, CUA_VERSION_HEADERS } from '../http';
import { clearApiKey } from '../storage';
import type { SandboxItem } from '../util';
import { openInBrowser, printSandboxList } from '../util';
@@ -69,6 +69,7 @@ async function fetchSandboxDetails(
try {
const statusRes = await fetch(statusUrl, {
+ headers: { ...CUA_VERSION_HEADERS },
signal: statusController.signal,
});
clearTimeout(statusTimeout);
@@ -98,6 +99,7 @@ async function fetchSandboxDetails(
'Content-Type': 'application/json',
'X-Container-Name': sandbox.name,
'X-API-Key': token,
+ ...CUA_VERSION_HEADERS,
},
body: JSON.stringify({
command: 'version',
diff --git a/libs/typescript/cua-cli/src/http.ts b/libs/typescript/cua-cli/src/http.ts
index 4f5f3ac23e..45221c3af5 100644
--- a/libs/typescript/cua-cli/src/http.ts
+++ b/libs/typescript/cua-cli/src/http.ts
@@ -1,5 +1,13 @@
import { API_BASE } from './config';
+const { version: cliVersion } = (await Bun.file(
+ new URL('../../package.json', import.meta.url)
+).json()) as { version: string };
+
+export const CUA_VERSION_HEADERS: Record = {
+ 'X-Cua-Client-Version': `cli:${cliVersion}`,
+};
+
export async function http(
path: string,
opts: { method?: string; token: string; body?: any }
@@ -7,6 +15,7 @@ export async function http(
const url = `${API_BASE}${path}`;
const headers: Record = {
Authorization: `Bearer ${opts.token}`,
+ ...CUA_VERSION_HEADERS,
};
if (opts.body) headers['content-type'] = 'application/json';
return fetch(url, {
diff --git a/libs/typescript/playground/src/adapters/cloud.ts b/libs/typescript/playground/src/adapters/cloud.ts
index ce5d9387b1..c48c765b77 100644
--- a/libs/typescript/playground/src/adapters/cloud.ts
+++ b/libs/typescript/playground/src/adapters/cloud.ts
@@ -12,6 +12,10 @@ import type {
} from './types';
import type { AgentMessage, Chat, ModelProvider } from '../types';
+const CUA_VERSION_HEADERS: Record = {
+ 'X-Cua-Client-Version': `playground:${__CUA_VERSION__}`,
+};
+
// =============================================================================
// API Error Helper
// =============================================================================
@@ -42,6 +46,7 @@ class CloudPersistenceAdapter implements PersistenceAdapter {
headers: {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
+ ...CUA_VERSION_HEADERS,
...options?.headers,
},
});
@@ -137,6 +142,7 @@ class CloudComputerAdapter implements ComputerAdapter {
headers: {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
+ ...CUA_VERSION_HEADERS,
...options?.headers,
},
});
@@ -217,7 +223,7 @@ class CloudInferenceAdapter implements InferenceAdapter {
async getAvailableModels(): Promise {
try {
const response = await fetch(`${this.baseUrl}/v1/models`, {
- headers: { Authorization: `Bearer ${this.apiKey}` },
+ headers: { Authorization: `Bearer ${this.apiKey}`, ...CUA_VERSION_HEADERS },
});
if (!response.ok) {
diff --git a/libs/typescript/playground/src/globals.d.ts b/libs/typescript/playground/src/globals.d.ts
new file mode 100644
index 0000000000..f78c5ad001
--- /dev/null
+++ b/libs/typescript/playground/src/globals.d.ts
@@ -0,0 +1,2 @@
+/** Build-time constant injected by tsdown `define`. */
+declare const __CUA_VERSION__: string;
diff --git a/libs/typescript/playground/src/hooks/useAgentRequest.ts b/libs/typescript/playground/src/hooks/useAgentRequest.ts
index 83ff00899e..1787393d8f 100644
--- a/libs/typescript/playground/src/hooks/useAgentRequest.ts
+++ b/libs/typescript/playground/src/hooks/useAgentRequest.ts
@@ -7,6 +7,10 @@ import { usePlaygroundTelemetry } from '../telemetry';
import type { AgentMessage, UserMessage } from '../types';
import { isVM, isCustomComputer } from '../types';
+const CUA_VERSION_HEADERS: Record = {
+ 'X-Cua-Client-Version': `playground:${__CUA_VERSION__}`,
+};
+
// Agent client interface for making requests
interface AgentClientOptions {
timeout?: number;
@@ -36,7 +40,7 @@ class AgentClient {
// Try /cmd endpoint (cloud sandboxes use this for health checks)
await fetch(`${this.baseUrl}/cmd`, {
method: 'POST',
- headers: { 'Content-Type': 'application/json' },
+ headers: { 'Content-Type': 'application/json', ...CUA_VERSION_HEADERS },
body: JSON.stringify({ command: 'version', params: {} }),
signal: this.options.signal || AbortSignal.timeout(5000),
});
@@ -66,6 +70,7 @@ class AgentClient {
const headers: Record = {
'Content-Type': 'application/json',
+ ...CUA_VERSION_HEADERS,
};
if (this.options.apiKey) {
headers['X-API-Key'] = this.options.apiKey;
diff --git a/libs/typescript/playground/src/styles.css b/libs/typescript/playground/src/styles.css
index 86b6cbb91b..1ed4444e23 100644
--- a/libs/typescript/playground/src/styles.css
+++ b/libs/typescript/playground/src/styles.css
@@ -1,4 +1,4 @@
-@import "tw-animate-css";
+@import 'tw-animate-css';
/* Standalone dark mode - works with both class and media query */
@custom-variant dark (&:where(.dark, .dark *));
diff --git a/libs/typescript/playground/tsdown.config.ts b/libs/typescript/playground/tsdown.config.ts
index f2a1bb671e..03a049b523 100644
--- a/libs/typescript/playground/tsdown.config.ts
+++ b/libs/typescript/playground/tsdown.config.ts
@@ -1,11 +1,17 @@
+import { readFileSync } from 'node:fs';
import { defineConfig } from 'tsdown';
+const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf-8'));
+
export default defineConfig([
{
entry: ['./src/index.ts'],
platform: 'browser',
dts: true,
copy: ['./src/styles.css'],
+ define: {
+ __CUA_VERSION__: JSON.stringify(pkg.version),
+ },
external: [
// React - MUST be external to avoid duplicate React instances
'react',
diff --git a/libs/typescript/pnpm-lock.yaml b/libs/typescript/pnpm-lock.yaml
index 2b08b26925..8e46d3e7f0 100644
--- a/libs/typescript/pnpm-lock.yaml
+++ b/libs/typescript/pnpm-lock.yaml
@@ -15,8 +15,8 @@ importers:
agent:
dependencies:
'@trycua/core':
- specifier: ^0.1.2
- version: 0.1.4
+ specifier: workspace:^
+ version: link:../core
peerjs:
specifier: ^1.5.4
version: 1.5.5
@@ -46,8 +46,8 @@ importers:
computer:
dependencies:
'@trycua/core':
- specifier: ^0.1.2
- version: 0.1.4
+ specifier: workspace:^
+ version: link:../core
pino:
specifier: ^9.7.0
version: 9.14.0
@@ -1050,9 +1050,6 @@ packages:
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
- '@trycua/core@0.1.4':
- resolution: {integrity: sha512-ZRO0VmzHpvkUkGKTk8AZTlHqWqOSlURcW/ckqraPOtGZev0Pe1mCiPEhY2c/xZ4zye6y7Ut7fqGTwRuKOJ3Q7A==}
-
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@@ -2813,13 +2810,6 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
- '@trycua/core@0.1.4':
- dependencies:
- '@types/uuid': 10.0.0
- pino: 9.14.0
- posthog-node: 5.24.10
- uuid: 11.1.0
-
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
diff --git a/pyproject.toml b/pyproject.toml
index 387ee0419a..12ac017d84 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -50,6 +50,7 @@ docs-scripts = [
"markitdown>=0.0.1",
"modal>=0.63.0",
"gitpython>=3.1.43",
+ "griffe>=2.0.0",
]
test = [
"aioresponses>=0.7.4",
diff --git a/uv.lock b/uv.lock
index 151e34d1ae..1abfbee7de 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1039,7 +1039,7 @@ wheels = [
[[package]]
name = "cua-agent"
-version = "0.7.24"
+version = "0.7.27"
source = { editable = "libs/python/agent" }
dependencies = [
{ name = "aiohttp" },
@@ -1208,7 +1208,7 @@ requires-dist = [
[[package]]
name = "cua-computer"
-version = "0.5.12"
+version = "0.5.14"
source = { editable = "libs/python/computer" }
dependencies = [
{ name = "aiohttp" },
@@ -1252,7 +1252,7 @@ provides-extras = ["lume", "lumier", "ui", "all"]
[[package]]
name = "cua-computer-server"
-version = "0.3.16"
+version = "0.3.18"
source = { editable = "libs/python/computer-server" }
dependencies = [
{ name = "aiohttp" },
@@ -1318,7 +1318,7 @@ provides-extras = ["macos", "linux", "windows"]
[[package]]
name = "cua-core"
-version = "0.1.16"
+version = "0.1.17"
source = { editable = "libs/python/core" }
dependencies = [
{ name = "posthog" },
@@ -1446,6 +1446,7 @@ docs-scripts = [
{ name = "crawl4ai" },
{ name = "fastmcp" },
{ name = "gitpython" },
+ { name = "griffe" },
{ name = "lancedb" },
{ name = "markdown-it-py" },
{ name = "markitdown" },
@@ -1498,6 +1499,7 @@ docs-scripts = [
{ name = "crawl4ai", specifier = ">=0.4.0" },
{ name = "fastmcp", specifier = ">=2.14.0" },
{ name = "gitpython", specifier = ">=3.1.43" },
+ { name = "griffe", specifier = ">=2.0.0" },
{ name = "lancedb", specifier = ">=0.4.0" },
{ name = "markdown-it-py", specifier = ">=3.0.0" },
{ name = "markitdown", specifier = ">=0.0.1" },
@@ -2227,6 +2229,38 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" },
]
+[[package]]
+name = "griffe"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "griffecli" },
+ { name = "griffelib" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/94/ee21d41e7eb4f823b94603b9d40f86d3c7fde80eacc2c3c71845476dddaa/griffe-2.0.0-py3-none-any.whl", hash = "sha256:5418081135a391c3e6e757a7f3f156f1a1a746cc7b4023868ff7d5e2f9a980aa", size = 5214, upload-time = "2026-02-09T19:09:44.105Z" },
+]
+
+[[package]]
+name = "griffecli"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama" },
+ { name = "griffelib" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/ed/d93f7a447bbf7a935d8868e9617cbe1cadf9ee9ee6bd275d3040fbf93d60/griffecli-2.0.0-py3-none-any.whl", hash = "sha256:9f7cd9ee9b21d55e91689358978d2385ae65c22f307a63fb3269acf3f21e643d", size = 9345, upload-time = "2026-02-09T19:09:42.554Z" },
+]
+
+[[package]]
+name = "griffelib"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" },
+]
+
[[package]]
name = "groovy"
version = "0.1.2"