diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e95b27a..1c2e7f4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,15 +1,31 @@
name: CI
-# Runners: uvacompute (https://uvacompute.com)
-# To enable, set the UVA_RUNNER repo variable to the correct runner label.
-# runs-on: ${{ vars.UVA_RUNNER || 'ubuntu-latest' }}
-
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
+ inputs:
+ bump:
+ description: Version bump type (only for workflow_dispatch)
+ type: choice
+ options:
+ - patch
+ - minor
+ - major
+ default: patch
+ publish_npm:
+ description: Publish to npm
+ type: boolean
+ default: true
+ publish_crates:
+ description: Publish to crates.io
+ type: boolean
+ default: true
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: write
@@ -17,7 +33,7 @@ permissions:
jobs:
changes:
name: Changes
- runs-on: ubuntu-latest
+ runs-on: [self-hosted, netty]
outputs:
rust: ${{ steps.check.outputs.rust }}
version: ${{ steps.version.outputs.version }}
@@ -56,34 +72,36 @@ jobs:
id: version
if: github.event_name != 'pull_request' && steps.check.outputs.rust == 'true'
run: |
- BASE=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
- IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE"
+ CURRENT=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
- LATEST=$(git tag -l "v${MAJOR}.${MINOR}.*" | sort -V | tail -1)
-
- if [ -z "$LATEST" ]; then
- NEW="$BASE"
- else
- LATEST_VER="${LATEST#v}"
- IFS='.' read -r _ _ LATEST_PATCH <<< "$LATEST_VER"
- NEW_PATCH=$((LATEST_PATCH + 1))
- NEW="${MAJOR}.${MINOR}.${NEW_PATCH}"
- fi
-
- # Ensure the computed version does not already have a tag
- while git rev-parse "v${NEW}" >/dev/null 2>&1; do
- IFS='.' read -r MAJOR MINOR PATCH <<< "$NEW"
- NEW="${MAJOR}.${MINOR}.$((PATCH + 1))"
- done
+ BUMP="${{ inputs.bump || 'patch' }}"
+ case "$BUMP" in
+ major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
+ minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
+ patch)
+ LATEST=$(git tag -l "v${MAJOR}.${MINOR}.*" | sort -V | tail -1)
+ if [ -z "$LATEST" ]; then
+ NEW_PATCH=$PATCH
+ else
+ LATEST_VER="${LATEST#v}"
+ IFS='.' read -r _ _ LATEST_PATCH <<< "$LATEST_VER"
+ NEW_PATCH=$((LATEST_PATCH + 1))
+ fi
+ PATCH=$NEW_PATCH
+ ;;
+ esac
+ NEW="${MAJOR}.${MINOR}.${PATCH}"
echo "version=${NEW}" >> "$GITHUB_OUTPUT"
echo "tag=v${NEW}" >> "$GITHUB_OUTPUT"
+ echo "Computed version: ${NEW} (v${NEW})"
validate:
name: Validate
needs: changes
if: needs.changes.outputs.rust == 'true'
- runs-on: ubuntu-latest
+ runs-on: [self-hosted, netty]
steps:
- uses: actions/checkout@v4
@@ -107,9 +125,6 @@ jobs:
- name: Install site dependencies
run: pnpm --dir site install --frozen-lockfile
- - name: Install system dependencies
- run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev
-
- name: Format check
run: make fmt-check
@@ -126,7 +141,7 @@ jobs:
name: Integration (Xvfb)
needs: changes
if: needs.changes.outputs.rust == 'true'
- runs-on: ubuntu-latest
+ runs-on: [self-hosted, netty]
steps:
- uses: actions/checkout@v4
@@ -134,9 +149,6 @@ jobs:
- uses: Swatinem/rust-cache@v2
- - name: Install system dependencies
- run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev xvfb
-
- name: Xvfb integration tests
run: make test-integration
@@ -144,7 +156,7 @@ jobs:
name: Distribution Validate
needs: changes
if: needs.changes.outputs.rust == 'true'
- runs-on: ubuntu-latest
+ runs-on: [self-hosted, netty]
steps:
- uses: actions/checkout@v4
@@ -156,21 +168,16 @@ jobs:
with:
node-version: 22
- - uses: cachix/install-nix-action@v30
- with:
- extra_nix_config: |
- experimental-features = nix-command flakes
-
- - name: Install system dependencies
- run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev
-
- name: Distribution validation
run: make dist-validate
+ # --- Release pipeline: update-manifests -> build -> release -> publish ---
+ # These stay on ubuntu-latest for artifact upload/download and registry publishing.
+
update-manifests:
name: Update Manifests
needs: [changes, validate, integration, distribution]
- if: github.event_name != 'pull_request'
+ if: github.event_name != 'pull_request' && needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -183,29 +190,31 @@ jobs:
with:
node-version: 22
- - name: Update version in Cargo.toml
+ - name: Update versions
run: |
CURRENT=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
NEW="${{ needs.changes.outputs.version }}"
if [ "$CURRENT" != "$NEW" ]; then
sed -i "0,/^version = \"${CURRENT}\"/s//version = \"${NEW}\"/" Cargo.toml
- node -e 'const fs=require("node:fs"); const path="npm/deskctl-cli/package.json"; const pkg=JSON.parse(fs.readFileSync(path,"utf8")); pkg.version=process.argv[1]; fs.writeFileSync(path, JSON.stringify(pkg, null, 2)+"\n");' "$NEW"
cargo generate-lockfile
fi
+ node -e '
+ const fs = require("node:fs");
+ const p = "npm/deskctl/package.json";
+ const pkg = JSON.parse(fs.readFileSync(p, "utf8"));
+ pkg.version = process.argv[1];
+ fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + "\n");
+ ' "$NEW"
- name: Commit, tag, and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
-
- if ! git diff --quiet; then
- git add Cargo.toml Cargo.lock npm/deskctl-cli/package.json
+ git add Cargo.toml Cargo.lock npm/deskctl/package.json
+ if ! git diff --cached --quiet; then
git commit -m "release: ${{ needs.changes.outputs.tag }} [skip ci]"
fi
-
- if ! git rev-parse "${{ needs.changes.outputs.tag }}" >/dev/null 2>&1; then
- git tag "${{ needs.changes.outputs.tag }}"
- fi
+ git tag "${{ needs.changes.outputs.tag }}"
git push origin main --tags
build:
@@ -217,7 +226,6 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ needs.changes.outputs.tag }}
- fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
with:
@@ -228,6 +236,16 @@ jobs:
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev
+ - name: Verify version
+ run: |
+ CARGO_VER=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
+ EXPECTED="${{ needs.changes.outputs.version }}"
+ if [ "$CARGO_VER" != "$EXPECTED" ]; then
+ echo "Version mismatch: Cargo.toml=$CARGO_VER expected=$EXPECTED"
+ exit 1
+ fi
+ echo "Building version $CARGO_VER"
+
- name: Clippy
run: cargo clippy -- -D warnings
@@ -243,7 +261,7 @@ jobs:
release:
name: Release
needs: [changes, build, update-manifests]
- if: github.event_name != 'pull_request'
+ if: github.event_name != 'pull_request' && needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -272,3 +290,75 @@ jobs:
artifacts/deskctl-linux-x86_64 \
artifacts/checksums.txt
fi
+
+ publish-npm:
+ name: Publish npm
+ needs: [changes, update-manifests, release]
+ if: >-
+ github.event_name != 'pull_request'
+ && needs.changes.outputs.rust == 'true'
+ && (inputs.publish_npm == true || inputs.publish_npm == '')
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ needs.changes.outputs.tag }}
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ registry-url: https://registry.npmjs.org
+
+ - name: Check if already published
+ id: published
+ run: |
+ VERSION="${{ needs.changes.outputs.version }}"
+ if npm view "deskctl@${VERSION}" version >/dev/null 2>&1; then
+ echo "npm=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "npm=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Validate npm package
+ if: steps.published.outputs.npm != 'true'
+ run: node npm/deskctl/scripts/validate-package.js
+
+ - name: Publish npm
+ if: steps.published.outputs.npm != 'true'
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ run: npm publish ./npm/deskctl --access public
+
+ publish-crates:
+ name: Publish crates.io
+ needs: [changes, update-manifests, release]
+ if: >-
+ github.event_name != 'pull_request'
+ && needs.changes.outputs.rust == 'true'
+ && (inputs.publish_crates == true || inputs.publish_crates == '')
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ needs.changes.outputs.tag }}
+
+ - uses: dtolnay/rust-toolchain@stable
+
+ - name: Install system dependencies
+ run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev
+
+ - name: Check if already published
+ id: published
+ run: |
+ VERSION="${{ needs.changes.outputs.version }}"
+ if curl -fsSL "https://crates.io/api/v1/crates/deskctl/${VERSION}" >/dev/null 2>&1; then
+ echo "crates=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "crates=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Publish crates.io
+ if: steps.published.outputs.crates != 'true'
+ env:
+ CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+ run: cargo publish --locked
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
deleted file mode 100644
index 329f151..0000000
--- a/.github/workflows/publish.yml
+++ /dev/null
@@ -1,102 +0,0 @@
-name: Publish Registries
-
-on:
- workflow_dispatch:
- inputs:
- tag:
- description: Release tag to publish (for example v0.1.5)
- required: true
- type: string
- publish_npm:
- description: Publish deskctl-cli to npm
- required: true
- type: boolean
- default: false
- publish_crates:
- description: Publish deskctl to crates.io
- required: true
- type: boolean
- default: false
-
-permissions:
- contents: read
-
-jobs:
- publish:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- with:
- ref: ${{ inputs.tag }}
-
- - uses: dtolnay/rust-toolchain@stable
-
- - uses: actions/setup-node@v4
- with:
- node-version: 22
-
- - name: Install system dependencies
- run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev
-
- - name: Verify release exists and contains canonical assets
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- gh release view "${{ inputs.tag }}" --json assets --jq '.assets[].name' > /tmp/release-assets.txt
- grep -Fx "deskctl-linux-x86_64" /tmp/release-assets.txt >/dev/null
- grep -Fx "checksums.txt" /tmp/release-assets.txt >/dev/null
-
- - name: Verify versions align with tag
- run: |
- TAG="${{ inputs.tag }}"
- VERSION="${TAG#v}"
- CARGO_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
- NPM_VERSION=$(node -p 'require("./npm/deskctl-cli/package.json").version')
-
- test "$VERSION" = "$CARGO_VERSION"
- test "$VERSION" = "$NPM_VERSION"
-
- - name: Check current published state
- id: published
- run: |
- VERSION="${{ inputs.tag }}"
- VERSION="${VERSION#v}"
-
- if npm view "deskctl-cli@${VERSION}" version >/dev/null 2>&1; then
- echo "npm=true" >> "$GITHUB_OUTPUT"
- else
- echo "npm=false" >> "$GITHUB_OUTPUT"
- fi
-
- if curl -fsSL "https://crates.io/api/v1/crates/deskctl/${VERSION}" >/dev/null 2>&1; then
- echo "crates=true" >> "$GITHUB_OUTPUT"
- else
- echo "crates=false" >> "$GITHUB_OUTPUT"
- fi
-
- - name: Validate npm package
- run: |
- mkdir -p ./tmp/npm-pack
- node npm/deskctl-cli/scripts/validate-package.js
- npm pack ./npm/deskctl-cli --pack-destination ./tmp/npm-pack >/dev/null
-
- - name: Validate crate publish path
- run: cargo publish --dry-run --locked
-
- - name: Publish npm
- if: inputs.publish_npm && steps.published.outputs.npm != 'true'
- env:
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: npm publish ./npm/deskctl-cli --access public
-
- - name: Publish crates.io
- if: inputs.publish_crates && steps.published.outputs.crates != 'true'
- env:
- CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- run: cargo publish --locked
-
- - name: Summary
- run: |
- echo "tag=${{ inputs.tag }}"
- echo "npm_already_published=${{ steps.published.outputs.npm }}"
- echo "crates_already_published=${{ steps.published.outputs.crates }}"
diff --git a/.gitignore b/.gitignore
index db552f7..40542a9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,5 +5,5 @@ secret/
.claude/
.codex/
openspec/
-npm/deskctl-cli/vendor/
-npm/deskctl-cli/*.tgz
+npm/deskctl/vendor/
+npm/deskctl/*.tgz
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bdbce4e..97e8c7c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -21,7 +21,7 @@ pnpm --dir site install
- `src/` holds production code and unit tests
- `tests/` holds integration tests
- `tests/support/` holds shared X11 and daemon helpers for integration coverage
-- `docs/runtime-output.md` is the stable-vs-best-effort runtime output contract for agent-facing CLI work
+- `docs/runtime-contract.md` is the stable-vs-best-effort runtime output contract for agent-facing CLI work
Keep integration-only helpers out of `src/`.
@@ -70,7 +70,7 @@ The hook config intentionally stays small:
Distribution support currently ships through:
- crate: `deskctl`
-- npm package: `deskctl-cli`
+- npm package: `deskctl`
- repo flake: `flake.nix`
- command name on every channel: `deskctl`
diff --git a/Cargo.lock b/Cargo.lock
index 71a9a54..eb0e2ce 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -241,9 +241,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cc"
-version = "1.2.57"
+version = "1.2.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
+checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -400,7 +400,7 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "deskctl"
-version = "0.1.6"
+version = "0.1.14"
dependencies = [
"ab_glyph",
"anyhow",
@@ -911,9 +911,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.91"
+version = "0.3.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
+checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -1039,9 +1039,9 @@ dependencies = [
[[package]]
name = "mio"
-version = "1.1.1"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [
"libc",
"wasi",
@@ -1699,9 +1699,9 @@ dependencies = [
[[package]]
name = "simd-adler32"
-version = "0.3.8"
+version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "simd_helpers"
@@ -1861,9 +1861,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
-version = "1.22.0"
+version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
+checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
dependencies = [
"getrandom 0.4.2",
"js-sys",
@@ -1907,9 +1907,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
-version = "0.2.114"
+version = "0.2.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
+checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a"
dependencies = [
"cfg-if",
"once_cell",
@@ -1920,9 +1920,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.114"
+version = "0.2.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
+checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1930,9 +1930,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.114"
+version = "0.2.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
+checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -1943,9 +1943,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.114"
+version = "0.2.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
+checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93"
dependencies = [
"unicode-ident",
]
@@ -2297,9 +2297,9 @@ dependencies = [
[[package]]
name = "zune-jpeg"
-version = "0.5.14"
+version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b7a1c0af6e5d8d1363f4994b7a091ccf963d8b694f7da5b0b9cceb82da2c0a6"
+checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
dependencies = [
"zune-core",
]
diff --git a/Cargo.toml b/Cargo.toml
index b05507b..be051c7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "deskctl"
-version = "0.1.6"
+version = "0.1.14"
edition = "2021"
description = "X11 desktop control CLI for agents"
license = "MIT"
diff --git a/Makefile b/Makefile
index 97857e3..7e1f852 100644
--- a/Makefile
+++ b/Makefile
@@ -38,10 +38,10 @@ npm-package-check:
echo "npm is required for npm packaging validation."; \
exit 1; \
fi
- node npm/deskctl-cli/scripts/validate-package.js
+ node npm/deskctl/scripts/validate-package.js
rm -rf tmp/npm-pack tmp/npm-install
mkdir -p tmp/npm-pack tmp/npm-install/bin
- npm pack ./npm/deskctl-cli --pack-destination ./tmp/npm-pack >/dev/null
+ npm pack ./npm/deskctl --pack-destination ./tmp/npm-pack >/dev/null
@if [ "$$(uname -s)" != "Linux" ]; then \
echo "Skipping npm package runtime smoke test on non-Linux host."; \
else \
diff --git a/README.md b/README.md
index 036396a..dccbe04 100644
--- a/README.md
+++ b/README.md
@@ -1,256 +1,46 @@
# deskctl
+[](https://www.npmjs.com/package/deskctl)
+[](skills/deskctl)
+
+Desktop control cli for AI agents on X11.
+
+https://github.com/user-attachments/assets/e820787e-4d1a-463f-bdcf-a829588778bf
-Desktop control CLI for AI agents on Linux X11.
## Install
-### Cargo
-
```bash
-cargo install deskctl
+npm install -g deskctl
```
-Source builds on Linux require:
-
-- Rust 1.75+
-- `pkg-config`
-- X11 development libraries for input and windowing, typically `libx11-dev` and `libxtst-dev` on Debian/Ubuntu
-
-### npm
-
```bash
-npm install -g deskctl-cli
-deskctl --help
+deskctl doctor
+deskctl snapshot --annotate
```
-One-shot execution is also supported:
+## Skill
```bash
-npx deskctl-cli --help
+npx skills add harivansh-afk/deskctl
```
-`deskctl-cli` currently supports `linux-x64` and installs the `deskctl` command by downloading the matching GitHub Release asset.
+## Docs
-### Nix
+- runtime contract: [docs/runtime-contract.md](docs/runtime-contract.md)
+- releasing: [docs/releasing.md](docs/releasing.md)
+- contributing: [CONTRIBUTING.md](CONTRIBUTING.md)
+
+## Install paths
+
+Nix:
```bash
nix run github:harivansh-afk/deskctl -- --help
nix profile install github:harivansh-afk/deskctl
```
-The repo flake is the supported Nix install surface in this phase.
-
-### Docker Convenience
-
-Build a Linux binary locally with Docker:
-
-```bash
-docker compose -f docker/docker-compose.yml run --rm build
-```
-
-This writes `dist/deskctl-linux-x86_64`.
-
-Copy it to an SSH machine where `scp` is unavailable:
-
-```bash
-ssh -p 443 deskctl@ssh.agentcomputer.ai 'cat > ~/deskctl && chmod +x ~/deskctl' < dist/deskctl-linux-x86_64
-```
-
-Run it on an X11 session:
-
-```bash
-DISPLAY=:1 XDG_SESSION_TYPE=x11 ~/deskctl --json snapshot --annotate
-```
-
-### Local Source Build
+Rust:
```bash
cargo build
```
-
-## Quick Start
-
-```bash
-# Diagnose the environment first
-deskctl doctor
-
-# See the desktop
-deskctl snapshot
-
-# Query focused runtime state
-deskctl get active-window
-deskctl get monitors
-
-# Click a window
-deskctl click @w1
-
-# Type text
-deskctl type "hello world"
-
-# Wait for a window or focus transition
-deskctl wait window --selector 'title=Firefox' --timeout 10
-deskctl wait focus --selector 'class=firefox' --timeout 5
-
-# Focus by explicit selector
-deskctl focus 'title=Firefox'
-```
-
-## Architecture
-
-Client-daemon architecture over Unix sockets (NDJSON wire protocol).
-The daemon starts automatically on first command and keeps the X11 connection alive for fast repeated calls.
-
-Source layout:
-
-- `src/lib.rs` exposes the shared library target
-- `src/main.rs` is the thin CLI wrapper
-- `src/` contains production code and unit tests
-- `tests/` contains Linux/X11 integration tests
-- `tests/support/` contains shared integration helpers
-
-## Runtime Requirements
-
-- Linux with X11 session
-- Rust 1.75+ plus the source-build dependencies above when building from source
-
-The binary itself only links the standard glibc runtime on Linux (`libc`, `libm`, `libgcc_s`).
-
-For deskctl to be fully functional on a fresh VM you still need:
-
-- an X11 server and an active `DISPLAY`
-- `XDG_SESSION_TYPE=x11` or an equivalent X11 session environment
-- a window manager or desktop environment that exposes standard EWMH properties such as `_NET_CLIENT_LIST_STACKING` and `_NET_ACTIVE_WINDOW`
-- an X server with the extensions needed for input simulation and screen metadata, which is standard on normal desktop X11 setups
-
-If setup fails, run:
-
-```bash
-deskctl doctor
-```
-
-## Contract Notes
-
-- `@wN` refs are short-lived handles assigned by `snapshot` and `list-windows`
-- `--json` output includes a stable `window_id` for programmatic targeting within the current daemon session
-- `list-windows` is a cheap read-only operation and does not capture or write a screenshot
-- the stable runtime JSON/error contract is documented in [docs/runtime-output.md](docs/runtime-output.md)
-
-## Read and Wait Surface
-
-The grouped runtime reads are:
-
-```bash
-deskctl get active-window
-deskctl get monitors
-deskctl get version
-deskctl get systeminfo
-```
-
-The grouped runtime waits are:
-
-```bash
-deskctl wait window --selector 'title=Firefox' --timeout 10
-deskctl wait focus --selector 'id=win3' --timeout 5
-```
-
-Successful `get active-window`, `wait window`, and `wait focus` responses return a `window` payload with:
-- `ref_id`
-- `window_id`
-- `title`
-- `app_name`
-- geometry (`x`, `y`, `width`, `height`)
-- state flags (`focused`, `minimized`)
-
-`get monitors` returns:
-- `count`
-- `monitors[]` with geometry and primary/automatic flags
-
-`get version` returns:
-- `version`
-- `backend`
-
-`get systeminfo` stays runtime-scoped and returns:
-- `backend`
-- `display`
-- `session_type`
-- `session`
-- `socket_path`
-- `screen`
-- `monitor_count`
-- `monitors`
-
-Wait timeout and selector failures are structured in `--json` mode so agents can recover without string parsing.
-
-## Output Policy
-
-Text mode is compact and follow-up-oriented, but JSON is the parsing contract.
-
-- use `--json` when an agent needs strict parsing
-- rely on `window_id`, selector-related fields, grouped read payloads, and structured error `kind` values for stable automation
-- treat monitor naming, incidental whitespace, and default screenshot file names as best-effort
-
-See [docs/runtime-output.md](docs/runtime-output.md) for the exact stable-vs-best-effort breakdown.
-
-## Distribution
-
-- GitHub Releases are the canonical binary source
-- crates.io package: `deskctl`
-- npm package: `deskctl-cli`
-- installed command on every channel: `deskctl`
-- repo-owned Nix install path: `flake.nix`
-
-For maintainer publishing and release steps, see [docs/releasing.md](docs/releasing.md).
-
-## Selector Contract
-
-Explicit selector modes:
-
-```bash
-ref=w1
-id=win1
-title=Firefox
-class=firefox
-focused
-```
-
-Legacy refs remain supported:
-
-```bash
-@w1
-w1
-win1
-```
-
-Bare selectors such as `firefox` are still supported as fuzzy substring matches, but they now fail on ambiguity and return candidate windows instead of silently picking the first match.
-
-## Support Boundary
-
-`deskctl` supports Linux X11 in this phase. Wayland and Hyprland are explicitly out of scope for the current runtime contract.
-
-## Workflow
-
-Local validation uses the root `Makefile`:
-
-```bash
-make fmt-check
-make lint
-make test-unit
-make test-integration
-make site-format-check
-make validate
-```
-
-`make validate` is the full repo-quality check and requires Linux with `xvfb-run` plus `pnpm --dir site install`.
-
-The repository standardizes on `pre-commit` for fast commit-time checks:
-
-```bash
-pre-commit install
-pre-commit run --all-files
-```
-
-See [CONTRIBUTING.md](CONTRIBUTING.md) for the full contributor guide.
-
-## Acknowledgements
-
-- [@barrettruth](github.com/barrettruth) - i stole the website from [vimdoc](https://github.com/barrettruth/vimdoc-language-server)
diff --git a/demo/index.html b/demo/index.html
new file mode 100644
index 0000000..70ac230
--- /dev/null
+++ b/demo/index.html
@@ -0,0 +1,969 @@
+
+
+
+
+
+deskctl - Desktop Control for AI Agents
+
+
+
+
+
+
deskctl
+
desktop control CLI for AI agents
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 📝
+ task_brief.txt
+ 2.1 KB
+
+
+ 📊
+ nvda_q1_data.csv
+ 48 KB
+
+
+ 📄
+ prev_report.pdf
+ 1.2 MB
+
+
+ 📁
+ archive/
+ --
+
+
+
+ task: Prepare NVDA Q1 earnings summary
+ source: finance.yahoo.com, local csv
+ output: Google Docs report with chart
+
+
+
+
+
+
+
+
+
Chrome - Yahoo Finance
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Chrome - Google Docs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Files
+
Yahoo Finance
+
Google Docs
+
+
+
+
+
+
+
+
+
AI agent controlling a live desktop via deskctl
+
+
+
+
+
+
diff --git a/docs/releasing.md b/docs/releasing.md
index 7271b83..849d661 100644
--- a/docs/releasing.md
+++ b/docs/releasing.md
@@ -12,14 +12,14 @@ GitHub Releases are the canonical binary source. The npm package consumes those
## Package Names
- crate: `deskctl`
-- npm package: `deskctl-cli`
+- npm package: `deskctl`
- installed command: `deskctl`
## Prerequisites
Before the first live publish on each registry:
-- npm ownership for `deskctl-cli`
+- npm ownership for `deskctl`
- crates.io ownership for `deskctl`
- repository secrets:
- `NPM_TOKEN`
@@ -59,12 +59,12 @@ The repository release workflow:
- publishes the canonical GitHub Release asset
- uploads `checksums.txt`
-The registry publish workflow:
+The registry publish jobs (npm and crates.io run in parallel):
-- targets an existing release tag
-- checks that Cargo, npm, and the requested tag all agree on version
-- checks whether that version is already published on npm and crates.io
-- only publishes the channels explicitly requested
+- target an existing release tag
+- check whether that version is already published on the respective registry
+- skip already-published versions
+- both default to enabled; can be toggled via workflow_dispatch inputs
## Rerun Safety
diff --git a/docs/runtime-contract.md b/docs/runtime-contract.md
new file mode 100644
index 0000000..ee4727b
--- /dev/null
+++ b/docs/runtime-contract.md
@@ -0,0 +1,70 @@
+# deskctl runtime contract
+
+All commands support `--json` and use the same top-level envelope:
+
+```json
+{
+ "success": true,
+ "data": {},
+ "error": null
+}
+```
+
+Use `--json` whenever you need to parse output programmatically.
+
+## Stable window fields
+
+Whenever a response includes a window payload, these fields are stable:
+
+- `ref_id`
+- `window_id`
+- `title`
+- `app_name`
+- `x`
+- `y`
+- `width`
+- `height`
+- `focused`
+- `minimized`
+
+Use `window_id` for stable targeting inside a live daemon session. Use
+`ref_id` or `@wN` for short-lived follow-up actions after `snapshot` or
+`list-windows`.
+
+## Stable grouped reads
+
+- `deskctl get active-window` -> `data.window`
+- `deskctl get monitors` -> `data.count`, `data.monitors`
+- `deskctl get version` -> `data.version`, `data.backend`
+- `deskctl get systeminfo` -> runtime-scoped diagnostic fields such as
+ `backend`, `display`, `session_type`, `session`, `socket_path`, `screen`,
+ `monitor_count`, and `monitors`
+
+## Stable waits
+
+- `deskctl wait window` -> `data.wait`, `data.selector`, `data.elapsed_ms`,
+ `data.window`
+- `deskctl wait focus` -> `data.wait`, `data.selector`, `data.elapsed_ms`,
+ `data.window`
+
+## Stable structured error kinds
+
+When a command fails with structured JSON data, these `kind` values are stable:
+
+- `selector_not_found`
+- `selector_ambiguous`
+- `selector_invalid`
+- `timeout`
+- `not_found`
+
+Wait failures may also include `window_not_focused` in the last observation
+payload.
+
+## Best-effort fields
+
+Treat these as useful but non-contractual:
+
+- exact monitor names
+- incidental text formatting in non-JSON mode
+- default screenshot file names when no explicit path was provided
+- environment-dependent ordering details from the window manager
diff --git a/docs/runtime-output.md b/docs/runtime-output.md
deleted file mode 100644
index 7312357..0000000
--- a/docs/runtime-output.md
+++ /dev/null
@@ -1,178 +0,0 @@
-# Runtime Output Contract
-
-This document defines the current output contract for `deskctl`.
-
-It is intentionally scoped to the current Linux X11 runtime surface.
-It does not promise stability for future Wayland or window-manager-specific features.
-
-## Goals
-
-- Keep `deskctl` fully non-interactive
-- Make text output actionable for quick terminal and agent loops
-- Make `--json` safe for agent consumption without depending on incidental formatting
-
-## JSON Envelope
-
-Every runtime command uses the same top-level JSON envelope:
-
-```json
-{
- "success": true,
- "data": {},
- "error": null
-}
-```
-
-Stable top-level fields:
-
-- `success`
-- `data`
-- `error`
-
-`success` is always the authoritative success/failure bit.
-When `success` is `false`, the CLI exits non-zero in both text mode and `--json` mode.
-
-## Stable Fields
-
-These fields are stable for agent consumption in the current Phase 1 runtime contract.
-
-### Window Identity
-
-Whenever a runtime response includes a window payload, these fields are stable:
-
-- `ref_id`
-- `window_id`
-- `title`
-- `app_name`
-- `x`
-- `y`
-- `width`
-- `height`
-- `focused`
-- `minimized`
-
-`window_id` is the stable public identifier for a live daemon session.
-`ref_id` is a short-lived convenience handle for the current window snapshot/ref map.
-
-### Grouped Reads
-
-`deskctl get active-window`
-
-- stable: `data.window`
-
-`deskctl get monitors`
-
-- stable: `data.count`
-- stable: `data.monitors`
-- stable per monitor:
- - `name`
- - `x`
- - `y`
- - `width`
- - `height`
- - `width_mm`
- - `height_mm`
- - `primary`
- - `automatic`
-
-`deskctl get version`
-
-- stable: `data.version`
-- stable: `data.backend`
-
-`deskctl get systeminfo`
-
-- stable: `data.backend`
-- stable: `data.display`
-- stable: `data.session_type`
-- stable: `data.session`
-- stable: `data.socket_path`
-- stable: `data.screen`
-- stable: `data.monitor_count`
-- stable: `data.monitors`
-
-### Waits
-
-`deskctl wait window`
-`deskctl wait focus`
-
-- stable: `data.wait`
-- stable: `data.selector`
-- stable: `data.elapsed_ms`
-- stable: `data.window`
-
-### Selector-Driven Action Success
-
-For selector-driven action commands that resolve a window target, these identifiers are stable when present:
-
-- `data.ref_id`
-- `data.window_id`
-- `data.title`
-- `data.selector`
-
-This applies to:
-
-- `click`
-- `dblclick`
-- `focus`
-- `close`
-- `move-window`
-- `resize-window`
-
-The exact human-readable text rendering of those commands is not part of the JSON contract.
-
-### Artifact-Producing Commands
-
-`snapshot`
-`screenshot`
-
-- stable: `data.screenshot`
-
-When the command also returns windows, `data.windows` uses the stable window payload documented above.
-
-## Stable Structured Error Kinds
-
-When a runtime command returns structured JSON failure data, these error kinds are stable:
-
-- `selector_not_found`
-- `selector_ambiguous`
-- `selector_invalid`
-- `timeout`
-- `not_found`
-- `window_not_focused` as `data.last_observation.kind` or equivalent observation payload
-
-Stable structured failure fields include:
-
-- `data.kind`
-- `data.selector` when selector-related
-- `data.mode` when selector-related
-- `data.candidates` for ambiguous selector failures
-- `data.message` for invalid selector failures
-- `data.wait`
-- `data.timeout_ms`
-- `data.poll_ms`
-- `data.last_observation`
-
-## Best-Effort Fields
-
-These values are useful but environment-dependent and should be treated as best-effort:
-
-- exact monitor naming conventions
-- EWMH/window-manager-dependent window ordering details
-- cosmetic text formatting in non-JSON mode
-- screenshot file names when the caller did not provide an explicit path
-- command stderr wording outside the structured `kind` classifications above
-
-## Text Mode Expectations
-
-Text mode is intended to stay compact and follow-up-useful.
-
-The exact whitespace/alignment of text output is not stable.
-The following expectations are stable at the behavioral level:
-
-- important runtime reads print actionable identifiers or geometry
-- selector failures print enough detail to recover without `--json`
-- artifact-producing commands print the artifact path
-- window listings print both `@wN` refs and `window_id` values
-
-If an agent needs strict parsing, it should use `--json`.
diff --git a/npm/deskctl-cli/README.md b/npm/deskctl/README.md
similarity index 59%
rename from npm/deskctl-cli/README.md
rename to npm/deskctl/README.md
index fd6f610..81f07f4 100644
--- a/npm/deskctl-cli/README.md
+++ b/npm/deskctl/README.md
@@ -1,11 +1,11 @@
-# deskctl-cli
+# deskctl
-`deskctl-cli` installs the `deskctl` command for Linux X11 systems.
+`deskctl` installs the command for Linux X11 systems.
## Install
```bash
-npm install -g deskctl-cli
+npm install -g deskctl
```
After install, run:
@@ -14,10 +14,22 @@ After install, run:
deskctl --help
```
+To upgrade version:
+
+```bash
+deskctl upgrade
+```
+
+For non-interactive use:
+
+```bash
+deskctl upgrade --yes
+```
+
One-shot usage is also supported:
```bash
-npx deskctl-cli --help
+npx deskctl --help
```
## Runtime Support
@@ -26,7 +38,7 @@ npx deskctl-cli --help
- X11 session
- currently packaged release asset: `linux-x64`
-`deskctl-cli` downloads the matching GitHub Release binary during install.
+`deskctl` downloads the matching GitHub Release binary during install.
Unsupported targets fail during install with a clear runtime support error instead of installing a broken command.
If you want the Rust source-install path instead, use:
diff --git a/npm/deskctl-cli/bin/deskctl.js b/npm/deskctl/bin/deskctl.js
similarity index 91%
rename from npm/deskctl-cli/bin/deskctl.js
rename to npm/deskctl/bin/deskctl.js
index 9f9b480..b8514cf 100644
--- a/npm/deskctl-cli/bin/deskctl.js
+++ b/npm/deskctl/bin/deskctl.js
@@ -17,7 +17,7 @@ function main() {
`Expected: ${binaryPath}`,
`Package version: ${pkg.version}`,
`Release tag: ${releaseTag(pkg)}`,
- "Try reinstalling deskctl-cli or check that your target is supported."
+ "Try reinstalling deskctl or check that your target is supported."
].join("\n")
);
process.exit(1);
diff --git a/npm/deskctl-cli/package.json b/npm/deskctl/package.json
similarity index 83%
rename from npm/deskctl-cli/package.json
rename to npm/deskctl/package.json
index 84f27ee..c676924 100644
--- a/npm/deskctl-cli/package.json
+++ b/npm/deskctl/package.json
@@ -1,7 +1,7 @@
{
- "name": "deskctl-cli",
- "version": "0.1.6",
- "description": "Installable deskctl CLI package for Linux X11 agents",
+ "name": "deskctl",
+ "version": "0.1.14",
+ "description": "Installable deskctl package for Linux X11 agents",
"license": "MIT",
"homepage": "https://github.com/harivansh-afk/deskctl",
"repository": {
diff --git a/npm/deskctl-cli/scripts/postinstall.js b/npm/deskctl/scripts/postinstall.js
similarity index 94%
rename from npm/deskctl-cli/scripts/postinstall.js
rename to npm/deskctl/scripts/postinstall.js
index de1b1d0..1f43ad0 100644
--- a/npm/deskctl-cli/scripts/postinstall.js
+++ b/npm/deskctl/scripts/postinstall.js
@@ -44,6 +44,6 @@ async function main() {
}
main().catch((error) => {
- console.error(`deskctl-cli install failed: ${error.message}`);
+ console.error(`deskctl install failed: ${error.message}`);
process.exit(1);
});
diff --git a/npm/deskctl-cli/scripts/support.js b/npm/deskctl/scripts/support.js
similarity index 97%
rename from npm/deskctl-cli/scripts/support.js
rename to npm/deskctl/scripts/support.js
index 8d41520..1fd0d47 100644
--- a/npm/deskctl-cli/scripts/support.js
+++ b/npm/deskctl/scripts/support.js
@@ -26,7 +26,7 @@ function supportedTarget(platform = process.platform, arch = process.arch) {
}
throw new Error(
- `deskctl-cli currently supports linux-x64 only. Received ${platform}-${arch}.`
+ `deskctl currently supports linux-x64 only. Received ${platform}-${arch}.`
);
}
diff --git a/npm/deskctl-cli/scripts/validate-package.js b/npm/deskctl/scripts/validate-package.js
similarity index 87%
rename from npm/deskctl-cli/scripts/validate-package.js
rename to npm/deskctl/scripts/validate-package.js
index 46d3e87..450fd6c 100644
--- a/npm/deskctl-cli/scripts/validate-package.js
+++ b/npm/deskctl/scripts/validate-package.js
@@ -26,13 +26,13 @@ function main() {
}
if (pkg.bin?.deskctl !== "bin/deskctl.js") {
- throw new Error("deskctl-cli must expose the deskctl bin entrypoint.");
+ throw new Error("deskctl must expose the deskctl bin entrypoint.");
}
const target = supportedTarget("linux", "x64");
const targetPath = vendorBinaryPath(target);
const vendorDir = path.dirname(targetPath);
- if (!vendorDir.endsWith(path.join("deskctl-cli", "vendor"))) {
+ if (!vendorDir.endsWith(path.join("deskctl", "vendor"))) {
throw new Error("Vendor binary directory resolved unexpectedly.");
}
}
diff --git a/site/src/layouts/DocLayout.astro b/site/src/layouts/DocLayout.astro
index f2608de..afc8648 100644
--- a/site/src/layouts/DocLayout.astro
+++ b/site/src/layouts/DocLayout.astro
@@ -30,7 +30,7 @@ function formatTocText(text: string): string {
{
!isIndex && (
-