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
13 changes: 9 additions & 4 deletions .github/workflows/cd-rust-cua-driver.yml
Original file line number Diff line number Diff line change
Expand Up @@ -461,14 +461,19 @@ jobs:
# OS so we publish one asset per release tag. Naming matches the
# versioned binary tarballs.
VERSION="${{ steps.version.outputs.version }}"
# Asset name keeps the `cua-driver-rs-v*` prefix for backward
# compat with the URL skills.rs constructs. Internal directory
# is `cua-driver/` to match the renamed SKILL_PACK_NAME — both
# current binaries and old ones extract correctly because the
# tarball-extractor strips whatever top-level dir it finds.
SKILLS_STAGE="cua-driver-rs-v${VERSION}-skills"
mkdir -p "${SKILLS_STAGE}/cua-driver-rs"
if [ -d libs/cua-driver/rust/Skills/cua-driver-rs ]; then
cp -R libs/cua-driver/rust/Skills/cua-driver-rs/* "${SKILLS_STAGE}/cua-driver-rs/"
mkdir -p "${SKILLS_STAGE}/cua-driver"
if [ -d libs/cua-driver/rust/Skills/cua-driver ]; then
cp -R libs/cua-driver/rust/Skills/cua-driver/* "${SKILLS_STAGE}/cua-driver/"
tar -czf "release-upload/${SKILLS_STAGE}.tar.gz" "${SKILLS_STAGE}"
echo "Packaged skill pack: release-upload/${SKILLS_STAGE}.tar.gz"
else
echo "Note: libs/cua-driver/rust/Skills/cua-driver-rs not present; skipping skill-pack asset."
echo "Note: libs/cua-driver/rust/Skills/cua-driver not present; skipping skill-pack asset."
fi

echo "Release files:"
Expand Down
15 changes: 8 additions & 7 deletions libs/cua-driver/rust/crates/cua-driver/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,14 @@ pub enum Command {
Autostart { subcommand: String },
/// `cua-driver skills {install|update|uninstall|status|path}` —
/// agent skill-pack management. The verb is the ONLY way a user
/// installs or updates the cua-driver-rs skill pack into their
/// agent dirs (Claude Code / Codex / OpenClaw / OpenCode); the
/// install scripts never touch ~/.claude/skills/ etc. directly.
/// `install` fetches the matching versioned release asset
/// (`cua-driver-rs-v<v>-skills.tar.gz`) from GitHub, places it
/// under `<HomeDir>/skills/cua-driver-rs/`, and symlinks into each
/// detected agent's `skills/` dir. See
/// installs or updates the cua-driver skill pack into their agent
/// dirs (Claude Code / Codex / OpenClaw / OpenCode); the install
/// scripts never touch ~/.claude/skills/ etc. directly. `install`
/// fetches the matching versioned release asset
/// (`cua-driver-rs-v<v>-skills.tar.gz` — the asset filename keeps
/// the legacy `-rs` for backward-compat with pinned URLs) from
/// GitHub, places it under `<HomeDir>/skills/cua-driver/`, and
/// symlinks into each detected agent's `skills/` dir. See
/// `crates/cua-driver/src/skills.rs`.
Skills { subcommand: String, flags: Vec<String> },
}
Expand Down
125 changes: 98 additions & 27 deletions libs/cua-driver/rust/crates/cua-driver/src/skills.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
//! - `update` — same as `install --force`: re-fetch even if local copy
//! already exists, refreshes content.
//! - `uninstall [--all]` — remove the agent symlinks. With `--all`, also
//! delete the local copy under `<HomeDir>/skills/cua-driver-rs/`.
//! delete the local copy under `<HomeDir>/skills/cua-driver/` (and the
//! pre-rename `cua-driver-rs/` location if present).
//! - `status` — print local install state + per-agent link state.
//! - `path` — print `<HomeDir>/skills/cua-driver-rs` (the local copy).
//! - `path` — print `<HomeDir>/skills/cua-driver` (the local copy).
//!
//! ## Fetch source
//!
Expand All @@ -28,7 +29,7 @@
//!
//! `--from <tag>` lets the user pin a different release tag.
//! `--from main` fetches the latest from the `main` branch via the
//! `Skills/cua-driver-rs/` directory (one HTTP call per file — used
//! `Skills/cua-driver/` directory (one HTTP call per file — used
//! for bleeding-edge dev validation; not the default).
//!
//! ## Agent detection
Expand All @@ -43,14 +44,20 @@
//!
//! Only acts on a given agent when its parent skills dir already
//! exists (i.e. the agent itself is installed). Never clobbers an
//! existing `<agent_skills>/cua-driver-rs` link — preserves dev users'
//! existing `<agent_skills>/cua-driver` link — preserves dev users'
//! hand-rolled symlinks.

use anyhow::{anyhow, bail, Context, Result};
use std::fs;
use std::path::{Path, PathBuf};

const SKILL_PACK_NAME: &str = "cua-driver-rs";
const SKILL_PACK_NAME: &str = "cua-driver";
/// Pre-rename name. The skill pack used to install as `cua-driver-rs`
/// (when the Rust port lived at `libs/cua-driver-rs/`). On install /
/// uninstall we sweep this name out of every agent skills dir and the
/// local stage so a user who had the old skill installed ends up with
/// exactly one pack, named consistently with the rest of the binary.
const LEGACY_SKILL_PACK_NAME: &str = "cua-driver-rs";
const SKILL_FILES: &[&str] = &[
"README.md",
"SKILL.md",
Expand All @@ -61,7 +68,7 @@ const SKILL_FILES: &[&str] = &[
"TESTS.md",
];

/// Local install path for the skill pack: `<HomeDir>/skills/cua-driver-rs`.
/// Local install path for the skill pack: `<HomeDir>/skills/cua-driver`.
fn local_skill_dir() -> Result<PathBuf> {
let home = home_dir()?;
Ok(home.join("skills").join(SKILL_PACK_NAME))
Expand Down Expand Up @@ -172,6 +179,12 @@ fn install(flags: &[String], force: bool) -> Result<()> {
&& flags.iter().zip(flags.iter().skip(1)).any(|(a, b)| a == "--from" && b == "main"));
let force = force || flags.iter().any(|f| f == "--force");

// Sweep the legacy `cua-driver-rs`-named pack out FIRST so the
// post-install state has exactly one skill pack at the new name.
// Done before fetch so a fresh install on a previously-installed
// machine doesn't leave orphan links pointing at a stale local dir.
sweep_legacy_skill_pack();

let local = local_skill_dir()?;
let already_present = local.join("SKILL.md").exists();

Expand All @@ -197,6 +210,50 @@ fn install(flags: &[String], force: bool) -> Result<()> {
Ok(())
}

/// Best-effort removal of any pre-rename skill pack — the local stage at
/// `<HomeDir>/skills/cua-driver-rs/` and every `<agent_skills>/cua-driver-rs`
/// symlink/junction. Runs at the start of `install` / `update` so a user
/// who had the legacy name installed gets cleanly migrated without
/// having to run `skills uninstall` first.
///
/// Silent on failure — this is a UX nicety, not a correctness boundary.
/// The new pack still installs even if a stale junction can't be cleaned.
fn sweep_legacy_skill_pack() {
// Local stage at <HomeDir>/skills/cua-driver-rs.
if let Ok(home) = home_dir() {
let legacy_local = home.join("skills").join(LEGACY_SKILL_PACK_NAME);
if legacy_local.exists() {
if let Err(e) = fs::remove_dir_all(&legacy_local) {
eprintln!(" warning: could not remove legacy local pack at {}: {e}",
legacy_local.display());
} else {
println!(" cleaned up legacy local pack at {}", legacy_local.display());
}
}
}
// Agent links named `<parent>/cua-driver-rs`.
for agent in AGENTS {
let parent = match agent.parent_path() {
Ok(p) => p,
Err(_) => continue,
};
let legacy_link = parent.join(LEGACY_SKILL_PACK_NAME);
if !parent.exists() || !legacy_link.symlink_metadata().is_ok() {
continue;
}
if !is_symlink_or_junction(&legacy_link) {
// Real directory — don't clobber user-managed content.
continue;
}
if let Err(e) = remove_link(&legacy_link) {
eprintln!(" warning: could not remove legacy {} link at {}: {e}",
agent.label, legacy_link.display());
} else {
println!(" cleaned up legacy {} link at {}", agent.label, legacy_link.display());
}
}
}

/// Returns `Ok(true)` when a new link was created, `Ok(false)` when
/// skipped (parent dir missing, link already there, etc.).
fn link_agent(agent: Agent, local_skill_dir: &Path) -> Result<bool> {
Expand Down Expand Up @@ -253,7 +310,7 @@ fn fetch_into(dest: &Path, from_main: bool) -> Result<()> {

if from_main {
// Per-file raw GitHub fetch — used for bleeding-edge dev validation.
let base = "https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/rust/Skills/cua-driver-rs";
let base = "https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/rust/Skills/cua-driver";
for f in SKILL_FILES {
let url = format!("{base}/{f}");
let body = http_get_text(&url)
Expand Down Expand Up @@ -298,9 +355,11 @@ fn http_get_bytes(url: &str) -> Result<Vec<u8>> {
fn extract_tar_gz(bytes: &[u8], dest: &Path) -> Result<()> {
let gz = flate2::read::GzDecoder::new(bytes);
let mut archive = tar::Archive::new(gz);
// The tarball contains a `cua-driver-rs/` top-level dir matching the
// The tarball contains a `cua-driver/` top-level dir matching the
// pack name. Strip it during extraction so the .md files land
// directly in `dest` (which is itself `<HomeDir>/skills/cua-driver-rs`).
// directly in `dest` (which is itself `<HomeDir>/skills/cua-driver`).
// Pre-rename tarballs had `cua-driver-rs/` — the stripping logic
// below is name-agnostic so both shapes extract identically.
for entry in archive.entries()? {
let mut entry = entry?;
let path = entry.path()?.into_owned();
Expand Down Expand Up @@ -328,28 +387,40 @@ use std::io::Read;
fn uninstall(flags: &[String]) -> Result<()> {
let remove_local = flags.iter().any(|f| f == "--all");
let mut removed_any = false;
for agent in AGENTS {
let link = match agent.link_path() {
Ok(p) => p,
Err(_) => continue,
};
if link.symlink_metadata().is_ok() {
// Only remove if it's a symlink/junction we manage. If a
// user replaced it with a real dir, leave it alone.
if is_symlink_or_junction(&link) {
remove_link(&link)?;
println!(" ✅ removed {} link at {}", agent.label, link.display());
removed_any = true;
} else {
println!(" {} link at {} is not a symlink/junction; leaving alone", agent.label, link.display());
// Try BOTH the current name and the legacy `cua-driver-rs` name so a
// user who installed under the old name and then `skills uninstall`s
// ends up clean. Same symlink/junction safety check applies to each.
for name in [SKILL_PACK_NAME, LEGACY_SKILL_PACK_NAME] {
for agent in AGENTS {
let parent = match agent.parent_path() {
Ok(p) => p,
Err(_) => continue,
};
let link = parent.join(name);
if link.symlink_metadata().is_ok() {
// Only remove if it's a symlink/junction we manage. If a
// user replaced it with a real dir, leave it alone.
if is_symlink_or_junction(&link) {
remove_link(&link)?;
println!(" ✅ removed {} link at {}", agent.label, link.display());
removed_any = true;
} else {
println!(" {} link at {} is not a symlink/junction; leaving alone", agent.label, link.display());
}
}
}
}
if remove_local {
let local = local_skill_dir()?;
if local.exists() {
fs::remove_dir_all(&local)?;
println!(" ✅ removed local skill pack at {}", local.display());
// Local stage at the current name + any legacy stage from before
// the rename. Both are owned by the installer; safe to delete.
if let Ok(home) = home_dir() {
for name in [SKILL_PACK_NAME, LEGACY_SKILL_PACK_NAME] {
let local = home.join("skills").join(name);
if local.exists() {
fs::remove_dir_all(&local)?;
println!(" ✅ removed local skill pack at {}", local.display());
}
}
}
}
if !removed_any {
Expand Down
8 changes: 4 additions & 4 deletions libs/cua-driver/rust/crates/mcp-server/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ pub fn initialize_result() -> Value {
serde_json::json!({
"protocolVersion": "2025-06-18",
"capabilities": { "tools": {} },
"serverInfo": { "name": "cua-driver-rs", "version": env!("CARGO_PKG_VERSION") },
"serverInfo": { "name": "cua-driver", "version": env!("CARGO_PKG_VERSION") },
"instructions": agent_instructions()
})
}
Expand All @@ -160,7 +160,7 @@ pub fn initialize_result() -> Value {
/// connecting client. The spec frames this as a "hint... MAY be added
/// to the system prompt" — eager, every-turn cost. We keep it under
/// the community-recommended ~200-word ceiling and host the long-form
/// workflow in `Skills/cua-driver-rs/SKILL.md`.
/// workflow in `Skills/cua-driver/SKILL.md`.
///
/// Templated per-host: the accessibility-tree provider name (AX on
/// macOS, UIA on Windows, AT-SPI on Linux) is injected so a connecting
Expand All @@ -186,7 +186,7 @@ fn agent_instructions() -> String {
};

format!(
r#"cua-driver-rs: cross-platform background computer-use automation.
r#"cua-driver: cross-platform background computer-use automation.

Tools let you interact with any app without stealing keyboard focus or moving the visible cursor. Prefer element_index ({tree_kind}) paths over pixel coordinates — they work on backgrounded/hidden windows.

Expand All @@ -199,6 +199,6 @@ Workflow per turn:

Agent cursor: set_agent_cursor_* tools visualise where the agent is acting without affecting the real mouse pointer.

If a `cua-driver-rs` skill is loaded in your harness (Claude Code / Codex / OpenClaw / OpenCode dirs), prefer its detailed workflow — SKILL.md plus {platform_skill_pointer}. Install with `cua-driver skills install` if not yet present."#
If a `cua-driver` skill is loaded in your harness (Claude Code / Codex / OpenClaw / OpenCode dirs), prefer its detailed workflow — SKILL.md plus {platform_skill_pointer}. Install with `cua-driver skills install` if not yet present."#
)
}
8 changes: 7 additions & 1 deletion libs/cua-driver/scripts/uninstall.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,14 @@ $CurrentDir = Join-Path $PackagesDir "current"

# Skill junctions — mirrors the AGENTS list in
# libs/cua-driver/rust/crates/cua-driver/src/skills.rs (the verb that
# creates them) so we remove from the same paths.
# creates them) so we remove from the same paths. Both the current
# `cua-driver` name and the pre-rename `cua-driver-rs` name are swept
# so a user who installed before the rename ends up clean.
$SkillJunctions = @(
(Join-Path $env:USERPROFILE ".claude\skills\cua-driver"),
(Join-Path $env:USERPROFILE ".agents\skills\cua-driver"),
(Join-Path $env:USERPROFILE ".openclaw\skills\cua-driver"),
(Join-Path $env:APPDATA "opencode\skills\cua-driver"),
(Join-Path $env:USERPROFILE ".claude\skills\cua-driver-rs"),
(Join-Path $env:USERPROFILE ".agents\skills\cua-driver-rs"),
(Join-Path $env:USERPROFILE ".openclaw\skills\cua-driver-rs"),
Expand Down
12 changes: 10 additions & 2 deletions libs/cua-driver/scripts/uninstall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,11 @@ if [[ "$USE_RUST_BACKEND" == "1" ]]; then
HOME_DIR="${CUA_DRIVER_RS_HOME:-$HOME/.cua-driver-rs}"
LAUNCHAGENT_PLIST="$HOME/Library/LaunchAgents/com.trycua.cua-driver-rs.plist"
SYSTEMD_USER_UNIT="$HOME/.config/systemd/user/cua-driver-rs.service"
SKILL_PACK_NAME="cua-driver-rs"
SKILL_PACK_NAME="cua-driver"
# Pre-rename skill pack name — swept alongside the current one so
# users who installed under the legacy name end up clean after
# `uninstall.sh --backend=rust`.
LEGACY_SKILL_PACK_NAME="cua-driver-rs"

# Rust-install marker. The post-rename Rust bundle path
# `/Applications/CuaDriver.app` is shared with the Swift driver
Expand Down Expand Up @@ -296,7 +300,11 @@ if [[ "$USE_RUST_BACKEND" == "1" ]]; then
"$HOME/.claude/skills/$SKILL_PACK_NAME" \
"$HOME/.agents/skills/$SKILL_PACK_NAME" \
"$HOME/.openclaw/skills/$SKILL_PACK_NAME" \
"$HOME/.config/opencode/skills/$SKILL_PACK_NAME"; do
"$HOME/.config/opencode/skills/$SKILL_PACK_NAME" \
"$HOME/.claude/skills/$LEGACY_SKILL_PACK_NAME" \
"$HOME/.agents/skills/$LEGACY_SKILL_PACK_NAME" \
"$HOME/.openclaw/skills/$LEGACY_SKILL_PACK_NAME" \
"$HOME/.config/opencode/skills/$LEGACY_SKILL_PACK_NAME"; do
if [[ -L "$SKILL_LINK" ]]; then
rm -f "$SKILL_LINK"
log "removed skill symlink $SKILL_LINK"
Expand Down
Loading