Runtime for desktop GUI automation on macOS. Run procedures locally, or stream them from a remote WebSocket server.
Project description
case-sdk
Dual-mode runtime for desktop GUI automation on macOS.
- Local mode — write your own procedures in Python and run them on your machine.
- Remote mode — with an API key, stream procedures from a remote WebSocket server. The procedure source never lands on the client's disk.
Both modes share the same Runtime API.
Developers: see docs/DEVELOPER.md for API contracts, JSON schemas (docs/schemas/), procedure authoring, and MCP.
Install
pip install case-sdk # CLI + Python API
pip install "case-sdk[mcp]" # add the MCP server for Claude Code / agents
From source (for contributors):
git clone https://github.com/daemonlabshq/case-sdk
cd case-sdk
pip install -e ".[dev,mcp]"
Grant Accessibility in System Settings → Privacy & Security → Accessibility. Then run case-sdk doctor to verify.
Screen Recording is only required if a procedure uses the screenshot sensor.
Quickstart
case-sdk doctor # check permissions + config
case-sdk procedures # list built-in DaVinci procedures
case-sdk run davinci/focus_resolve # bring DaVinci frontmost
case-sdk run examples/open_textedit.json --param name=world
case-sdk doctor reports any missing macOS permissions and deep-links to System Settings. case-sdk procedures shows the discoverable catalog (built-ins plus anything under ~/.case/procedures/).
Write a procedure
A procedure is a Python generator. It yields Action to do things and Sensor to read state. The driver runs each yield against the local harness and feeds the result back into the generator via gen.send(...).
# my_proc.py
from case_sdk import Action, Sensor
def procedure(params):
yield Action("open_app", name="TextEdit")
yield Action("wait", ms=500)
yield Action("type", text="hello")
yield Action("hotkey", combo=["cmd", "s"])
title = yield Sensor("window_title")
return {"success": True, "title": title["value"]}
Run a local procedure
from case_sdk import Runtime
Runtime().execute_file("my_proc.py")
Or from the shell:
case-sdk run my_proc.py
Run a remote procedure (BYO server / managed case-api coming)
case-sdk speaks a WebSocket envelope protocol (see case_sdk.protocol) so a
server can hold a procedure and stream one step at a time while the client
harness executes it locally. You bring your own server today; the managed
case-api service is in active development and will be the default hosted
backend.
from case_sdk import Runtime
rt = Runtime(api_key="sk_...", api_base="wss://your-server.example/ws")
rt.execute("add_clip_to_timeline", filepath="/path/to/clip.mov")
Or:
case-sdk init --api wss://your-server.example/ws --key sk_... # one-time
case-sdk run add_clip_to_timeline --param filepath=/x.mov
In remote mode the server holds the procedure. The runtime receives one action at a time over WebSocket, runs it locally, returns the observation, and waits for the next instruction. Procedure source never lands on the client's disk.
Built-in primitives
Actions: open_app, activate_app, click, click_ref, type, hotkey, scroll, mouse_down, mouse_up, wait, ax_press, ax_set_value, fn_key, drag, menu_click.
Sensors: window_title, ax_value, screenshot.
Inspect the AX tree (authoring)
Discover UI elements before writing ax_press / ax_set_value steps:
case-sdk ax-tree --frontmost
case-sdk ax-tree "DaVinci Resolve" --depth 10
case-sdk ax-tree "DaVinci Resolve" --find --role AXButton --title-contains Append
Python API:
from case_sdk import app_tree, find_elements
tree = app_tree("TextEdit", max_depth=8)
refs = find_elements("DaVinci Resolve", role="AXButton", title_contains="Save")
Each node includes role, title, and optional path (child indices). Use those fields in element_ref when yielding AX actions.
Verification (Project.db and more)
After GUI actions, confirm state with verify steps in JSON procedures (or call from Python):
{
"verify_mode": "soft",
"steps": [
{"verify": "davinci.db_snapshot", "as": "before"},
{"action": "wait", "ms": 500},
{"verify": "davinci.db_delta", "field": "timeline_video", "baseline": "before", "min_delta": 1}
]
}
soft(default) — log a warning and continue (good for demos; unsaved Resolve projects).hard— raiseClientErrorwith codeE_VERIFY(set"verify_mode": "hard"orCASE_SDK_VERIFY_MODE=hard).
Built-in verifiers:
| Name | Purpose |
|---|---|
davinci.db_snapshot |
Store row-count baseline (as optional) |
davinci.db_delta |
Poll until a field grows (or max_delta for deletes) |
davinci.media_pool_name |
Clip name appears in media pool |
davinci.timeline_clips |
Read timeline items (min_clips optional) |
case-sdk verify-list
Procedure discovery (Phase 0)
Built-in DaVinci procedures ship with the package. Override or add your own under
~/.case/procedures/ (created by case-sdk init).
case-sdk procedures
case-sdk run davinci/import_and_append --param filepath=/path/to/clip.mov
case-sdk run davinci/append_to_timeline
Search path (first match wins): $CASE_SDK_PROCEDURES_PATH → ~/.case/procedures → ~/.case-sdk/procedures → built-ins.
| Built-in name | Purpose |
|---|---|
davinci/focus_resolve |
Activate DaVinci |
davinci/go_to_page |
Switch page tab (--param page=edit) |
davinci/open_media_pool |
Edit page + open Media Pool panel |
davinci/import_media |
Import one file (filepath, auto clip_basename) |
davinci/import_media_on_edit |
Go to Edit, then import (filepath) |
davinci/append_to_timeline |
Append selected pool clip (menu click + DB verify) |
davinci/append_to_timeline_hotkey |
Append via Fn+F9 fallback |
davinci/import_and_append |
Import then append |
JSON step lists (no Python required)
Write a .json procedure and run it like a .py file:
{
"steps": [
{"action": "open_app", "name": "TextEdit"},
{"action": "wait", "ms": 500},
{"action": "type", "text": "hello ${name}"}
],
"return": {"success": true}
}
case-sdk run examples/open_textedit.json --param name=world
CLI
case-sdk run <path-or-name> [--key sk_xxx] [--api wss://...] [--param k=v]...
case-sdk ax-tree [app] [--frontmost] [--find] [--role ...] [--title-contains ...]
case-sdk procedures [--json]
case-sdk verify-list
case-sdk init [--key sk_xxx] [--api wss://...] [--force]
case-sdk doctor
case-sdk --version
Configuration resolves in order: CLI flag → environment (CASE_SDK_API_KEY, CASE_SDK_API_BASE) → ~/.case-sdk/config.toml.
MCP (agents — Claude Code, etc.)
The case-mcp server exposes the full local SDK over MCP stdio. It does not duplicate
automation logic — every tool delegates to case_sdk (same as the CLI).
pip install -e ".[mcp]"
case-sdk doctor
# Claude Code (use your venv python path)
claude mcp add case-sdk -- python -m case_mcp.server
| MCP tool | Purpose |
|---|---|
case_help |
Planner guide + tool catalog |
case_doctor |
Permissions and procedure dirs |
case_focus_app |
Activate an app before keystrokes |
case_ax_tree |
Dump AX hierarchy |
case_find_elements |
Search → element_ref dicts |
case_list_procedures |
Built-in + user procedure names |
case_list_verifiers |
JSON verify step names |
case_run |
Run a procedure (params dict, optional verify_mode) |
Example agent flow: case_list_procedures → case_focus_app("DaVinci Resolve") →
case_run("davinci/import_and_append", {"filepath": "/abs/path/clip.mov"}).
Remote streaming procedures remain CLI-only for now (case-sdk run with --key).
MCP troubleshooting
If claude mcp list shows case-sdk: ... ✗ Failed to connect, the most
common cause is the mcp package missing from the venv you installed
case-sdk into. Verify and fix:
/path/to/your/venv/bin/pip show mcp
/path/to/your/venv/bin/pip install "mcp[cli]>=1.0"
claude mcp remove case-sdk
claude mcp add case-sdk -- /path/to/your/venv/bin/python -m case_mcp.server
A quick import probe that crashes loudly when mcp is missing:
python -c "from case_mcp.server import create_mcp; create_mcp(); print('ok')"
Error handling
Inside a procedure you can catch harness failures and decide what to do:
from case_sdk import Action, ClientError
def procedure(params):
try:
yield Action("hotkey", combo=["cmd", "s"])
except ClientError as e:
if e.code == "E_TIMEOUT":
return {"success": False, "reason": "save timed out"}
raise
ClientError represents a recoverable harness failure (timeout, primitive raised). Uncaught exceptions and re-raised ClientErrors surface to the caller of Runtime.execute* as ProcedureError.
Security model
case-sdk is a desktop automation runtime. Once Accessibility is granted, a procedure can do anything the user can — and that is the design, not a leak.
- Local
.pyprocedures execute arbitrary Python at the installing user's privilege. Treat them like shell scripts. - JSON procedures are not a sandbox. They reach every harness primitive (
click,type,menu_click,ax_set_value,hotkey,screenshot, ...). A JSON procedure can do anything a Python procedure can short ofimport. menu_clickruns AppleScript, which can in turn run shell commands. Treat thescriptfield as shell-equivalent.- Remote mode (
--key/Runtime(api_key=...)) trusts the server unconditionally. Whatever WebSocket server you point case-sdk at can stream any action — clicks, keystrokes, screenshot requests — to your machine. Only connect to servers you control or pay for. - The
screenshotsensor captures the entire screen by default. A procedure or server requesting it can see whatever is on your display.
Practical guidance:
- Only run procedures you wrote or trust.
- Keep
~/.case/procedures/to yourself (chmod 700). Anyone with write access there can drive your apps. - Don't paste secrets into procedure params if the procedure may forward them anywhere.
- Grant Accessibility only while you're actively using case-sdk; revoke when done.
case-sdk is built for trusted automation flows. It is not built to sandbox untrusted code.
License
Elastic License 2.0 (ELv2). Source-available. You may use, modify, and self-host case-sdk freely. You may not provide it to third parties as a hosted or managed service. See LICENSE for the full text.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file case_sdk-0.1.2.tar.gz.
File metadata
- Download URL: case_sdk-0.1.2.tar.gz
- Upload date:
- Size: 94.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a9c0fa16310e2e068b5df00d4fbbcbe0dfc80b061c88b16ab04ee2020f848fe7
|
|
| MD5 |
574b393c18a5b39aa2548341bb1688e3
|
|
| BLAKE2b-256 |
357347a031ab9edef982e63bfb1d36dd52026c2be6575251d73e74431fad0c43
|
Provenance
The following attestation bundles were made for case_sdk-0.1.2.tar.gz:
Publisher:
publish.yml on daemonlabshq/case-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
case_sdk-0.1.2.tar.gz -
Subject digest:
a9c0fa16310e2e068b5df00d4fbbcbe0dfc80b061c88b16ab04ee2020f848fe7 - Sigstore transparency entry: 1597012983
- Sigstore integration time:
-
Permalink:
daemonlabshq/case-sdk@13fa3f328e567d194c82c182f90121bf9555f2c1 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/daemonlabshq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@13fa3f328e567d194c82c182f90121bf9555f2c1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file case_sdk-0.1.2-py3-none-any.whl.
File metadata
- Download URL: case_sdk-0.1.2-py3-none-any.whl
- Upload date:
- Size: 72.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72228b37f803b69b281825f9ca07441a2f9f5581c4c8979f973dc8d6570e0faa
|
|
| MD5 |
a0f471b8ddb52f5b7bc1fd2b1a1760eb
|
|
| BLAKE2b-256 |
4608b868d2582363104bfa5416528b2b47bda8d881af8ca4332161b3b30bf31b
|
Provenance
The following attestation bundles were made for case_sdk-0.1.2-py3-none-any.whl:
Publisher:
publish.yml on daemonlabshq/case-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
case_sdk-0.1.2-py3-none-any.whl -
Subject digest:
72228b37f803b69b281825f9ca07441a2f9f5581c4c8979f973dc8d6570e0faa - Sigstore transparency entry: 1597013060
- Sigstore integration time:
-
Permalink:
daemonlabshq/case-sdk@13fa3f328e567d194c82c182f90121bf9555f2c1 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/daemonlabshq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@13fa3f328e567d194c82c182f90121bf9555f2c1 -
Trigger Event:
push
-
Statement type: