This is page 15 of 21. Use http://codebase.md/trycua/cua?page={x} to view the full context.
# Directory Structure
```
├── .cursorignore
├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github
│ ├── FUNDING.yml
│ ├── scripts
│ │ ├── get_pyproject_version.py
│ │ └── tests
│ │ ├── __init__.py
│ │ ├── README.md
│ │ └── test_get_pyproject_version.py
│ └── workflows
│ ├── bump-version.yml
│ ├── ci-lume.yml
│ ├── docker-publish-cua-linux.yml
│ ├── docker-publish-cua-windows.yml
│ ├── docker-publish-kasm.yml
│ ├── docker-publish-xfce.yml
│ ├── docker-reusable-publish.yml
│ ├── link-check.yml
│ ├── lint.yml
│ ├── npm-publish-cli.yml
│ ├── npm-publish-computer.yml
│ ├── npm-publish-core.yml
│ ├── publish-lume.yml
│ ├── pypi-publish-agent.yml
│ ├── pypi-publish-computer-server.yml
│ ├── pypi-publish-computer.yml
│ ├── pypi-publish-core.yml
│ ├── pypi-publish-mcp-server.yml
│ ├── pypi-publish-som.yml
│ ├── pypi-reusable-publish.yml
│ ├── python-tests.yml
│ ├── test-cua-models.yml
│ └── test-validation-script.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .prettierignore
├── .prettierrc.yaml
├── .vscode
│ ├── docs.code-workspace
│ ├── extensions.json
│ ├── launch.json
│ ├── libs-ts.code-workspace
│ ├── lume.code-workspace
│ ├── lumier.code-workspace
│ ├── py.code-workspace
│ └── settings.json
├── blog
│ ├── app-use.md
│ ├── assets
│ │ ├── composite-agents.png
│ │ ├── docker-ubuntu-support.png
│ │ ├── hack-booth.png
│ │ ├── hack-closing-ceremony.jpg
│ │ ├── hack-cua-ollama-hud.jpeg
│ │ ├── hack-leaderboard.png
│ │ ├── hack-the-north.png
│ │ ├── hack-winners.jpeg
│ │ ├── hack-workshop.jpeg
│ │ ├── hud-agent-evals.png
│ │ └── trajectory-viewer.jpeg
│ ├── bringing-computer-use-to-the-web.md
│ ├── build-your-own-operator-on-macos-1.md
│ ├── build-your-own-operator-on-macos-2.md
│ ├── cloud-windows-ga-macos-preview.md
│ ├── composite-agents.md
│ ├── computer-use-agents-for-growth-hacking.md
│ ├── cua-hackathon.md
│ ├── cua-playground-preview.md
│ ├── cua-vlm-router.md
│ ├── hack-the-north.md
│ ├── hud-agent-evals.md
│ ├── human-in-the-loop.md
│ ├── introducing-cua-cli.md
│ ├── introducing-cua-cloud-containers.md
│ ├── lume-to-containerization.md
│ ├── neurips-2025-cua-papers.md
│ ├── sandboxed-python-execution.md
│ ├── training-computer-use-models-trajectories-1.md
│ ├── trajectory-viewer.md
│ ├── ubuntu-docker-support.md
│ └── windows-sandbox.md
├── CONTRIBUTING.md
├── Development.md
├── Dockerfile
├── docs
│ ├── .env.example
│ ├── .gitignore
│ ├── content
│ │ └── docs
│ │ ├── agent-sdk
│ │ │ ├── agent-loops.mdx
│ │ │ ├── benchmarks
│ │ │ │ ├── index.mdx
│ │ │ │ ├── interactive.mdx
│ │ │ │ ├── introduction.mdx
│ │ │ │ ├── meta.json
│ │ │ │ ├── osworld-verified.mdx
│ │ │ │ ├── screenspot-pro.mdx
│ │ │ │ └── screenspot-v2.mdx
│ │ │ ├── callbacks
│ │ │ │ ├── agent-lifecycle.mdx
│ │ │ │ ├── cost-saving.mdx
│ │ │ │ ├── index.mdx
│ │ │ │ ├── logging.mdx
│ │ │ │ ├── meta.json
│ │ │ │ ├── pii-anonymization.mdx
│ │ │ │ └── trajectories.mdx
│ │ │ ├── chat-history.mdx
│ │ │ ├── custom-tools.mdx
│ │ │ ├── customizing-computeragent.mdx
│ │ │ ├── integrations
│ │ │ │ ├── hud.mdx
│ │ │ │ ├── meta.json
│ │ │ │ └── observability.mdx
│ │ │ ├── mcp-server
│ │ │ │ ├── client-integrations.mdx
│ │ │ │ ├── configuration.mdx
│ │ │ │ ├── index.mdx
│ │ │ │ ├── installation.mdx
│ │ │ │ ├── llm-integrations.mdx
│ │ │ │ ├── meta.json
│ │ │ │ ├── tools.mdx
│ │ │ │ └── usage.mdx
│ │ │ ├── message-format.mdx
│ │ │ ├── meta.json
│ │ │ ├── migration-guide.mdx
│ │ │ ├── prompt-caching.mdx
│ │ │ ├── supported-agents
│ │ │ │ ├── composed-agents.mdx
│ │ │ │ ├── computer-use-agents.mdx
│ │ │ │ ├── grounding-models.mdx
│ │ │ │ ├── human-in-the-loop.mdx
│ │ │ │ └── meta.json
│ │ │ ├── supported-model-providers
│ │ │ │ ├── cua-vlm-router.mdx
│ │ │ │ ├── index.mdx
│ │ │ │ └── local-models.mdx
│ │ │ ├── telemetry.mdx
│ │ │ └── usage-tracking.mdx
│ │ ├── cli-playbook
│ │ │ ├── commands.mdx
│ │ │ ├── index.mdx
│ │ │ └── meta.json
│ │ ├── computer-sdk
│ │ │ ├── cloud-vm-management.mdx
│ │ │ ├── commands.mdx
│ │ │ ├── computer-server
│ │ │ │ ├── Commands.mdx
│ │ │ │ ├── index.mdx
│ │ │ │ ├── meta.json
│ │ │ │ ├── REST-API.mdx
│ │ │ │ └── WebSocket-API.mdx
│ │ │ ├── computer-ui.mdx
│ │ │ ├── computers.mdx
│ │ │ ├── custom-computer-handlers.mdx
│ │ │ ├── meta.json
│ │ │ ├── sandboxed-python.mdx
│ │ │ └── tracing-api.mdx
│ │ ├── example-usecases
│ │ │ ├── form-filling.mdx
│ │ │ ├── gemini-complex-ui-navigation.mdx
│ │ │ ├── meta.json
│ │ │ ├── post-event-contact-export.mdx
│ │ │ └── windows-app-behind-vpn.mdx
│ │ ├── get-started
│ │ │ ├── meta.json
│ │ │ └── quickstart.mdx
│ │ ├── index.mdx
│ │ ├── macos-vm-cli-playbook
│ │ │ ├── lume
│ │ │ │ ├── cli-reference.mdx
│ │ │ │ ├── faq.md
│ │ │ │ ├── http-api.mdx
│ │ │ │ ├── index.mdx
│ │ │ │ ├── installation.mdx
│ │ │ │ ├── meta.json
│ │ │ │ └── prebuilt-images.mdx
│ │ │ ├── lumier
│ │ │ │ ├── building-lumier.mdx
│ │ │ │ ├── docker-compose.mdx
│ │ │ │ ├── docker.mdx
│ │ │ │ ├── index.mdx
│ │ │ │ ├── installation.mdx
│ │ │ │ └── meta.json
│ │ │ └── meta.json
│ │ └── meta.json
│ ├── next.config.mjs
│ ├── package-lock.json
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── postcss.config.mjs
│ ├── public
│ │ └── img
│ │ ├── agent_gradio_ui.png
│ │ ├── agent.png
│ │ ├── bg-dark.jpg
│ │ ├── bg-light.jpg
│ │ ├── cli.png
│ │ ├── computer.png
│ │ ├── grounding-with-gemini3.gif
│ │ ├── hero.png
│ │ ├── laminar_trace_example.png
│ │ ├── som_box_threshold.png
│ │ └── som_iou_threshold.png
│ ├── README.md
│ ├── source.config.ts
│ ├── src
│ │ ├── app
│ │ │ ├── (home)
│ │ │ │ ├── [[...slug]]
│ │ │ │ │ └── page.tsx
│ │ │ │ └── layout.tsx
│ │ │ ├── api
│ │ │ │ ├── posthog
│ │ │ │ │ └── [...path]
│ │ │ │ │ └── route.ts
│ │ │ │ └── search
│ │ │ │ └── route.ts
│ │ │ ├── favicon.ico
│ │ │ ├── global.css
│ │ │ ├── layout.config.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── llms.mdx
│ │ │ │ └── [[...slug]]
│ │ │ │ └── route.ts
│ │ │ ├── llms.txt
│ │ │ │ └── route.ts
│ │ │ ├── robots.ts
│ │ │ └── sitemap.ts
│ │ ├── assets
│ │ │ ├── discord-black.svg
│ │ │ ├── discord-white.svg
│ │ │ ├── logo-black.svg
│ │ │ └── logo-white.svg
│ │ ├── components
│ │ │ ├── analytics-tracker.tsx
│ │ │ ├── cookie-consent.tsx
│ │ │ ├── doc-actions-menu.tsx
│ │ │ ├── editable-code-block.tsx
│ │ │ ├── footer.tsx
│ │ │ ├── hero.tsx
│ │ │ ├── iou.tsx
│ │ │ ├── mermaid.tsx
│ │ │ └── page-feedback.tsx
│ │ ├── lib
│ │ │ ├── llms.ts
│ │ │ └── source.ts
│ │ ├── mdx-components.tsx
│ │ └── providers
│ │ └── posthog-provider.tsx
│ └── tsconfig.json
├── examples
│ ├── agent_examples.py
│ ├── agent_ui_examples.py
│ ├── browser_tool_example.py
│ ├── cloud_api_examples.py
│ ├── computer_examples_windows.py
│ ├── computer_examples.py
│ ├── computer_ui_examples.py
│ ├── computer-example-ts
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── pnpm-lock.yaml
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── helpers.ts
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── docker_examples.py
│ ├── evals
│ │ ├── hud_eval_examples.py
│ │ └── wikipedia_most_linked.txt
│ ├── pylume_examples.py
│ ├── sandboxed_functions_examples.py
│ ├── som_examples.py
│ ├── tracing_examples.py
│ ├── utils.py
│ └── winsandbox_example.py
├── img
│ ├── agent_gradio_ui.png
│ ├── agent.png
│ ├── cli.png
│ ├── computer.png
│ ├── logo_black.png
│ └── logo_white.png
├── libs
│ ├── kasm
│ │ ├── Dockerfile
│ │ ├── LICENSE
│ │ ├── README.md
│ │ └── src
│ │ └── ubuntu
│ │ └── install
│ │ └── firefox
│ │ ├── custom_startup.sh
│ │ ├── firefox.desktop
│ │ └── install_firefox.sh
│ ├── lume
│ │ ├── .cursorignore
│ │ ├── CONTRIBUTING.md
│ │ ├── Development.md
│ │ ├── img
│ │ │ └── cli.png
│ │ ├── Package.resolved
│ │ ├── Package.swift
│ │ ├── README.md
│ │ ├── resources
│ │ │ └── lume.entitlements
│ │ ├── scripts
│ │ │ ├── build
│ │ │ │ ├── build-debug.sh
│ │ │ │ ├── build-release-notarized.sh
│ │ │ │ └── build-release.sh
│ │ │ └── install.sh
│ │ ├── src
│ │ │ ├── Commands
│ │ │ │ ├── Clone.swift
│ │ │ │ ├── Config.swift
│ │ │ │ ├── Create.swift
│ │ │ │ ├── Delete.swift
│ │ │ │ ├── Get.swift
│ │ │ │ ├── Images.swift
│ │ │ │ ├── IPSW.swift
│ │ │ │ ├── List.swift
│ │ │ │ ├── Logs.swift
│ │ │ │ ├── Options
│ │ │ │ │ └── FormatOption.swift
│ │ │ │ ├── Prune.swift
│ │ │ │ ├── Pull.swift
│ │ │ │ ├── Push.swift
│ │ │ │ ├── Run.swift
│ │ │ │ ├── Serve.swift
│ │ │ │ ├── Set.swift
│ │ │ │ └── Stop.swift
│ │ │ ├── ContainerRegistry
│ │ │ │ ├── ImageContainerRegistry.swift
│ │ │ │ ├── ImageList.swift
│ │ │ │ └── ImagesPrinter.swift
│ │ │ ├── Errors
│ │ │ │ └── Errors.swift
│ │ │ ├── FileSystem
│ │ │ │ ├── Home.swift
│ │ │ │ ├── Settings.swift
│ │ │ │ ├── VMConfig.swift
│ │ │ │ ├── VMDirectory.swift
│ │ │ │ └── VMLocation.swift
│ │ │ ├── LumeController.swift
│ │ │ ├── Main.swift
│ │ │ ├── Server
│ │ │ │ ├── Handlers.swift
│ │ │ │ ├── HTTP.swift
│ │ │ │ ├── Requests.swift
│ │ │ │ ├── Responses.swift
│ │ │ │ └── Server.swift
│ │ │ ├── Utils
│ │ │ │ ├── CommandRegistry.swift
│ │ │ │ ├── CommandUtils.swift
│ │ │ │ ├── Logger.swift
│ │ │ │ ├── NetworkUtils.swift
│ │ │ │ ├── Path.swift
│ │ │ │ ├── ProcessRunner.swift
│ │ │ │ ├── ProgressLogger.swift
│ │ │ │ ├── String.swift
│ │ │ │ └── Utils.swift
│ │ │ ├── Virtualization
│ │ │ │ ├── DarwinImageLoader.swift
│ │ │ │ ├── DHCPLeaseParser.swift
│ │ │ │ ├── ImageLoaderFactory.swift
│ │ │ │ └── VMVirtualizationService.swift
│ │ │ ├── VM
│ │ │ │ ├── DarwinVM.swift
│ │ │ │ ├── LinuxVM.swift
│ │ │ │ ├── VM.swift
│ │ │ │ ├── VMDetails.swift
│ │ │ │ ├── VMDetailsPrinter.swift
│ │ │ │ ├── VMDisplayResolution.swift
│ │ │ │ └── VMFactory.swift
│ │ │ └── VNC
│ │ │ ├── PassphraseGenerator.swift
│ │ │ └── VNCService.swift
│ │ └── tests
│ │ ├── Mocks
│ │ │ ├── MockVM.swift
│ │ │ ├── MockVMVirtualizationService.swift
│ │ │ └── MockVNCService.swift
│ │ ├── VM
│ │ │ └── VMDetailsPrinterTests.swift
│ │ ├── VMTests.swift
│ │ ├── VMVirtualizationServiceTests.swift
│ │ └── VNCServiceTests.swift
│ ├── lumier
│ │ ├── .dockerignore
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ └── src
│ │ ├── bin
│ │ │ └── entry.sh
│ │ ├── config
│ │ │ └── constants.sh
│ │ ├── hooks
│ │ │ └── on-logon.sh
│ │ └── lib
│ │ ├── utils.sh
│ │ └── vm.sh
│ ├── python
│ │ ├── agent
│ │ │ ├── .bumpversion.cfg
│ │ │ ├── agent
│ │ │ │ ├── __init__.py
│ │ │ │ ├── __main__.py
│ │ │ │ ├── adapters
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── cua_adapter.py
│ │ │ │ │ ├── huggingfacelocal_adapter.py
│ │ │ │ │ ├── human_adapter.py
│ │ │ │ │ ├── mlxvlm_adapter.py
│ │ │ │ │ └── models
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── generic.py
│ │ │ │ │ ├── internvl.py
│ │ │ │ │ ├── opencua.py
│ │ │ │ │ └── qwen2_5_vl.py
│ │ │ │ ├── agent.py
│ │ │ │ ├── callbacks
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── base.py
│ │ │ │ │ ├── budget_manager.py
│ │ │ │ │ ├── image_retention.py
│ │ │ │ │ ├── logging.py
│ │ │ │ │ ├── operator_validator.py
│ │ │ │ │ ├── pii_anonymization.py
│ │ │ │ │ ├── prompt_instructions.py
│ │ │ │ │ ├── telemetry.py
│ │ │ │ │ └── trajectory_saver.py
│ │ │ │ ├── cli.py
│ │ │ │ ├── computers
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── base.py
│ │ │ │ │ ├── cua.py
│ │ │ │ │ └── custom.py
│ │ │ │ ├── decorators.py
│ │ │ │ ├── human_tool
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── __main__.py
│ │ │ │ │ ├── server.py
│ │ │ │ │ └── ui.py
│ │ │ │ ├── integrations
│ │ │ │ │ └── hud
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── agent.py
│ │ │ │ │ └── proxy.py
│ │ │ │ ├── loops
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── anthropic.py
│ │ │ │ │ ├── base.py
│ │ │ │ │ ├── composed_grounded.py
│ │ │ │ │ ├── gelato.py
│ │ │ │ │ ├── gemini.py
│ │ │ │ │ ├── generic_vlm.py
│ │ │ │ │ ├── glm45v.py
│ │ │ │ │ ├── gta1.py
│ │ │ │ │ ├── holo.py
│ │ │ │ │ ├── internvl.py
│ │ │ │ │ ├── model_types.csv
│ │ │ │ │ ├── moondream3.py
│ │ │ │ │ ├── omniparser.py
│ │ │ │ │ ├── openai.py
│ │ │ │ │ ├── opencua.py
│ │ │ │ │ ├── uiins.py
│ │ │ │ │ ├── uitars.py
│ │ │ │ │ └── uitars2.py
│ │ │ │ ├── proxy
│ │ │ │ │ ├── examples.py
│ │ │ │ │ └── handlers.py
│ │ │ │ ├── responses.py
│ │ │ │ ├── tools
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── browser_tool.py
│ │ │ │ ├── types.py
│ │ │ │ └── ui
│ │ │ │ ├── __init__.py
│ │ │ │ ├── __main__.py
│ │ │ │ └── gradio
│ │ │ │ ├── __init__.py
│ │ │ │ ├── app.py
│ │ │ │ └── ui_components.py
│ │ │ ├── benchmarks
│ │ │ │ ├── .gitignore
│ │ │ │ ├── contrib.md
│ │ │ │ ├── interactive.py
│ │ │ │ ├── models
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── base.py
│ │ │ │ │ └── gta1.py
│ │ │ │ ├── README.md
│ │ │ │ ├── ss-pro.py
│ │ │ │ ├── ss-v2.py
│ │ │ │ └── utils.py
│ │ │ ├── example.py
│ │ │ ├── pyproject.toml
│ │ │ ├── README.md
│ │ │ └── tests
│ │ │ ├── conftest.py
│ │ │ └── test_computer_agent.py
│ │ ├── bench-ui
│ │ │ ├── bench_ui
│ │ │ │ ├── __init__.py
│ │ │ │ ├── api.py
│ │ │ │ └── child.py
│ │ │ ├── examples
│ │ │ │ ├── folder_example.py
│ │ │ │ ├── gui
│ │ │ │ │ ├── index.html
│ │ │ │ │ ├── logo.svg
│ │ │ │ │ └── styles.css
│ │ │ │ ├── output_overlay.png
│ │ │ │ └── simple_example.py
│ │ │ ├── pyproject.toml
│ │ │ ├── README.md
│ │ │ └── tests
│ │ │ └── test_port_detection.py
│ │ ├── computer
│ │ │ ├── .bumpversion.cfg
│ │ │ ├── computer
│ │ │ │ ├── __init__.py
│ │ │ │ ├── computer.py
│ │ │ │ ├── diorama_computer.py
│ │ │ │ ├── helpers.py
│ │ │ │ ├── interface
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── base.py
│ │ │ │ │ ├── factory.py
│ │ │ │ │ ├── generic.py
│ │ │ │ │ ├── linux.py
│ │ │ │ │ ├── macos.py
│ │ │ │ │ ├── models.py
│ │ │ │ │ └── windows.py
│ │ │ │ ├── logger.py
│ │ │ │ ├── models.py
│ │ │ │ ├── providers
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── base.py
│ │ │ │ │ ├── cloud
│ │ │ │ │ │ ├── __init__.py
│ │ │ │ │ │ └── provider.py
│ │ │ │ │ ├── docker
│ │ │ │ │ │ ├── __init__.py
│ │ │ │ │ │ └── provider.py
│ │ │ │ │ ├── factory.py
│ │ │ │ │ ├── lume
│ │ │ │ │ │ ├── __init__.py
│ │ │ │ │ │ └── provider.py
│ │ │ │ │ ├── lume_api.py
│ │ │ │ │ ├── lumier
│ │ │ │ │ │ ├── __init__.py
│ │ │ │ │ │ └── provider.py
│ │ │ │ │ ├── types.py
│ │ │ │ │ └── winsandbox
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── provider.py
│ │ │ │ │ └── setup_script.ps1
│ │ │ │ ├── tracing_wrapper.py
│ │ │ │ ├── tracing.py
│ │ │ │ ├── ui
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── __main__.py
│ │ │ │ │ └── gradio
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── app.py
│ │ │ │ └── utils.py
│ │ │ ├── poetry.toml
│ │ │ ├── pyproject.toml
│ │ │ ├── README.md
│ │ │ └── tests
│ │ │ ├── conftest.py
│ │ │ ├── test_computer.py
│ │ │ └── test_helpers.py
│ │ ├── computer-server
│ │ │ ├── .bumpversion.cfg
│ │ │ ├── computer_server
│ │ │ │ ├── __init__.py
│ │ │ │ ├── __main__.py
│ │ │ │ ├── browser.py
│ │ │ │ ├── cli.py
│ │ │ │ ├── diorama
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── base.py
│ │ │ │ │ ├── diorama_computer.py
│ │ │ │ │ ├── diorama.py
│ │ │ │ │ ├── draw.py
│ │ │ │ │ ├── macos.py
│ │ │ │ │ └── safezone.py
│ │ │ │ ├── handlers
│ │ │ │ │ ├── base.py
│ │ │ │ │ ├── factory.py
│ │ │ │ │ ├── generic.py
│ │ │ │ │ ├── linux.py
│ │ │ │ │ ├── macos.py
│ │ │ │ │ └── windows.py
│ │ │ │ ├── main.py
│ │ │ │ ├── server.py
│ │ │ │ ├── utils
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── wallpaper.py
│ │ │ │ └── watchdog.py
│ │ │ ├── examples
│ │ │ │ ├── __init__.py
│ │ │ │ └── usage_example.py
│ │ │ ├── pyproject.toml
│ │ │ ├── README.md
│ │ │ ├── run_server.py
│ │ │ ├── test_connection.py
│ │ │ └── tests
│ │ │ ├── conftest.py
│ │ │ └── test_server.py
│ │ ├── core
│ │ │ ├── .bumpversion.cfg
│ │ │ ├── core
│ │ │ │ ├── __init__.py
│ │ │ │ └── telemetry
│ │ │ │ ├── __init__.py
│ │ │ │ └── posthog.py
│ │ │ ├── poetry.toml
│ │ │ ├── pyproject.toml
│ │ │ ├── README.md
│ │ │ └── tests
│ │ │ ├── conftest.py
│ │ │ └── test_telemetry.py
│ │ ├── mcp-server
│ │ │ ├── .bumpversion.cfg
│ │ │ ├── build-extension.py
│ │ │ ├── CONCURRENT_SESSIONS.md
│ │ │ ├── desktop-extension
│ │ │ │ ├── cua-extension.mcpb
│ │ │ │ ├── desktop_extension.png
│ │ │ │ ├── manifest.json
│ │ │ │ ├── README.md
│ │ │ │ ├── requirements.txt
│ │ │ │ ├── run_server.sh
│ │ │ │ └── setup.py
│ │ │ ├── mcp_server
│ │ │ │ ├── __init__.py
│ │ │ │ ├── __main__.py
│ │ │ │ ├── server.py
│ │ │ │ └── session_manager.py
│ │ │ ├── pdm.lock
│ │ │ ├── pyproject.toml
│ │ │ ├── QUICK_TEST_COMMANDS.sh
│ │ │ ├── quick_test_local_option.py
│ │ │ ├── README.md
│ │ │ ├── scripts
│ │ │ │ ├── install_mcp_server.sh
│ │ │ │ └── start_mcp_server.sh
│ │ │ ├── test_mcp_server_local_option.py
│ │ │ └── tests
│ │ │ ├── conftest.py
│ │ │ └── test_mcp_server.py
│ │ ├── pylume
│ │ │ └── tests
│ │ │ ├── conftest.py
│ │ │ └── test_pylume.py
│ │ └── som
│ │ ├── .bumpversion.cfg
│ │ ├── LICENSE
│ │ ├── poetry.toml
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── som
│ │ │ ├── __init__.py
│ │ │ ├── detect.py
│ │ │ ├── detection.py
│ │ │ ├── models.py
│ │ │ ├── ocr.py
│ │ │ ├── util
│ │ │ │ └── utils.py
│ │ │ └── visualization.py
│ │ └── tests
│ │ ├── conftest.py
│ │ └── test_omniparser.py
│ ├── qemu-docker
│ │ ├── linux
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── entry.sh
│ │ │ └── vm
│ │ │ ├── image
│ │ │ │ └── README.md
│ │ │ └── setup
│ │ │ ├── install.sh
│ │ │ ├── setup-cua-server.sh
│ │ │ └── setup.sh
│ │ ├── README.md
│ │ └── windows
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ └── src
│ │ ├── entry.sh
│ │ └── vm
│ │ ├── image
│ │ │ └── README.md
│ │ └── setup
│ │ ├── install.bat
│ │ ├── on-logon.ps1
│ │ ├── setup-cua-server.ps1
│ │ ├── setup-utils.psm1
│ │ └── setup.ps1
│ ├── typescript
│ │ ├── .gitignore
│ │ ├── .nvmrc
│ │ ├── agent
│ │ │ ├── examples
│ │ │ │ ├── playground-example.html
│ │ │ │ └── README.md
│ │ │ ├── package.json
│ │ │ ├── README.md
│ │ │ ├── src
│ │ │ │ ├── client.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── types.ts
│ │ │ ├── tests
│ │ │ │ └── client.test.ts
│ │ │ ├── tsconfig.json
│ │ │ ├── tsdown.config.ts
│ │ │ └── vitest.config.ts
│ │ ├── computer
│ │ │ ├── .editorconfig
│ │ │ ├── .gitattributes
│ │ │ ├── .gitignore
│ │ │ ├── LICENSE
│ │ │ ├── package.json
│ │ │ ├── README.md
│ │ │ ├── src
│ │ │ │ ├── computer
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── providers
│ │ │ │ │ │ ├── base.ts
│ │ │ │ │ │ ├── cloud.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── interface
│ │ │ │ │ ├── base.ts
│ │ │ │ │ ├── factory.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── linux.ts
│ │ │ │ │ ├── macos.ts
│ │ │ │ │ └── windows.ts
│ │ │ │ └── types.ts
│ │ │ ├── tests
│ │ │ │ ├── computer
│ │ │ │ │ └── cloud.test.ts
│ │ │ │ ├── interface
│ │ │ │ │ ├── factory.test.ts
│ │ │ │ │ ├── index.test.ts
│ │ │ │ │ ├── linux.test.ts
│ │ │ │ │ ├── macos.test.ts
│ │ │ │ │ └── windows.test.ts
│ │ │ │ └── setup.ts
│ │ │ ├── tsconfig.json
│ │ │ ├── tsdown.config.ts
│ │ │ └── vitest.config.ts
│ │ ├── core
│ │ │ ├── .editorconfig
│ │ │ ├── .gitattributes
│ │ │ ├── .gitignore
│ │ │ ├── LICENSE
│ │ │ ├── package.json
│ │ │ ├── README.md
│ │ │ ├── src
│ │ │ │ ├── index.ts
│ │ │ │ └── telemetry
│ │ │ │ ├── clients
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── posthog.ts
│ │ │ │ └── index.ts
│ │ │ ├── tests
│ │ │ │ └── telemetry.test.ts
│ │ │ ├── tsconfig.json
│ │ │ ├── tsdown.config.ts
│ │ │ └── vitest.config.ts
│ │ ├── cua-cli
│ │ │ ├── .gitignore
│ │ │ ├── .prettierrc
│ │ │ ├── bun.lock
│ │ │ ├── CLAUDE.md
│ │ │ ├── index.ts
│ │ │ ├── package.json
│ │ │ ├── README.md
│ │ │ ├── src
│ │ │ │ ├── auth.ts
│ │ │ │ ├── cli.ts
│ │ │ │ ├── commands
│ │ │ │ │ ├── auth.ts
│ │ │ │ │ └── sandbox.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── http.ts
│ │ │ │ ├── storage.ts
│ │ │ │ └── util.ts
│ │ │ └── tsconfig.json
│ │ ├── package.json
│ │ ├── pnpm-lock.yaml
│ │ ├── pnpm-workspace.yaml
│ │ └── README.md
│ └── xfce
│ ├── .dockerignore
│ ├── .gitignore
│ ├── Development.md
│ ├── Dockerfile
│ ├── Dockerfile.dev
│ ├── README.md
│ └── src
│ ├── scripts
│ │ ├── resize-display.sh
│ │ ├── start-computer-server.sh
│ │ ├── start-novnc.sh
│ │ ├── start-vnc.sh
│ │ └── xstartup.sh
│ ├── supervisor
│ │ └── supervisord.conf
│ └── xfce-config
│ ├── helpers.rc
│ ├── xfce4-power-manager.xml
│ └── xfce4-session.xml
├── LICENSE.md
├── Makefile
├── notebooks
│ ├── agent_nb.ipynb
│ ├── blog
│ │ ├── build-your-own-operator-on-macos-1.ipynb
│ │ └── build-your-own-operator-on-macos-2.ipynb
│ ├── composite_agents_docker_nb.ipynb
│ ├── computer_nb.ipynb
│ ├── computer_server_nb.ipynb
│ ├── customizing_computeragent.ipynb
│ ├── eval_osworld.ipynb
│ ├── ollama_nb.ipynb
│ ├── README.md
│ ├── sota_hackathon_cloud.ipynb
│ └── sota_hackathon.ipynb
├── package-lock.json
├── package.json
├── pnpm-lock.yaml
├── pyproject.toml
├── pyrightconfig.json
├── README.md
├── scripts
│ ├── install-cli.ps1
│ ├── install-cli.sh
│ ├── playground-docker.sh
│ ├── playground.sh
│ ├── run-docker-dev.sh
│ └── typescript-typecheck.js
├── TESTING.md
├── tests
│ ├── agent_loop_testing
│ │ ├── agent_test.py
│ │ └── README.md
│ ├── pytest.ini
│ ├── shell_cmd.py
│ ├── test_files.py
│ ├── test_mcp_server_session_management.py
│ ├── test_mcp_server_streaming.py
│ ├── test_shell_bash.py
│ ├── test_telemetry.py
│ ├── test_tracing.py
│ ├── test_venv.py
│ └── test_watchdog.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/libs/typescript/computer/src/interface/macos.ts:
--------------------------------------------------------------------------------
```typescript
/**
* macOS computer interface implementation.
*/
import type { ScreenSize } from '../types';
import type { AccessibilityNode, CursorPosition, MouseButton } from './base';
import { BaseComputerInterface } from './base';
export class MacOSComputerInterface extends BaseComputerInterface {
// Mouse Actions
/**
* Press and hold a mouse button at the specified coordinates.
* @param {number} [x] - X coordinate for the mouse action
* @param {number} [y] - Y coordinate for the mouse action
* @param {MouseButton} [button='left'] - Mouse button to press down
* @returns {Promise<void>}
*/
async mouseDown(x?: number, y?: number, button: MouseButton = 'left'): Promise<void> {
await this.sendCommand('mouse_down', { x, y, button });
}
/**
* Release a mouse button at the specified coordinates.
* @param {number} [x] - X coordinate for the mouse action
* @param {number} [y] - Y coordinate for the mouse action
* @param {MouseButton} [button='left'] - Mouse button to release
* @returns {Promise<void>}
*/
async mouseUp(x?: number, y?: number, button: MouseButton = 'left'): Promise<void> {
await this.sendCommand('mouse_up', { x, y, button });
}
/**
* Perform a left mouse click at the specified coordinates.
* @param {number} [x] - X coordinate for the click
* @param {number} [y] - Y coordinate for the click
* @returns {Promise<void>}
*/
async leftClick(x?: number, y?: number): Promise<void> {
await this.sendCommand('left_click', { x, y });
}
/**
* Perform a right mouse click at the specified coordinates.
* @param {number} [x] - X coordinate for the click
* @param {number} [y] - Y coordinate for the click
* @returns {Promise<void>}
*/
async rightClick(x?: number, y?: number): Promise<void> {
await this.sendCommand('right_click', { x, y });
}
/**
* Perform a double click at the specified coordinates.
* @param {number} [x] - X coordinate for the double click
* @param {number} [y] - Y coordinate for the double click
* @returns {Promise<void>}
*/
async doubleClick(x?: number, y?: number): Promise<void> {
await this.sendCommand('double_click', { x, y });
}
/**
* Move the cursor to the specified coordinates.
* @param {number} x - X coordinate to move to
* @param {number} y - Y coordinate to move to
* @returns {Promise<void>}
*/
async moveCursor(x: number, y: number): Promise<void> {
await this.sendCommand('move_cursor', { x, y });
}
/**
* Drag from current position to the specified coordinates.
* @param {number} x - X coordinate to drag to
* @param {number} y - Y coordinate to drag to
* @param {MouseButton} [button='left'] - Mouse button to use for dragging
* @param {number} [duration=0.5] - Duration of the drag operation in seconds
* @returns {Promise<void>}
*/
async dragTo(x: number, y: number, button: MouseButton = 'left', duration = 0.5): Promise<void> {
await this.sendCommand('drag_to', { x, y, button, duration });
}
/**
* Drag along a path of coordinates.
* @param {Array<[number, number]>} path - Array of [x, y] coordinate pairs to drag through
* @param {MouseButton} [button='left'] - Mouse button to use for dragging
* @param {number} [duration=0.5] - Duration of the drag operation in seconds
* @returns {Promise<void>}
*/
async drag(
path: Array<[number, number]>,
button: MouseButton = 'left',
duration = 0.5
): Promise<void> {
await this.sendCommand('drag', { path, button, duration });
}
// Keyboard Actions
/**
* Press and hold a key.
* @param {string} key - Key to press down
* @returns {Promise<void>}
*/
async keyDown(key: string): Promise<void> {
await this.sendCommand('key_down', { key });
}
/**
* Release a key.
* @param {string} key - Key to release
* @returns {Promise<void>}
*/
async keyUp(key: string): Promise<void> {
await this.sendCommand('key_up', { key });
}
/**
* Type text as if entered from keyboard.
* @param {string} text - Text to type
* @returns {Promise<void>}
*/
async typeText(text: string): Promise<void> {
await this.sendCommand('type_text', { text });
}
/**
* Press and release a key.
* @param {string} key - Key to press
* @returns {Promise<void>}
*/
async pressKey(key: string): Promise<void> {
await this.sendCommand('press_key', { key });
}
/**
* Press multiple keys simultaneously as a hotkey combination.
* @param {...string} keys - Keys to press together
* @returns {Promise<void>}
*/
async hotkey(...keys: string[]): Promise<void> {
await this.sendCommand('hotkey', { keys });
}
// Scrolling Actions
/**
* Scroll by the specified amount in x and y directions.
* @param {number} x - Horizontal scroll amount
* @param {number} y - Vertical scroll amount
* @returns {Promise<void>}
*/
async scroll(x: number, y: number): Promise<void> {
await this.sendCommand('scroll', { x, y });
}
/**
* Scroll down by the specified number of clicks.
* @param {number} [clicks=1] - Number of scroll clicks
* @returns {Promise<void>}
*/
async scrollDown(clicks = 1): Promise<void> {
await this.sendCommand('scroll_down', { clicks });
}
/**
* Scroll up by the specified number of clicks.
* @param {number} [clicks=1] - Number of scroll clicks
* @returns {Promise<void>}
*/
async scrollUp(clicks = 1): Promise<void> {
await this.sendCommand('scroll_up', { clicks });
}
// Screen Actions
/**
* Take a screenshot of the screen.
* @returns {Promise<Buffer>} Screenshot image data as a Buffer
* @throws {Error} If screenshot fails
*/
async screenshot(): Promise<Buffer> {
const response = await this.sendCommand('screenshot');
if (!response.image_data) {
throw new Error('Failed to take screenshot');
}
return Buffer.from(response.image_data as string, 'base64');
}
/**
* Get the current screen size.
* @returns {Promise<ScreenSize>} Screen dimensions
* @throws {Error} If unable to get screen size
*/
async getScreenSize(): Promise<ScreenSize> {
const response = await this.sendCommand('get_screen_size');
if (!response.success || !response.size) {
throw new Error('Failed to get screen size');
}
return response.size as ScreenSize;
}
/**
* Get the current cursor position.
* @returns {Promise<CursorPosition>} Current cursor coordinates
* @throws {Error} If unable to get cursor position
*/
async getCursorPosition(): Promise<CursorPosition> {
const response = await this.sendCommand('get_cursor_position');
if (!response.success || !response.position) {
throw new Error('Failed to get cursor position');
}
return response.position as CursorPosition;
}
// Window Management
/** Open a file path or URL with the default handler. */
async open(target: string): Promise<void> {
const response = await this.sendCommand('open', { target });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to open target');
}
}
/** Launch an application (string may include args). Returns pid if available. */
async launch(app: string, args?: string[]): Promise<number | undefined> {
const response = await this.sendCommand('launch', args ? { app, args } : { app });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to launch application');
}
return (response.pid as number) || undefined;
}
/** Get the current active window id. */
async getCurrentWindowId(): Promise<number | string> {
const response = await this.sendCommand('get_current_window_id');
if (!response.success || response.window_id === undefined) {
throw new Error((response.error as string) || 'Failed to get current window id');
}
return response.window_id as number | string;
}
/** Get windows belonging to an application (by name). */
async getApplicationWindows(app: string): Promise<Array<number | string>> {
const response = await this.sendCommand('get_application_windows', { app });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to get application windows');
}
return (response.windows as Array<number | string>) || [];
}
/** Get window title/name by id. */
async getWindowName(windowId: number | string): Promise<string> {
const response = await this.sendCommand('get_window_name', { window_id: windowId });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to get window name');
}
return (response.name as string) || '';
}
/** Get window size as [width, height]. */
async getWindowSize(windowId: number | string): Promise<[number, number]> {
const response = await this.sendCommand('get_window_size', { window_id: windowId });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to get window size');
}
return [Number(response.width) || 0, Number(response.height) || 0];
}
/** Get window position as [x, y]. */
async getWindowPosition(windowId: number | string): Promise<[number, number]> {
const response = await this.sendCommand('get_window_position', { window_id: windowId });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to get window position');
}
return [Number(response.x) || 0, Number(response.y) || 0];
}
/** Set window size. */
async setWindowSize(windowId: number | string, width: number, height: number): Promise<void> {
const response = await this.sendCommand('set_window_size', {
window_id: windowId,
width,
height,
});
if (!response.success) {
throw new Error((response.error as string) || 'Failed to set window size');
}
}
/** Set window position. */
async setWindowPosition(windowId: number | string, x: number, y: number): Promise<void> {
const response = await this.sendCommand('set_window_position', {
window_id: windowId,
x,
y,
});
if (!response.success) {
throw new Error((response.error as string) || 'Failed to set window position');
}
}
/** Maximize a window. */
async maximizeWindow(windowId: number | string): Promise<void> {
const response = await this.sendCommand('maximize_window', { window_id: windowId });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to maximize window');
}
}
/** Minimize a window. */
async minimizeWindow(windowId: number | string): Promise<void> {
const response = await this.sendCommand('minimize_window', { window_id: windowId });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to minimize window');
}
}
/** Activate a window by id. */
async activateWindow(windowId: number | string): Promise<void> {
const response = await this.sendCommand('activate_window', { window_id: windowId });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to activate window');
}
}
/** Close a window by id. */
async closeWindow(windowId: number | string): Promise<void> {
const response = await this.sendCommand('close_window', { window_id: windowId });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to close window');
}
}
// Desktop Actions
/**
* Get the current desktop environment string (e.g., 'xfce4', 'gnome', 'kde', 'mac', 'windows').
*/
async getDesktopEnvironment(): Promise<string> {
const response = await this.sendCommand('get_desktop_environment');
if (!response.success) {
throw new Error((response.error as string) || 'Failed to get desktop environment');
}
return (response.environment as string) || 'unknown';
}
/**
* Set the desktop wallpaper image.
* @param path Absolute path to the image file on the VM
*/
async setWallpaper(path: string): Promise<void> {
const response = await this.sendCommand('set_wallpaper', { path });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to set wallpaper');
}
}
// Clipboard Actions
/**
* Copy current selection to clipboard and return the content.
* @returns {Promise<string>} Clipboard content
* @throws {Error} If unable to get clipboard content
*/
async copyToClipboard(): Promise<string> {
const response = await this.sendCommand('copy_to_clipboard');
if (!response.success || !response.content) {
throw new Error('Failed to get clipboard content');
}
return response.content as string;
}
/**
* Set the clipboard content to the specified text.
* @param {string} text - Text to set in clipboard
* @returns {Promise<void>}
*/
async setClipboard(text: string): Promise<void> {
await this.sendCommand('set_clipboard', { text });
}
// File System Actions
/**
* Check if a file exists at the specified path.
* @param {string} path - Path to the file
* @returns {Promise<boolean>} True if file exists, false otherwise
*/
async fileExists(path: string): Promise<boolean> {
const response = await this.sendCommand('file_exists', { path });
return (response.exists as boolean) || false;
}
/**
* Check if a directory exists at the specified path.
* @param {string} path - Path to the directory
* @returns {Promise<boolean>} True if directory exists, false otherwise
*/
async directoryExists(path: string): Promise<boolean> {
const response = await this.sendCommand('directory_exists', { path });
return (response.exists as boolean) || false;
}
/**
* List the contents of a directory.
* @param {string} path - Path to the directory
* @returns {Promise<string[]>} Array of file and directory names
* @throws {Error} If unable to list directory
*/
async listDir(path: string): Promise<string[]> {
const response = await this.sendCommand('list_dir', { path });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to list directory');
}
return (response.files as string[]) || [];
}
/**
* Get the size of a file in bytes.
* @param {string} path - Path to the file
* @returns {Promise<number>} File size in bytes
* @throws {Error} If unable to get file size
*/
async getFileSize(path: string): Promise<number> {
const response = await this.sendCommand('get_file_size', { path });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to get file size');
}
return (response.size as number) || 0;
}
/**
* Read file content in chunks for large files.
* @private
* @param {string} path - Path to the file
* @param {number} offset - Starting byte offset
* @param {number} totalLength - Total number of bytes to read
* @param {number} [chunkSize=1048576] - Size of each chunk in bytes
* @returns {Promise<Buffer>} File content as Buffer
* @throws {Error} If unable to read file chunk
*/
private async readBytesChunked(
path: string,
offset: number,
totalLength: number,
chunkSize: number = 1024 * 1024
): Promise<Buffer> {
const chunks: Buffer[] = [];
let currentOffset = offset;
let remaining = totalLength;
while (remaining > 0) {
const readSize = Math.min(chunkSize, remaining);
const response = await this.sendCommand('read_bytes', {
path,
offset: currentOffset,
length: readSize,
});
if (!response.success) {
throw new Error((response.error as string) || 'Failed to read file chunk');
}
const chunkData = Buffer.from(response.content_b64 as string, 'base64');
chunks.push(chunkData);
currentOffset += readSize;
remaining -= readSize;
}
return Buffer.concat(chunks);
}
/**
* Write file content in chunks for large files.
* @private
* @param {string} path - Path to the file
* @param {Buffer} content - Content to write
* @param {boolean} [append=false] - Whether to append to existing file
* @param {number} [chunkSize=1048576] - Size of each chunk in bytes
* @returns {Promise<void>}
* @throws {Error} If unable to write file chunk
*/
private async writeBytesChunked(
path: string,
content: Buffer,
append: boolean = false,
chunkSize: number = 1024 * 1024
): Promise<void> {
const totalSize = content.length;
let currentOffset = 0;
while (currentOffset < totalSize) {
const chunkEnd = Math.min(currentOffset + chunkSize, totalSize);
const chunkData = content.subarray(currentOffset, chunkEnd);
// First chunk uses the original append flag, subsequent chunks always append
const chunkAppend = currentOffset === 0 ? append : true;
const response = await this.sendCommand('write_bytes', {
path,
content_b64: chunkData.toString('base64'),
append: chunkAppend,
});
if (!response.success) {
throw new Error((response.error as string) || 'Failed to write file chunk');
}
currentOffset = chunkEnd;
}
}
/**
* Read text from a file with specified encoding.
* @param {string} path - Path to the file to read
* @param {BufferEncoding} [encoding='utf8'] - Text encoding to use
* @returns {Promise<string>} The decoded text content of the file
*/
async readText(path: string, encoding: BufferEncoding = 'utf8'): Promise<string> {
const contentBytes = await this.readBytes(path);
return contentBytes.toString(encoding);
}
/**
* Write text to a file with specified encoding.
* @param {string} path - Path to the file to write
* @param {string} content - Text content to write
* @param {BufferEncoding} [encoding='utf8'] - Text encoding to use
* @param {boolean} [append=false] - Whether to append to the file instead of overwriting
* @returns {Promise<void>}
*/
async writeText(
path: string,
content: string,
encoding: BufferEncoding = 'utf8',
append: boolean = false
): Promise<void> {
const contentBytes = Buffer.from(content, encoding);
await this.writeBytes(path, contentBytes, append);
}
/**
* Read bytes from a file, with optional offset and length.
* @param {string} path - Path to the file
* @param {number} [offset=0] - Starting byte offset
* @param {number} [length] - Number of bytes to read (reads entire file if not specified)
* @returns {Promise<Buffer>} File content as Buffer
* @throws {Error} If unable to read file
*/
async readBytes(path: string, offset: number = 0, length?: number): Promise<Buffer> {
// For large files, use chunked reading
if (length === undefined) {
// Get file size first to determine if we need chunking
const fileSize = await this.getFileSize(path);
// If file is larger than 5MB, read in chunks
if (fileSize > 5 * 1024 * 1024) {
const readLength = offset > 0 ? fileSize - offset : fileSize;
return await this.readBytesChunked(path, offset, readLength);
}
}
const response = await this.sendCommand('read_bytes', {
path,
offset,
length,
});
if (!response.success) {
throw new Error((response.error as string) || 'Failed to read file');
}
return Buffer.from(response.content_b64 as string, 'base64');
}
/**
* Write bytes to a file.
* @param {string} path - Path to the file
* @param {Buffer} content - Content to write as Buffer
* @param {boolean} [append=false] - Whether to append to existing file
* @returns {Promise<void>}
* @throws {Error} If unable to write file
*/
async writeBytes(path: string, content: Buffer, append: boolean = false): Promise<void> {
// For large files, use chunked writing
if (content.length > 5 * 1024 * 1024) {
// 5MB threshold
await this.writeBytesChunked(path, content, append);
return;
}
const response = await this.sendCommand('write_bytes', {
path,
content_b64: content.toString('base64'),
append,
});
if (!response.success) {
throw new Error((response.error as string) || 'Failed to write file');
}
}
/**
* Delete a file at the specified path.
* @param {string} path - Path to the file to delete
* @returns {Promise<void>}
* @throws {Error} If unable to delete file
*/
async deleteFile(path: string): Promise<void> {
const response = await this.sendCommand('delete_file', { path });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to delete file');
}
}
/**
* Create a directory at the specified path.
* @param {string} path - Path where to create the directory
* @returns {Promise<void>}
* @throws {Error} If unable to create directory
*/
async createDir(path: string): Promise<void> {
const response = await this.sendCommand('create_dir', { path });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to create directory');
}
}
/**
* Delete a directory at the specified path.
* @param {string} path - Path to the directory to delete
* @returns {Promise<void>}
* @throws {Error} If unable to delete directory
*/
async deleteDir(path: string): Promise<void> {
const response = await this.sendCommand('delete_dir', { path });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to delete directory');
}
}
/**
* Execute a shell command and return stdout and stderr.
* @param {string} command - Command to execute
* @returns {Promise<[string, string]>} Tuple of [stdout, stderr]
* @throws {Error} If command execution fails
*/
async runCommand(command: string): Promise<[string, string]> {
const response = await this.sendCommand('run_command', { command });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to run command');
}
return [(response.stdout as string) || '', (response.stderr as string) || ''];
}
// Accessibility Actions
/**
* Get the accessibility tree of the current screen.
* @returns {Promise<AccessibilityNode>} Root accessibility node
* @throws {Error} If unable to get accessibility tree
*/
async getAccessibilityTree(): Promise<AccessibilityNode> {
const response = await this.sendCommand('get_accessibility_tree');
if (!response.success) {
throw new Error((response.error as string) || 'Failed to get accessibility tree');
}
return response as unknown as AccessibilityNode;
}
/**
* Convert coordinates to screen coordinates.
* @param {number} x - X coordinate to convert
* @param {number} y - Y coordinate to convert
* @returns {Promise<[number, number]>} Converted screen coordinates as [x, y]
* @throws {Error} If coordinate conversion fails
*/
async toScreenCoordinates(x: number, y: number): Promise<[number, number]> {
const response = await this.sendCommand('to_screen_coordinates', { x, y });
if (!response.success || !response.coordinates) {
throw new Error('Failed to convert to screen coordinates');
}
return response.coordinates as [number, number];
}
/**
* Convert coordinates to screenshot coordinates.
* @param {number} x - X coordinate to convert
* @param {number} y - Y coordinate to convert
* @returns {Promise<[number, number]>} Converted screenshot coordinates as [x, y]
* @throws {Error} If coordinate conversion fails
*/
async toScreenshotCoordinates(x: number, y: number): Promise<[number, number]> {
const response = await this.sendCommand('to_screenshot_coordinates', {
x,
y,
});
if (!response.success || !response.coordinates) {
throw new Error('Failed to convert to screenshot coordinates');
}
return response.coordinates as [number, number];
}
}
```
--------------------------------------------------------------------------------
/libs/lume/src/Server/Server.swift:
--------------------------------------------------------------------------------
```swift
import Darwin
import Foundation
import Network
// MARK: - Error Types
enum PortError: Error, LocalizedError {
case alreadyInUse(port: UInt16)
var errorDescription: String? {
switch self {
case .alreadyInUse(let port):
return "Port \(port) is already in use by another process"
}
}
}
// MARK: - Server Class
@MainActor
final class Server {
// MARK: - Route Type
private struct Route {
let method: String
let path: String
let handler: (HTTPRequest) async throws -> HTTPResponse
func matches(_ request: HTTPRequest) -> Bool {
if method != request.method { return false }
// Handle path parameters
let routeParts = path.split(separator: "/")
let requestParts = request.path.split(separator: "/")
if routeParts.count != requestParts.count { return false }
for (routePart, requestPart) in zip(routeParts, requestParts) {
if routePart.hasPrefix(":") { continue } // Path parameter
if routePart != requestPart { return false }
}
return true
}
func extractParams(_ request: HTTPRequest) -> [String: String] {
var params: [String: String] = [:]
let routeParts = path.split(separator: "/")
// Split request path to remove query parameters
let requestPathOnly = request.path.split(separator: "?", maxSplits: 1)[0]
let requestParts = requestPathOnly.split(separator: "/")
for (routePart, requestPart) in zip(routeParts, requestParts) {
if routePart.hasPrefix(":") {
let paramName = String(routePart.dropFirst())
params[paramName] = String(requestPart)
}
}
return params
}
}
// MARK: - Properties
private let port: NWEndpoint.Port
private let controller: LumeController
private var isRunning = false
private var listener: NWListener?
private var routes: [Route]
// MARK: - Initialization
init(port: UInt16 = 7777) {
self.port = NWEndpoint.Port(rawValue: port)!
self.controller = LumeController()
self.routes = []
// Define API routes after self is fully initialized
self.setupRoutes()
}
// MARK: - Route Setup
private func setupRoutes() {
routes = [
Route(
method: "GET", path: "/lume/vms",
handler: { [weak self] request in
guard let self else { throw HTTPError.internalError }
// Extract storage from query params if present
let storage = self.extractQueryParam(request: request, name: "storage")
return try await self.handleListVMs(storage: storage)
}),
Route(
method: "GET", path: "/lume/vms/:name",
handler: { [weak self] request in
guard let self else { throw HTTPError.internalError }
let params = Route(
method: "GET", path: "/lume/vms/:name",
handler: { _ in
HTTPResponse(statusCode: .ok, body: "")
}
).extractParams(request)
guard let name = params["name"] else {
return HTTPResponse(statusCode: .badRequest, body: "Missing VM name")
}
// Extract storage from query params if present
let storage = self.extractQueryParam(request: request, name: "storage")
return try await self.handleGetVM(name: name, storage: storage)
}),
Route(
method: "DELETE", path: "/lume/vms/:name",
handler: { [weak self] request in
guard let self else { throw HTTPError.internalError }
let params = Route(
method: "DELETE", path: "/lume/vms/:name",
handler: { _ in
HTTPResponse(statusCode: .ok, body: "")
}
).extractParams(request)
guard let name = params["name"] else {
return HTTPResponse(statusCode: .badRequest, body: "Missing VM name")
}
// Extract storage from query params if present
let storage = self.extractQueryParam(request: request, name: "storage")
return try await self.handleDeleteVM(name: name, storage: storage)
}),
Route(
method: "POST", path: "/lume/vms",
handler: { [weak self] request in
guard let self else { throw HTTPError.internalError }
return try await self.handleCreateVM(request.body)
}),
Route(
method: "POST", path: "/lume/vms/clone",
handler: { [weak self] request in
guard let self else { throw HTTPError.internalError }
return try await self.handleCloneVM(request.body)
}),
Route(
method: "PATCH", path: "/lume/vms/:name",
handler: { [weak self] request in
guard let self else { throw HTTPError.internalError }
let params = Route(
method: "PATCH", path: "/lume/vms/:name",
handler: { _ in
HTTPResponse(statusCode: .ok, body: "")
}
).extractParams(request)
guard let name = params["name"] else {
return HTTPResponse(statusCode: .badRequest, body: "Missing VM name")
}
return try await self.handleSetVM(name: name, body: request.body)
}),
Route(
method: "POST", path: "/lume/vms/:name/run",
handler: { [weak self] request in
guard let self else { throw HTTPError.internalError }
let params = Route(
method: "POST", path: "/lume/vms/:name/run",
handler: { _ in
HTTPResponse(statusCode: .ok, body: "")
}
).extractParams(request)
guard let name = params["name"] else {
return HTTPResponse(statusCode: .badRequest, body: "Missing VM name")
}
return try await self.handleRunVM(name: name, body: request.body)
}),
Route(
method: "POST", path: "/lume/vms/:name/stop",
handler: { [weak self] request in
guard let self else { throw HTTPError.internalError }
let params = Route(
method: "POST", path: "/lume/vms/:name/stop",
handler: { _ in
HTTPResponse(statusCode: .ok, body: "")
}
).extractParams(request)
guard let name = params["name"] else {
return HTTPResponse(statusCode: .badRequest, body: "Missing VM name")
}
Logger.info("Processing stop VM request", metadata: ["method": request.method, "path": request.path])
// Extract storage from the request body
var storage: String? = nil
if let bodyData = request.body, !bodyData.isEmpty {
do {
if let json = try JSONSerialization.jsonObject(with: bodyData) as? [String: Any],
let bodyStorage = json["storage"] as? String {
storage = bodyStorage
Logger.info("Extracted storage from request body", metadata: ["storage": bodyStorage])
}
} catch {
Logger.error("Failed to parse request body JSON", metadata: ["error": error.localizedDescription])
}
}
return try await self.handleStopVM(name: name, storage: storage)
}),
Route(
method: "GET", path: "/lume/ipsw",
handler: { [weak self] _ in
guard let self else { throw HTTPError.internalError }
return try await self.handleIPSW()
}),
Route(
method: "POST", path: "/lume/pull",
handler: { [weak self] request in
guard let self else { throw HTTPError.internalError }
return try await self.handlePull(request.body)
}),
Route(
method: "POST", path: "/lume/prune",
handler: { [weak self] _ in
guard let self else { throw HTTPError.internalError }
return try await self.handlePruneImages()
}),
Route(
method: "GET", path: "/lume/images",
handler: { [weak self] request in
guard let self else { throw HTTPError.internalError }
return try await self.handleGetImages(request)
}),
// New config endpoint
Route(
method: "GET", path: "/lume/config",
handler: { [weak self] _ in
guard let self else { throw HTTPError.internalError }
return try await self.handleGetConfig()
}),
Route(
method: "POST", path: "/lume/config",
handler: { [weak self] request in
guard let self else { throw HTTPError.internalError }
return try await self.handleUpdateConfig(request.body)
}),
Route(
method: "GET", path: "/lume/config/locations",
handler: { [weak self] _ in
guard let self else { throw HTTPError.internalError }
return try await self.handleGetLocations()
}),
Route(
method: "POST", path: "/lume/config/locations",
handler: { [weak self] request in
guard let self else { throw HTTPError.internalError }
return try await self.handleAddLocation(request.body)
}),
Route(
method: "DELETE", path: "/lume/config/locations/:name",
handler: { [weak self] request in
guard let self else { throw HTTPError.internalError }
let params = Route(
method: "DELETE", path: "/lume/config/locations/:name",
handler: { _ in
HTTPResponse(statusCode: .ok, body: "")
}
).extractParams(request)
guard let name = params["name"] else {
return HTTPResponse(statusCode: .badRequest, body: "Missing location name")
}
return try await self.handleRemoveLocation(name)
}),
// Logs retrieval route
Route(
method: "GET", path: "/lume/logs",
handler: { [weak self] request in
guard let self else { throw HTTPError.internalError }
// Extract query parameters
let type = self.extractQueryParam(request: request, name: "type") // "info", "error", or "all"
let linesParam = self.extractQueryParam(request: request, name: "lines")
let lines = linesParam.flatMap { Int($0) } // Convert to Int if present
return try await self.handleGetLogs(type: type, lines: lines)
}),
Route(
method: "POST", path: "/lume/config/locations/default/:name",
handler: { [weak self] request in
guard let self else { throw HTTPError.internalError }
let params = Route(
method: "POST", path: "/lume/config/locations/default/:name",
handler: { _ in
HTTPResponse(statusCode: .ok, body: "")
}
).extractParams(request)
guard let name = params["name"] else {
return HTTPResponse(statusCode: .badRequest, body: "Missing location name")
}
return try await self.handleSetDefaultLocation(name)
}),
Route(
method: "POST", path: "/lume/vms/push",
handler: { [weak self] request in
guard let self else { throw HTTPError.internalError }
return try await self.handlePush(request.body)
}),
]
}
// Helper to extract query parameters from the URL
private func extractQueryParam(request: HTTPRequest, name: String) -> String? {
// Extract only the query part by splitting on '?'
let parts = request.path.split(separator: "?", maxSplits: 1)
guard parts.count > 1 else { return nil } // No query parameters
let queryString = String(parts[1])
// Create a placeholder URL with the query string
if let urlComponents = URLComponents(string: "http://placeholder.com?"+queryString),
let queryItems = urlComponents.queryItems
{
return queryItems.first(where: { $0.name == name })?.value?.removingPercentEncoding
}
return nil
}
// MARK: - Port Utilities
private func isPortAvailable(port: Int) async -> Bool {
// Create a socket
let socketFD = socket(AF_INET, SOCK_STREAM, 0)
if socketFD == -1 {
return false
}
// Set socket options to allow reuse
var value: Int32 = 1
if setsockopt(
socketFD, SOL_SOCKET, SO_REUSEADDR, &value, socklen_t(MemoryLayout<Int32>.size)) == -1
{
close(socketFD)
return false
}
// Set up the address structure
var addr = sockaddr_in()
addr.sin_family = sa_family_t(AF_INET)
addr.sin_port = UInt16(port).bigEndian
addr.sin_addr.s_addr = INADDR_ANY.bigEndian
// Bind to the port
let bindResult = withUnsafePointer(to: &addr) { addrPtr in
addrPtr.withMemoryRebound(to: sockaddr.self, capacity: 1) { addrPtr in
Darwin.bind(socketFD, addrPtr, socklen_t(MemoryLayout<sockaddr_in>.size))
}
}
// Clean up
close(socketFD)
// If bind failed, the port is in use
return bindResult == 0
}
// MARK: - Server Lifecycle
func start() async throws {
// First check if the port is already in use
if !(await isPortAvailable(port: Int(port.rawValue))) {
// Don't log anything here, just throw the error
throw PortError.alreadyInUse(port: port.rawValue)
}
let parameters = NWParameters.tcp
listener = try NWListener(using: parameters, on: port)
// Create an actor to safely manage state transitions
actor StartupState {
var error: Error?
var isComplete = false
func setError(_ error: Error) {
self.error = error
self.isComplete = true
}
func setComplete() {
self.isComplete = true
}
func checkStatus() -> (isComplete: Bool, error: Error?) {
return (isComplete, error)
}
}
let startupState = StartupState()
// Set up a state update handler to detect port binding errors
listener?.stateUpdateHandler = { state in
Task {
switch state {
case .setup:
// Initial state, no action needed
Logger.info("Listener setup", metadata: ["port": "\(self.port.rawValue)"])
break
case .waiting(let error):
// Log the full error details to see what we're getting
Logger.error(
"Listener waiting",
metadata: [
"error": error.localizedDescription,
"debugDescription": error.debugDescription,
"localizedDescription": error.localizedDescription,
"port": "\(self.port.rawValue)",
])
// Check for different port in use error messages
if error.debugDescription.contains("Address already in use")
|| error.localizedDescription.contains("in use")
|| error.localizedDescription.contains("address already in use")
{
Logger.error(
"Port conflict detected", metadata: ["port": "\(self.port.rawValue)"])
await startupState.setError(
PortError.alreadyInUse(port: self.port.rawValue))
} else {
// Wait for a short period to see if the listener recovers
// Some network errors are transient
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
// If we're still waiting after delay, consider it an error
if case .waiting = await self.listener?.state {
await startupState.setError(error)
}
}
case .failed(let error):
// Log the full error details
Logger.error(
"Listener failed",
metadata: [
"error": error.localizedDescription,
"debugDescription": error.debugDescription,
"port": "\(self.port.rawValue)",
])
await startupState.setError(error)
case .ready:
// Listener successfully bound to port
Logger.info("Listener ready", metadata: ["port": "\(self.port.rawValue)"])
await startupState.setComplete()
case .cancelled:
// Listener was cancelled
Logger.info("Listener cancelled", metadata: ["port": "\(self.port.rawValue)"])
break
@unknown default:
Logger.info(
"Unknown listener state",
metadata: ["state": "\(state)", "port": "\(self.port.rawValue)"])
break
}
}
}
listener?.newConnectionHandler = { [weak self] connection in
Task { @MainActor [weak self] in
guard let self else { return }
self.handleConnection(connection)
}
}
listener?.start(queue: .main)
// Wait for either successful startup or an error
var status: (isComplete: Bool, error: Error?) = (false, nil)
repeat {
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
status = await startupState.checkStatus()
} while !status.isComplete
// If there was a startup error, throw it
if let error = status.error {
self.stop()
throw error
}
isRunning = true
Logger.info("Server started", metadata: ["port": "\(port.rawValue)"])
// Keep the server running
while isRunning {
try await Task.sleep(nanoseconds: 1_000_000_000)
}
}
func stop() {
isRunning = false
listener?.cancel()
}
// MARK: - Connection Handling
private func handleConnection(_ connection: NWConnection) {
connection.stateUpdateHandler = { [weak self] state in
switch state {
case .ready:
Task { @MainActor [weak self] in
guard let self else { return }
self.receiveData(connection)
}
case .failed(let error):
Logger.error("Connection failed", metadata: ["error": error.localizedDescription])
connection.cancel()
case .cancelled:
// Connection is already cancelled, no need to cancel again
break
default:
break
}
}
connection.start(queue: .main)
}
private func receiveData(_ connection: NWConnection) {
connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) {
[weak self] content, _, isComplete, error in
if let error = error {
Logger.error("Receive error", metadata: ["error": error.localizedDescription])
connection.cancel()
return
}
guard let data = content, !data.isEmpty else {
if isComplete {
connection.cancel()
}
return
}
Task { @MainActor [weak self] in
guard let self else { return }
do {
let response = try await self.handleRequest(data)
self.send(response, on: connection)
} catch {
let errorResponse = self.errorResponse(error)
self.send(errorResponse, on: connection)
}
}
}
}
private func send(_ response: HTTPResponse, on connection: NWConnection) {
let data = response.serialize()
Logger.info(
"Serialized response", metadata: ["data": String(data: data, encoding: .utf8) ?? ""])
connection.send(
content: data,
completion: .contentProcessed { [weak connection] error in
if let error = error {
Logger.error(
"Failed to send response", metadata: ["error": error.localizedDescription])
} else {
Logger.info("Response sent successfully")
}
if connection?.state != .cancelled {
connection?.cancel()
}
})
}
// MARK: - Request Handling
private func handleRequest(_ data: Data) async throws -> HTTPResponse {
Logger.info(
"Received request data", metadata: ["data": String(data: data, encoding: .utf8) ?? ""])
guard let request = HTTPRequest(data: data) else {
Logger.error("Failed to parse request")
return HTTPResponse(statusCode: .badRequest, body: "Invalid request")
}
Logger.info(
"Parsed request",
metadata: [
"method": request.method,
"path": request.path,
"headers": "\(request.headers)",
"body": String(data: request.body ?? Data(), encoding: .utf8) ?? "",
])
// Find matching route
guard let route = routes.first(where: { $0.matches(request) }) else {
return HTTPResponse(statusCode: .notFound, body: "Not found")
}
// Handle the request
let response = try await route.handler(request)
Logger.info(
"Sending response",
metadata: [
"statusCode": "\(response.statusCode.rawValue)",
"headers": "\(response.headers)",
"body": String(data: response.body ?? Data(), encoding: .utf8) ?? "",
])
return response
}
private func errorResponse(_ error: Error) -> HTTPResponse {
HTTPResponse(
statusCode: .internalServerError,
headers: ["Content-Type": "application/json"],
body: try! JSONEncoder().encode(APIError(message: error.localizedDescription))
)
}
}
```
--------------------------------------------------------------------------------
/libs/python/computer-server/computer_server/handlers/windows.py:
--------------------------------------------------------------------------------
```python
"""
Windows implementation of automation and accessibility handlers.
This implementation uses pyautogui for GUI automation and Windows-specific APIs
for accessibility and system operations.
"""
import asyncio
import base64
import logging
import os
import subprocess
from io import BytesIO
from typing import Any, Dict, List, Optional, Tuple
from pynput.keyboard import Controller as KeyboardController
from pynput.mouse import Controller as MouseController
# Configure logger
logger = logging.getLogger(__name__)
# Try to import pyautogui
try:
import pyautogui
pyautogui.FAILSAFE = False
logger.info("pyautogui successfully imported, GUI automation available")
except Exception as e:
logger.error(f"pyautogui import failed: {str(e)}. GUI operations will not work.")
pyautogui = None
# Try to import Windows-specific modules
try:
import win32api
import win32con
import win32gui
logger.info("Windows API modules successfully imported")
WINDOWS_API_AVAILABLE = True
except Exception as e:
logger.error(
f"Windows API modules import failed: {str(e)}. Some Windows-specific features will be unavailable."
)
WINDOWS_API_AVAILABLE = False
from .base import BaseAccessibilityHandler, BaseAutomationHandler
class WindowsAccessibilityHandler(BaseAccessibilityHandler):
"""Windows implementation of accessibility handler."""
async def get_accessibility_tree(self) -> Dict[str, Any]:
"""Get the accessibility tree of the current window.
Returns:
Dict[str, Any]: A dictionary containing the success status and either
the accessibility tree or an error message.
Structure: {"success": bool, "tree": dict} or
{"success": bool, "error": str}
"""
if not WINDOWS_API_AVAILABLE:
return {"success": False, "error": "Windows API not available"}
try:
# Get the foreground window
hwnd = win32gui.GetForegroundWindow()
if not hwnd:
return {"success": False, "error": "No foreground window found"}
# Get window information
window_text = win32gui.GetWindowText(hwnd)
rect = win32gui.GetWindowRect(hwnd)
tree = {
"role": "Window",
"title": window_text,
"position": {"x": rect[0], "y": rect[1]},
"size": {"width": rect[2] - rect[0], "height": rect[3] - rect[1]},
"children": [],
}
# Enumerate child windows
def enum_child_proc(hwnd_child, children_list):
"""Callback function to enumerate child windows and collect their information.
Args:
hwnd_child: Handle to the child window being enumerated.
children_list: List to append child window information to.
Returns:
bool: True to continue enumeration, False to stop.
"""
try:
child_text = win32gui.GetWindowText(hwnd_child)
child_rect = win32gui.GetWindowRect(hwnd_child)
child_class = win32gui.GetClassName(hwnd_child)
child_info = {
"role": child_class,
"title": child_text,
"position": {"x": child_rect[0], "y": child_rect[1]},
"size": {
"width": child_rect[2] - child_rect[0],
"height": child_rect[3] - child_rect[1],
},
"children": [],
}
children_list.append(child_info)
except Exception as e:
logger.debug(f"Error getting child window info: {e}")
return True
win32gui.EnumChildWindows(hwnd, enum_child_proc, tree["children"])
return {"success": True, "tree": tree}
except Exception as e:
logger.error(f"Error getting accessibility tree: {e}")
return {"success": False, "error": str(e)}
async def find_element(
self, role: Optional[str] = None, title: Optional[str] = None, value: Optional[str] = None
) -> Dict[str, Any]:
"""Find an element in the accessibility tree by criteria.
Args:
role (Optional[str]): The role or class name of the element to find.
title (Optional[str]): The title or text of the element to find.
value (Optional[str]): The value of the element (not used in Windows implementation).
Returns:
Dict[str, Any]: A dictionary containing the success status and either
the found element or an error message.
Structure: {"success": bool, "element": dict} or
{"success": bool, "error": str}
"""
if not WINDOWS_API_AVAILABLE:
return {"success": False, "error": "Windows API not available"}
try:
# Find window by title if specified
if title:
hwnd = win32gui.FindWindow(None, title)
if hwnd:
rect = win32gui.GetWindowRect(hwnd)
return {
"success": True,
"element": {
"role": "Window",
"title": title,
"position": {"x": rect[0], "y": rect[1]},
"size": {"width": rect[2] - rect[0], "height": rect[3] - rect[1]},
},
}
# Find window by class name if role is specified
if role:
hwnd = win32gui.FindWindow(role, None)
if hwnd:
window_text = win32gui.GetWindowText(hwnd)
rect = win32gui.GetWindowRect(hwnd)
return {
"success": True,
"element": {
"role": role,
"title": window_text,
"position": {"x": rect[0], "y": rect[1]},
"size": {"width": rect[2] - rect[0], "height": rect[3] - rect[1]},
},
}
return {"success": False, "error": "Element not found"}
except Exception as e:
logger.error(f"Error finding element: {e}")
return {"success": False, "error": str(e)}
class WindowsAutomationHandler(BaseAutomationHandler):
"""Windows implementation of automation handler using pyautogui and Windows APIs."""
mouse = MouseController()
keyboard = KeyboardController()
# Mouse Actions
async def mouse_down(
self, x: Optional[int] = None, y: Optional[int] = None, button: str = "left"
) -> Dict[str, Any]:
"""Press and hold a mouse button at the specified coordinates.
Args:
x (Optional[int]): The x-coordinate to move to before pressing. If None, uses current position.
y (Optional[int]): The y-coordinate to move to before pressing. If None, uses current position.
button (str): The mouse button to press ("left", "right", or "middle").
Returns:
Dict[str, Any]: A dictionary with success status and optional error message.
"""
if not pyautogui:
return {"success": False, "error": "pyautogui not available"}
try:
if x is not None and y is not None:
pyautogui.moveTo(x, y)
pyautogui.mouseDown(button=button)
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
async def mouse_up(
self, x: Optional[int] = None, y: Optional[int] = None, button: str = "left"
) -> Dict[str, Any]:
"""Release a mouse button at the specified coordinates.
Args:
x (Optional[int]): The x-coordinate to move to before releasing. If None, uses current position.
y (Optional[int]): The y-coordinate to move to before releasing. If None, uses current position.
button (str): The mouse button to release ("left", "right", or "middle").
Returns:
Dict[str, Any]: A dictionary with success status and optional error message.
"""
if not pyautogui:
return {"success": False, "error": "pyautogui not available"}
try:
if x is not None and y is not None:
pyautogui.moveTo(x, y)
pyautogui.mouseUp(button=button)
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
async def move_cursor(self, x: int, y: int) -> Dict[str, Any]:
"""Move the mouse cursor to the specified coordinates.
Args:
x (int): The x-coordinate to move to.
y (int): The y-coordinate to move to.
Returns:
Dict[str, Any]: A dictionary with success status and optional error message.
"""
if not pyautogui:
return {"success": False, "error": "pyautogui not available"}
try:
pyautogui.moveTo(x, y)
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
async def left_click(self, x: Optional[int] = None, y: Optional[int] = None) -> Dict[str, Any]:
"""Perform a left mouse click at the specified coordinates.
Args:
x (Optional[int]): The x-coordinate to click at. If None, clicks at current position.
y (Optional[int]): The y-coordinate to click at. If None, clicks at current position.
Returns:
Dict[str, Any]: A dictionary with success status and optional error message.
"""
if not pyautogui:
return {"success": False, "error": "pyautogui not available"}
try:
if x is not None and y is not None:
pyautogui.moveTo(x, y)
pyautogui.click()
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
async def right_click(self, x: Optional[int] = None, y: Optional[int] = None) -> Dict[str, Any]:
"""Perform a right mouse click at the specified coordinates.
Args:
x (Optional[int]): The x-coordinate to click at. If None, clicks at current position.
y (Optional[int]): The y-coordinate to click at. If None, clicks at current position.
Returns:
Dict[str, Any]: A dictionary with success status and optional error message.
"""
if not pyautogui:
return {"success": False, "error": "pyautogui not available"}
try:
if x is not None and y is not None:
pyautogui.moveTo(x, y)
pyautogui.rightClick()
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
async def double_click(
self, x: Optional[int] = None, y: Optional[int] = None
) -> Dict[str, Any]:
"""Perform a double left mouse click at the specified coordinates.
Args:
x (Optional[int]): The x-coordinate to double-click at. If None, clicks at current position.
y (Optional[int]): The y-coordinate to double-click at. If None, clicks at current position.
Returns:
Dict[str, Any]: A dictionary with success status and optional error message.
"""
if not pyautogui:
return {"success": False, "error": "pyautogui not available"}
try:
if x is not None and y is not None:
pyautogui.moveTo(x, y)
pyautogui.doubleClick(interval=0.1)
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
async def drag_to(
self, x: int, y: int, button: str = "left", duration: float = 0.5
) -> Dict[str, Any]:
"""Drag from the current position to the specified coordinates.
Args:
x (int): The x-coordinate to drag to.
y (int): The y-coordinate to drag to.
button (str): The mouse button to use for dragging ("left", "right", or "middle").
duration (float): The time in seconds to take for the drag operation.
Returns:
Dict[str, Any]: A dictionary with success status and optional error message.
"""
if not pyautogui:
return {"success": False, "error": "pyautogui not available"}
try:
pyautogui.dragTo(x, y, duration=duration, button=button)
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
async def drag(
self, path: List[Tuple[int, int]], button: str = "left", duration: float = 0.5
) -> Dict[str, Any]:
"""Drag the mouse through a series of coordinates.
Args:
path (List[Tuple[int, int]]): A list of (x, y) coordinate tuples to drag through.
button (str): The mouse button to use for dragging ("left", "right", or "middle").
duration (float): The total time in seconds for the entire drag operation.
Returns:
Dict[str, Any]: A dictionary with success status and optional error message.
"""
if not pyautogui:
return {"success": False, "error": "pyautogui not available"}
try:
if not path:
return {"success": False, "error": "Path is empty"}
# Move to first position
pyautogui.moveTo(*path[0])
# Drag through all positions
for x, y in path[1:]:
pyautogui.dragTo(x, y, duration=duration / len(path), button=button)
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
# Keyboard Actions
async def key_down(self, key: str) -> Dict[str, Any]:
"""Press and hold a keyboard key.
Args:
key (str): The key to press down (e.g., 'ctrl', 'shift', 'a').
Returns:
Dict[str, Any]: A dictionary with success status and optional error message.
"""
if not pyautogui:
return {"success": False, "error": "pyautogui not available"}
try:
pyautogui.keyDown(key)
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
async def key_up(self, key: str) -> Dict[str, Any]:
"""Release a keyboard key.
Args:
key (str): The key to release (e.g., 'ctrl', 'shift', 'a').
Returns:
Dict[str, Any]: A dictionary with success status and optional error message.
"""
if not pyautogui:
return {"success": False, "error": "pyautogui not available"}
try:
pyautogui.keyUp(key)
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
async def type_text(self, text: str) -> Dict[str, Any]:
"""Type the specified text.
Args:
text (str): The text to type.
Returns:
Dict[str, Any]: A dictionary with success status and optional error message.
"""
try:
# use pynput for Unicode support
self.keyboard.type(text)
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
async def press_key(self, key: str) -> Dict[str, Any]:
"""Press and release a keyboard key.
Args:
key (str): The key to press (e.g., 'enter', 'space', 'tab').
Returns:
Dict[str, Any]: A dictionary with success status and optional error message.
"""
if not pyautogui:
return {"success": False, "error": "pyautogui not available"}
try:
pyautogui.press(key)
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
async def hotkey(self, keys: List[str]) -> Dict[str, Any]:
"""Press a combination of keys simultaneously.
Args:
keys (List[str]): The keys to press together (e.g., ['ctrl', 'c'], ['alt', 'tab']).
Returns:
Dict[str, Any]: A dictionary with success status and optional error message.
"""
if not pyautogui:
return {"success": False, "error": "pyautogui not available"}
try:
pyautogui.hotkey(*keys)
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
# Scrolling Actions
async def scroll(self, x: int, y: int) -> Dict[str, Any]:
"""Scroll vertically at the current cursor position.
Args:
x (int): Horizontal scroll amount (not used in pyautogui implementation).
y (int): Vertical scroll amount. Positive values scroll up, negative values scroll down.
Returns:
Dict[str, Any]: A dictionary with success status and optional error message.
"""
if not pyautogui:
return {"success": False, "error": "pyautogui not available"}
try:
self.mouse.scroll(x, y)
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
async def scroll_down(self, clicks: int = 1) -> Dict[str, Any]:
"""Scroll down by the specified number of clicks.
Args:
clicks (int): The number of scroll clicks to perform downward.
Returns:
Dict[str, Any]: A dictionary with success status and optional error message.
"""
if not pyautogui:
return {"success": False, "error": "pyautogui not available"}
try:
pyautogui.scroll(-clicks)
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
async def scroll_up(self, clicks: int = 1) -> Dict[str, Any]:
"""Scroll up by the specified number of clicks.
Args:
clicks (int): The number of scroll clicks to perform upward.
Returns:
Dict[str, Any]: A dictionary with success status and optional error message.
"""
if not pyautogui:
return {"success": False, "error": "pyautogui not available"}
try:
pyautogui.scroll(clicks)
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
# Screen Actions
async def screenshot(self) -> Dict[str, Any]:
"""Capture a screenshot of the entire screen.
Returns:
Dict[str, Any]: A dictionary containing the success status and either
base64-encoded image data or an error message.
Structure: {"success": bool, "image_data": str} or
{"success": bool, "error": str}
"""
if not pyautogui:
return {"success": False, "error": "pyautogui not available"}
try:
from PIL import Image
screenshot = pyautogui.screenshot()
if not isinstance(screenshot, Image.Image):
return {"success": False, "error": "Failed to capture screenshot"}
buffered = BytesIO()
screenshot.save(buffered, format="PNG", optimize=True)
buffered.seek(0)
image_data = base64.b64encode(buffered.getvalue()).decode()
return {"success": True, "image_data": image_data}
except Exception as e:
return {"success": False, "error": f"Screenshot error: {str(e)}"}
async def get_screen_size(self) -> Dict[str, Any]:
"""Get the size of the screen in pixels.
Returns:
Dict[str, Any]: A dictionary containing the success status and either
screen size information or an error message.
Structure: {"success": bool, "size": {"width": int, "height": int}} or
{"success": bool, "error": str}
"""
try:
if pyautogui:
size = pyautogui.size()
return {"success": True, "size": {"width": size.width, "height": size.height}}
elif WINDOWS_API_AVAILABLE:
# Fallback to Windows API
width = win32api.GetSystemMetrics(win32con.SM_CXSCREEN)
height = win32api.GetSystemMetrics(win32con.SM_CYSCREEN)
return {"success": True, "size": {"width": width, "height": height}}
else:
return {"success": False, "error": "No screen size detection method available"}
except Exception as e:
return {"success": False, "error": str(e)}
async def get_cursor_position(self) -> Dict[str, Any]:
"""Get the current position of the mouse cursor.
Returns:
Dict[str, Any]: A dictionary containing the success status and either
cursor position or an error message.
Structure: {"success": bool, "position": {"x": int, "y": int}} or
{"success": bool, "error": str}
"""
try:
if pyautogui:
pos = pyautogui.position()
return {"success": True, "position": {"x": pos.x, "y": pos.y}}
elif WINDOWS_API_AVAILABLE:
# Fallback to Windows API
pos = win32gui.GetCursorPos()
return {"success": True, "position": {"x": pos[0], "y": pos[1]}}
else:
return {"success": False, "error": "No cursor position detection method available"}
except Exception as e:
return {"success": False, "error": str(e)}
# Clipboard Actions
async def copy_to_clipboard(self) -> Dict[str, Any]:
"""Get the current content of the clipboard.
Returns:
Dict[str, Any]: A dictionary containing the success status and either
clipboard content or an error message.
Structure: {"success": bool, "content": str} or
{"success": bool, "error": str}
"""
try:
import pyperclip
content = pyperclip.paste()
return {"success": True, "content": content}
except Exception as e:
return {"success": False, "error": str(e)}
async def set_clipboard(self, text: str) -> Dict[str, Any]:
"""Set the clipboard content to the specified text.
Args:
text (str): The text to copy to the clipboard.
Returns:
Dict[str, Any]: A dictionary with success status and optional error message.
"""
try:
import pyperclip
pyperclip.copy(text)
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
# Command Execution
async def run_command(self, command: str) -> Dict[str, Any]:
"""Execute a shell command asynchronously.
Args:
command (str): The shell command to execute.
Returns:
Dict[str, Any]: A dictionary containing the success status and either
command output or an error message.
Structure: {"success": bool, "stdout": str, "stderr": str, "return_code": int} or
{"success": bool, "error": str}
"""
try:
# Create subprocess
process = await asyncio.create_subprocess_shell(
command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)
# Wait for the subprocess to finish
stdout, stderr = await process.communicate()
# Return decoded output
return {
"success": True,
"stdout": stdout.decode() if stdout else "",
"stderr": stderr.decode() if stderr else "",
"return_code": process.returncode,
}
except Exception as e:
return {"success": False, "error": str(e)}
```
--------------------------------------------------------------------------------
/libs/typescript/computer/tests/interface/macos.test.ts:
--------------------------------------------------------------------------------
```typescript
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { WebSocket, WebSocketServer } from 'ws';
import { MacOSComputerInterface } from '../../src/interface/macos.ts';
describe('MacOSComputerInterface', () => {
// Define test parameters
const testParams = {
ipAddress: 'localhost',
username: 'testuser',
password: 'testpass',
// apiKey: "test-api-key", No API Key for local testing
vmName: 'test-vm',
};
// WebSocket server mock
let wss: WebSocketServer;
let serverPort: number;
let connectedClients: WebSocket[] = [];
// Track received messages for verification
interface ReceivedMessage {
action: string;
[key: string]: unknown;
}
let receivedMessages: ReceivedMessage[] = [];
// Set up WebSocket server before all tests
beforeEach(async () => {
receivedMessages = [];
connectedClients = [];
// Create WebSocket server on a random available port
wss = new WebSocketServer({ port: 0 });
serverPort = (wss.address() as { port: number }).port;
// Update test params with the actual server address
testParams.ipAddress = `localhost:${serverPort}`;
// Handle WebSocket connections
wss.on('connection', (ws) => {
connectedClients.push(ws);
// Handle incoming messages
ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
receivedMessages.push(message);
// Send appropriate responses based on action
switch (message.command) {
case 'screenshot':
ws.send(
JSON.stringify({
image_data: Buffer.from('fake-screenshot-data').toString('base64'),
success: true,
})
);
break;
case 'get_screen_size':
ws.send(
JSON.stringify({
size: { width: 1920, height: 1080 },
success: true,
})
);
break;
case 'get_cursor_position':
ws.send(
JSON.stringify({
position: { x: 100, y: 200 },
success: true,
})
);
break;
case 'copy_to_clipboard':
ws.send(
JSON.stringify({
content: 'clipboard content',
success: true,
})
);
break;
case 'file_exists':
ws.send(
JSON.stringify({
exists: true,
success: true,
})
);
break;
case 'directory_exists':
ws.send(
JSON.stringify({
exists: true,
success: true,
})
);
break;
case 'list_dir':
ws.send(
JSON.stringify({
files: ['file1.txt', 'file2.txt'],
success: true,
})
);
break;
case 'read_text':
ws.send(
JSON.stringify({
content: 'file content',
success: true,
})
);
break;
case 'read_bytes':
ws.send(
JSON.stringify({
content_b64: Buffer.from('binary content').toString('base64'),
success: true,
})
);
break;
case 'run_command':
ws.send(
JSON.stringify({
stdout: 'command output',
stderr: '',
success: true,
})
);
break;
case 'get_accessibility_tree':
ws.send(
JSON.stringify({
role: 'window',
title: 'Test Window',
bounds: { x: 0, y: 0, width: 1920, height: 1080 },
children: [],
success: true,
})
);
break;
case 'to_screen_coordinates':
case 'to_screenshot_coordinates':
ws.send(
JSON.stringify({
coordinates: [message.params?.x || 0, message.params?.y || 0],
success: true,
})
);
break;
default:
// For all other actions, just send success
ws.send(JSON.stringify({ success: true }));
break;
}
} catch (error) {
ws.send(JSON.stringify({ error: (error as Error).message }));
}
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
});
});
// Clean up WebSocket server after each test
afterEach(async () => {
// Close all connected clients
for (const client of connectedClients) {
if (client.readyState === WebSocket.OPEN) {
client.close();
}
}
// Close the server
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
});
describe('Connection Management', () => {
it('should connect with proper authentication headers', async () => {
const macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
// Verify the interface is connected
expect(macosInterface.isConnected()).toBe(true);
expect(connectedClients.length).toBe(1);
await macosInterface.disconnect();
});
it('should handle connection without API key', async () => {
// Create a separate server that doesn't check auth
const noAuthWss = new WebSocketServer({ port: 0 });
const noAuthPort = (noAuthWss.address() as { port: number }).port;
noAuthWss.on('connection', (ws) => {
ws.on('message', () => {
ws.send(JSON.stringify({ success: true }));
});
});
const macosInterface = new MacOSComputerInterface(
`localhost:${noAuthPort}`,
testParams.username,
testParams.password,
undefined,
undefined
);
await macosInterface.connect();
expect(macosInterface.isConnected()).toBe(true);
await macosInterface.disconnect();
await new Promise<void>((resolve) => {
noAuthWss.close(() => resolve());
});
});
});
describe('Mouse Actions', () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
});
afterEach(async () => {
if (macosInterface) {
await macosInterface.disconnect();
}
});
it('should send mouse_down command', async () => {
await macosInterface.mouseDown(100, 200, 'left');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'mouse_down',
params: {
x: 100,
y: 200,
button: 'left',
},
});
});
it('should send mouse_up command', async () => {
await macosInterface.mouseUp(100, 200, 'right');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'mouse_up',
params: {
x: 100,
y: 200,
button: 'right',
},
});
});
it('should send left_click command', async () => {
await macosInterface.leftClick(150, 250);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'left_click',
params: {
x: 150,
y: 250,
},
});
});
it('should send right_click command', async () => {
await macosInterface.rightClick(200, 300);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'right_click',
params: {
x: 200,
y: 300,
},
});
});
it('should send double_click command', async () => {
await macosInterface.doubleClick(250, 350);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'double_click',
params: {
x: 250,
y: 350,
},
});
});
it('should send move_cursor command', async () => {
await macosInterface.moveCursor(300, 400);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'move_cursor',
params: {
x: 300,
y: 400,
},
});
});
it('should send drag_to command', async () => {
await macosInterface.dragTo(400, 500, 'left', 1.5);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'drag_to',
params: {
x: 400,
y: 500,
button: 'left',
duration: 1.5,
},
});
});
it('should send drag command with path', async () => {
const path: Array<[number, number]> = [
[100, 100],
[200, 200],
[300, 300],
];
await macosInterface.drag(path, 'middle', 2.0);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'drag',
params: {
path: path,
button: 'middle',
duration: 2.0,
},
});
});
});
describe('Keyboard Actions', () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
});
afterEach(async () => {
if (macosInterface) {
await macosInterface.disconnect();
}
});
it('should send key_down command', async () => {
await macosInterface.keyDown('a');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'key_down',
params: {
key: 'a',
},
});
});
it('should send key_up command', async () => {
await macosInterface.keyUp('b');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'key_up',
params: {
key: 'b',
},
});
});
it('should send type_text command', async () => {
await macosInterface.typeText('Hello, World!');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'type_text',
params: {
text: 'Hello, World!',
},
});
});
it('should send press_key command', async () => {
await macosInterface.pressKey('enter');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'press_key',
params: {
key: 'enter',
},
});
});
it('should send hotkey command', async () => {
await macosInterface.hotkey('cmd', 'c');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'hotkey',
params: {
keys: ['cmd', 'c'],
},
});
});
});
describe('Scrolling Actions', () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
});
afterEach(async () => {
if (macosInterface) {
await macosInterface.disconnect();
}
});
it('should send scroll command', async () => {
await macosInterface.scroll(10, -5);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'scroll',
params: {
x: 10,
y: -5,
},
});
});
it('should send scroll_down command', async () => {
await macosInterface.scrollDown(3);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'scroll_down',
params: {
clicks: 3,
},
});
});
it('should send scroll_up command', async () => {
await macosInterface.scrollUp(2);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'scroll_up',
params: {
clicks: 2,
},
});
});
});
describe('Screen Actions', () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
});
afterEach(async () => {
if (macosInterface) {
await macosInterface.disconnect();
}
});
it('should get screenshot', async () => {
const screenshot = await macosInterface.screenshot();
expect(screenshot).toBeInstanceOf(Buffer);
expect(screenshot.toString()).toBe('fake-screenshot-data');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'screenshot',
params: {},
});
});
it('should get screen size', async () => {
const size = await macosInterface.getScreenSize();
expect(size).toEqual({ width: 1920, height: 1080 });
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'get_screen_size',
params: {},
});
});
it('should get cursor position', async () => {
const position = await macosInterface.getCursorPosition();
expect(position).toEqual({ x: 100, y: 200 });
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'get_cursor_position',
params: {},
});
});
});
describe('Clipboard Actions', () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
});
afterEach(async () => {
if (macosInterface) {
await macosInterface.disconnect();
}
});
it('should copy to clipboard', async () => {
const text = await macosInterface.copyToClipboard();
expect(text).toBe('clipboard content');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'copy_to_clipboard',
params: {},
});
});
it('should set clipboard', async () => {
await macosInterface.setClipboard('new clipboard text');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'set_clipboard',
params: {
text: 'new clipboard text',
},
});
});
});
describe('File System Actions', () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
});
afterEach(async () => {
if (macosInterface) {
await macosInterface.disconnect();
}
});
it('should check file exists', async () => {
const exists = await macosInterface.fileExists('/path/to/file');
expect(exists).toBe(true);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'file_exists',
params: {
path: '/path/to/file',
},
});
});
it('should check directory exists', async () => {
const exists = await macosInterface.directoryExists('/path/to/dir');
expect(exists).toBe(true);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'directory_exists',
params: {
path: '/path/to/dir',
},
});
});
it('should list directory', async () => {
const files = await macosInterface.listDir('/path/to/dir');
expect(files).toEqual(['file1.txt', 'file2.txt']);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'list_dir',
params: {
path: '/path/to/dir',
},
});
});
it('should read text file', async () => {
const content = await macosInterface.readText('/path/to/file.txt');
expect(content).toBe('file content');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'read_text',
params: {
path: '/path/to/file.txt',
},
});
});
it('should write text file', async () => {
await macosInterface.writeText('/path/to/file.txt', 'new content');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'write_text',
params: {
path: '/path/to/file.txt',
content: 'new content',
},
});
});
it('should read binary file', async () => {
const content = await macosInterface.readBytes('/path/to/file.bin');
expect(content).toBeInstanceOf(Buffer);
expect(content.toString()).toBe('binary content');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'read_bytes',
params: {
path: '/path/to/file.bin',
},
});
});
it('should write binary file', async () => {
const buffer = Buffer.from('binary data');
await macosInterface.writeBytes('/path/to/file.bin', buffer);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'write_bytes',
params: {
path: '/path/to/file.bin',
content_b64: buffer.toString('base64'),
},
});
});
it('should delete file', async () => {
await macosInterface.deleteFile('/path/to/file');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'delete_file',
params: {
path: '/path/to/file',
},
});
});
it('should create directory', async () => {
await macosInterface.createDir('/path/to/new/dir');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'create_dir',
params: {
path: '/path/to/new/dir',
},
});
});
it('should delete directory', async () => {
await macosInterface.deleteDir('/path/to/dir');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'delete_dir',
params: {
path: '/path/to/dir',
},
});
});
it('should run command', async () => {
const [stdout, stderr] = await macosInterface.runCommand('ls -la');
expect(stdout).toBe('command output');
expect(stderr).toBe('');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'run_command',
params: {
command: 'ls -la',
},
});
});
});
describe('Accessibility Actions', () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
});
afterEach(async () => {
if (macosInterface) {
await macosInterface.disconnect();
}
});
it('should get accessibility tree', async () => {
const tree = await macosInterface.getAccessibilityTree();
expect(tree).toEqual({
role: 'window',
title: 'Test Window',
bounds: { x: 0, y: 0, width: 1920, height: 1080 },
children: [],
success: true,
});
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'get_accessibility_tree',
params: {},
});
});
it('should convert to screen coordinates', async () => {
const [x, y] = await macosInterface.toScreenCoordinates(100, 200);
expect(x).toBe(100);
expect(y).toBe(200);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'to_screen_coordinates',
params: {
x: 100,
y: 200,
},
});
});
it('should convert to screenshot coordinates', async () => {
const [x, y] = await macosInterface.toScreenshotCoordinates(300, 400);
expect(x).toBe(300);
expect(y).toBe(400);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'to_screenshot_coordinates',
params: {
x: 300,
y: 400,
},
});
});
});
describe('Error Handling', () => {
it('should handle WebSocket connection errors', async () => {
// Use a valid but unreachable IP to avoid DNS errors
const macosInterface = new MacOSComputerInterface(
'localhost:9999',
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
// Connection should fail
await expect(macosInterface.connect()).rejects.toThrow();
});
it('should handle command errors', async () => {
// Create a server that returns errors
const errorWss = new WebSocketServer({ port: 0 });
const errorPort = (errorWss.address() as { port: number }).port;
errorWss.on('connection', (ws) => {
ws.on('message', () => {
ws.send(JSON.stringify({ error: 'Command failed', success: false }));
});
});
const macosInterface = new MacOSComputerInterface(
`localhost:${errorPort}`,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
// Command should throw error
await expect(macosInterface.leftClick(100, 100)).rejects.toThrow('Command failed');
await macosInterface.disconnect();
await new Promise<void>((resolve) => {
errorWss.close(() => resolve());
});
});
it('should handle disconnection gracefully', async () => {
const macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
expect(macosInterface.isConnected()).toBe(true);
// Disconnect
macosInterface.disconnect();
expect(macosInterface.isConnected()).toBe(false);
// Should reconnect automatically on next command
await macosInterface.leftClick(100, 100);
expect(macosInterface.isConnected()).toBe(true);
await macosInterface.disconnect();
});
it('should handle force close', async () => {
const macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
expect(macosInterface.isConnected()).toBe(true);
// Force close
macosInterface.forceClose();
expect(macosInterface.isConnected()).toBe(false);
});
});
});
```
--------------------------------------------------------------------------------
/docs/content/docs/macos-vm-cli-playbook/lume/http-api.mdx:
--------------------------------------------------------------------------------
```markdown
---
title: HTTP Server API
description: Lume exposes a local HTTP API server that listens at localhost for programmatic management of VMs.
---
import { Tabs, Tab } from 'fumadocs-ui/components/tabs';
import { Callout } from 'fumadocs-ui/components/callout';
## Default URL
```
http://localhost:7777
```
<Callout type="info">
The HTTP API service runs on port `7777` by default. If you'd like to use a different port, pass
the `--port` option during installation or when running `lume serve`.
</Callout>
## Endpoints
---
### Create VM
Create a new virtual machine.
`POST: /lume/vms`
#### Parameters
| Name | Type | Required | Description |
| -------- | ------- | -------- | ------------------------------------ |
| name | string | Yes | Name of the VM |
| os | string | Yes | Guest OS (`macOS`, `linux`, etc.) |
| cpu | integer | Yes | Number of CPU cores |
| memory | string | Yes | Memory size (e.g. `4GB`) |
| diskSize | string | Yes | Disk size (e.g. `64GB`) |
| display | string | No | Display resolution (e.g. `1024x768`) |
| ipsw | string | No | IPSW version (e.g. `latest`) |
| storage | string | No | Storage type (`ssd`, etc.) |
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
curl --connect-timeout 6000 \
--max-time 5000 \
-X POST \
-H "Content-Type: application/json" \
-d '{
"name": "lume_vm",
"os": "macOS",
"cpu": 2,
"memory": "4GB",
"diskSize": "64GB",
"display": "1024x768",
"ipsw": "latest",
"storage": "ssd"
}' \
http://localhost:7777/lume/vms
```
</Tab>
<Tab value="Python">
```python
import requests
payload = {
"name": "lume_vm",
"os": "macOS",
"cpu": 2,
"memory": "4GB",
"diskSize": "64GB",
"display": "1024x768",
"ipsw": "latest",
"storage": "ssd"
}
r = requests.post("http://localhost:7777/lume/vms", json=payload, timeout=50)
print(r.json())
```
</Tab>
<Tab value="TypeScript">
```typescript
const payload = {
name: 'lume_vm',
os: 'macOS',
cpu: 2,
memory: '4GB',
diskSize: '64GB',
display: '1024x768',
ipsw: 'latest',
storage: 'ssd',
};
const res = await fetch('http://localhost:7777/lume/vms', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
console.log(await res.json());
```
</Tab>
</Tabs>
---
### Run VM
Run a virtual machine instance.
`POST: /lume/vms/:name/run`
#### Parameters
| Name | Type | Required | Description |
| ----------------- | --------------- | -------- | --------------------------------------------------- |
| noDisplay | boolean | No | If true, do not start VNC client |
| sharedDirectories | array of object | No | List of shared directories (`hostPath`, `readOnly`) |
| recoveryMode | boolean | No | Start in recovery mode |
| storage | string | No | Storage type (`ssd`, etc.) |
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
# Basic run
curl --connect-timeout 6000 \
--max-time 5000 \
-X POST \
http://localhost:7777/lume/vms/my-vm-name/run
# Run with VNC client started and shared directory
curl --connect-timeout 6000 \
--max-time 5000 \
-X POST \
-H "Content-Type: application/json" \
-d '{
"noDisplay": false,
"sharedDirectories": [
{
"hostPath": "~/Projects",
"readOnly": false
}
],
"recoveryMode": false,
"storage": "ssd"
}' \
http://localhost:7777/lume/vms/lume_vm/run
```
</Tab>
<Tab value="Python">
```python
import requests
# Basic run
r = requests.post("http://localhost:7777/lume/vms/my-vm-name/run", timeout=50)
print(r.json())
# With VNC and shared directory
payload = {
"noDisplay": False,
"sharedDirectories": [
{"hostPath": "~/Projects", "readOnly": False}
],
"recoveryMode": False,
"storage": "ssd"
}
r = requests.post("http://localhost:7777/lume/vms/lume_vm/run", json=payload, timeout=50)
print(r.json())
```
</Tab>
<Tab value="TypeScript">
```typescript
// Basic run
let res = await fetch('http://localhost:7777/lume/vms/my-vm-name/run', {
method: 'POST',
});
console.log(await res.json());
// With VNC and shared directory
const payload = {
noDisplay: false,
sharedDirectories: [{ hostPath: '~/Projects', readOnly: false }],
recoveryMode: false,
storage: 'ssd',
};
res = await fetch('http://localhost:7777/lume/vms/lume_vm/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
console.log(await res.json());
```
</Tab>
</Tabs>
---
### List VMs
List all virtual machines.
`GET: /lume/vms`
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
curl --connect-timeout 6000 \
--max-time 5000 \
http://localhost:7777/lume/vms
```
</Tab>
<Tab value="Python">
```python
import requests
r = requests.get("http://localhost:7777/lume/vms", timeout=50)
print(r.json())
```
</Tab>
<Tab value="TypeScript">
```typescript
const res = await fetch('http://localhost:7777/lume/vms');
console.log(await res.json());
```
</Tab>
</Tabs>
```json
[
{
"name": "my-vm",
"state": "stopped",
"os": "macOS",
"cpu": 2,
"memory": "4GB",
"diskSize": "64GB"
},
{
"name": "my-vm-2",
"state": "stopped",
"os": "linux",
"cpu": 2,
"memory": "4GB",
"diskSize": "64GB"
}
]
```
---
### Get VM Details
Get details for a specific virtual machine.
`GET: /lume/vms/:name`
#### Parameters
| Name | Type | Required | Description |
| ------- | ------ | -------- | -------------------------- |
| storage | string | No | Storage type (`ssd`, etc.) |
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
# Basic get
curl --connect-timeout 6000 \
--max-time 5000 \
http://localhost:7777/lume/vms/lume_vm
# Get with specific storage
curl --connect-timeout 6000 \
--max-time 5000 \
http://localhost:7777/lume/vms/lume_vm?storage=ssd
```
</Tab>
<Tab value="Python">
```python
import requests
# Basic get
details = requests.get("http://localhost:7777/lume/vms/lume_vm", timeout=50)
print(details.json())
# Get with specific storage
details = requests.get("http://localhost:7777/lume/vms/lume_vm", params={"storage": "ssd"}, timeout=50)
print(details.json())
```
</Tab>
<Tab value="TypeScript">
```typescript
// Basic get
let res = await fetch('http://localhost:7777/lume/vms/lume_vm');
console.log(await res.json());
// Get with specific storage
res = await fetch('http://localhost:7777/lume/vms/lume_vm?storage=ssd');
console.log(await res.json());
```
</Tab>
</Tabs>
```json
{
"name": "lume_vm",
"state": "stopped",
"os": "macOS",
"cpu": 2,
"memory": "4GB",
"diskSize": "64GB",
"display": "1024x768",
"ipAddress": "192.168.65.2",
"vncPort": 5900,
"sharedDirectories": [
{
"hostPath": "~/Projects",
"readOnly": false,
"tag": "com.apple.virtio-fs.automount"
}
]
}
```
---
### Update VM Configuration
Update the configuration of a virtual machine.
`PATCH: /lume/vms/:name`
#### Parameters
| Name | Type | Required | Description |
| -------- | ------- | -------- | ------------------------------------- |
| cpu | integer | No | Number of CPU cores |
| memory | string | No | Memory size (e.g. `8GB`) |
| diskSize | string | No | Disk size (e.g. `100GB`) |
| display | string | No | Display resolution (e.g. `1920x1080`) |
| storage | string | No | Storage type (`ssd`, etc.) |
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
curl --connect-timeout 6000 \
--max-time 5000 \
-X PATCH \
-H "Content-Type: application/json" \
-d '{
"cpu": 4,
"memory": "8GB",
"diskSize": "100GB",
"display": "1920x1080",
"storage": "ssd"
}' \
http://localhost:7777/lume/vms/lume_vm
```
</Tab>
<Tab value="Python">
```python
import requests
payload = {
"cpu": 4,
"memory": "8GB",
"diskSize": "100GB",
"display": "1920x1080",
"storage": "ssd"
}
r = requests.patch("http://localhost:7777/lume/vms/lume_vm", json=payload, timeout=50)
print(r.json())
```
</Tab>
<Tab value="TypeScript">
```typescript
const payload = {
cpu: 4,
memory: '8GB',
diskSize: '100GB',
display: '1920x1080',
storage: 'ssd',
};
const res = await fetch('http://localhost:7777/lume/vms/lume_vm', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
console.log(await res.json());
```
</Tab>
</Tabs>
---
### Stop VM
Stop a running virtual machine.
`POST: /lume/vms/:name/stop`
#### Parameters
| Name | Type | Required | Description |
| ------- | ------ | -------- | -------------------------- |
| storage | string | No | Storage type (`ssd`, etc.) |
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
# Basic stop
curl --connect-timeout 6000 \
--max-time 5000 \
-X POST \
http://localhost:7777/lume/vms/lume_vm/stop
# Stop with storage location specified
curl --connect-timeout 6000 \
--max-time 5000 \
-X POST \
http://localhost:7777/lume/vms/lume_vm/stop?storage=ssd
```
</Tab>
<Tab value="Python">
```python
import requests
# Basic stop
r = requests.post("http://localhost:7777/lume/vms/lume_vm/stop", timeout=50)
print(r.json())
# Stop with storage location specified
r = requests.post("http://localhost:7777/lume/vms/lume_vm/stop", params={"storage": "ssd"}, timeout=50)
print(r.json())
```
</Tab>
<Tab value="TypeScript">
```typescript
// Basic stop
let res = await fetch('http://localhost:7777/lume/vms/lume_vm/stop', {
method: 'POST',
});
console.log(await res.json());
// Stop with storage location specified
res = await fetch('http://localhost:7777/lume/vms/lume_vm/stop?storage=ssd', {
method: 'POST',
});
console.log(await res.json());
```
</Tab>
</Tabs>
---
### Delete VM
Delete a virtual machine instance.
`DELETE: /lume/vms/:name`
#### Parameters
| Name | Type | Required | Description |
| ------- | ------ | -------- | -------------------------- |
| storage | string | No | Storage type (`ssd`, etc.) |
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
# Basic delete
curl --connect-timeout 6000 \
--max-time 5000 \
-X DELETE \
http://localhost:7777/lume/vms/lume_vm
# Delete with specific storage
curl --connect-timeout 6000 \
--max-time 5000 \
-X DELETE \
http://localhost:7777/lume/vms/lume_vm?storage=ssd
```
</Tab>
<Tab value="Python">
```python
import requests
# Basic delete
r = requests.delete("http://localhost:7777/lume/vms/lume_vm", timeout=50)
print(r.status_code)
# Delete with specific storage
r = requests.delete("http://localhost:7777/lume/vms/lume_vm", params={"storage": "ssd"}, timeout=50)
print(r.status_code)
```
</Tab>
<Tab value="TypeScript">
```typescript
// Basic delete
let res = await fetch('http://localhost:7777/lume/vms/lume_vm', {
method: 'DELETE',
});
console.log(res.status);
// Delete with specific storage
res = await fetch('http://localhost:7777/lume/vms/lume_vm?storage=ssd', {
method: 'DELETE',
});
console.log(res.status);
```
</Tab>
</Tabs>
---
### Clone VM
Clone an existing virtual machine.
`POST: /lume/vms/clone`
#### Parameters
| Name | Type | Required | Description |
| -------------- | ------ | -------- | ----------------------------------- |
| name | string | Yes | Source VM name |
| newName | string | Yes | New VM name |
| sourceLocation | string | No | Source storage location (`default`) |
| destLocation | string | No | Destination storage location |
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
curl --connect-timeout 6000 \
--max-time 5000 \
-X POST \
-H "Content-Type: application/json" \
-d '{
"name": "source-vm",
"newName": "cloned-vm",
"sourceLocation": "default",
"destLocation": "ssd"
}' \
http://localhost:7777/lume/vms/clone
```
</Tab>
<Tab value="Python">
```python
import requests
payload = {
"name": "source-vm",
"newName": "cloned-vm",
"sourceLocation": "default",
"destLocation": "ssd"
}
r = requests.post("http://localhost:7777/lume/vms/clone", json=payload, timeout=50)
print(r.json())
```
</Tab>
<Tab value="TypeScript">
```typescript
const payload = {
name: 'source-vm',
newName: 'cloned-vm',
sourceLocation: 'default',
destLocation: 'ssd',
};
const res = await fetch('http://localhost:7777/lume/vms/clone', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
console.log(await res.json());
```
</Tab>
</Tabs>
---
### Pull VM Image
Pull a VM image from a registry.
`POST: /lume/pull`
#### Parameters
| Name | Type | Required | Description |
| ------------ | ------ | -------- | ------------------------------------- |
| image | string | Yes | Image name (e.g. `macos-sequoia-...`) |
| name | string | No | VM name for the pulled image |
| registry | string | No | Registry host (e.g. `ghcr.io`) |
| organization | string | No | Organization name |
| storage | string | No | Storage type (`ssd`, etc.) |
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
curl --connect-timeout 6000 \
--max-time 5000 \
-X POST \
-H "Content-Type: application/json" \
-d '{
"image": "macos-sequoia-vanilla:latest",
"name": "my-vm-name",
"registry": "ghcr.io",
"organization": "trycua",
"storage": "ssd"
}' \
http://localhost:7777/lume/pull
```
</Tab>
<Tab value="Python">
```python
import requests
payload = {
"image": "macos-sequoia-vanilla:latest",
"name": "my-vm-name",
"registry": "ghcr.io",
"organization": "trycua",
"storage": "ssd"
}
r = requests.post("http://localhost:7777/lume/pull", json=payload, timeout=50)
print(r.json())
```
</Tab>
<Tab value="TypeScript">
```typescript
const payload = {
image: 'macos-sequoia-vanilla:latest',
name: 'my-vm-name',
registry: 'ghcr.io',
organization: 'trycua',
storage: 'ssd',
};
const res = await fetch('http://localhost:7777/lume/pull', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
console.log(await res.json());
```
</Tab>
</Tabs>
---
### Push VM Image
Push a VM to a registry as an image (asynchronous operation).
`POST: /lume/vms/push`
#### Parameters
| Name | Type | Required | Description |
| ------------ | ----------- | -------- | ------------------------------------ |
| name | string | Yes | Local VM name to push |
| imageName | string | Yes | Image name in registry |
| tags | array | Yes | Image tags (e.g. `["latest", "v1"]`) |
| organization | string | Yes | Organization name |
| registry | string | No | Registry host (e.g. `ghcr.io`) |
| chunkSizeMb | integer | No | Chunk size in MB for upload |
| storage | string/null | No | Storage type (`ssd`, etc.) |
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
curl --connect-timeout 6000 \
--max-time 5000 \
-X POST \
-H "Content-Type: application/json" \
-d '{
"name": "my-local-vm",
"imageName": "my-image",
"tags": ["latest", "v1"],
"organization": "my-org",
"registry": "ghcr.io",
"chunkSizeMb": 512,
"storage": null
}' \
http://localhost:7777/lume/vms/push
```
</Tab>
<Tab value="Python">
```python
import requests
payload = {
"name": "my-local-vm",
"imageName": "my-image",
"tags": ["latest", "v1"],
"organization": "my-org",
"registry": "ghcr.io",
"chunkSizeMb": 512,
"storage": None
}
r = requests.post("http://localhost:7777/lume/vms/push", json=payload, timeout=50)
print(r.json())
```
</Tab>
<Tab value="TypeScript">
```typescript
const payload = {
name: 'my-local-vm',
imageName: 'my-image',
tags: ['latest', 'v1'],
organization: 'my-org',
registry: 'ghcr.io',
chunkSizeMb: 512,
storage: null,
};
const res = await fetch('http://localhost:7777/lume/vms/push', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
console.log(await res.json());
```
</Tab>
</Tabs>
**Response (202 Accepted):**
```json
{
"message": "Push initiated in background",
"name": "my-local-vm",
"imageName": "my-image",
"tags": ["latest", "v1"]
}
```
---
### List Images
List available VM images.
`GET: /lume/images`
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
curl --connect-timeout 6000 \
--max-time 5000 \
http://localhost:7777/lume/images
```
</Tab>
<Tab value="Python">
```python
import requests
r = requests.get("http://localhost:7777/lume/images", timeout=50)
print(r.json())
```
</Tab>
<Tab value="TypeScript">
```typescript
const res = await fetch('http://localhost:7777/lume/images');
console.log(await res.json());
```
</Tab>
</Tabs>
```json
{
"local": ["macos-sequoia-xcode:latest", "macos-sequoia-vanilla:latest"]
}
```
---
### Prune Images
Remove unused VM images to free up disk space.
`POST: /lume/prune`
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
curl --connect-timeout 6000 \
--max-time 5000 \
-X POST \
http://localhost:7777/lume/prune
```
</Tab>
<Tab value="Python">
```python
import requests
r = requests.post("http://localhost:7777/lume/prune", timeout=50)
print(r.json())
```
</Tab>
<Tab value="TypeScript">
```typescript
const res = await fetch('http://localhost:7777/lume/prune', {
method: 'POST',
});
console.log(await res.json());
```
</Tab>
</Tabs>
---
### Get Latest IPSW URL
Get the URL for the latest macOS IPSW file.
`GET: /lume/ipsw`
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
curl --connect-timeout 6000 \
--max-time 5000 \
http://localhost:7777/lume/ipsw
```
</Tab>
<Tab value="Python">
```python
import requests
r = requests.get("http://localhost:7777/lume/ipsw", timeout=50)
print(r.json())
```
</Tab>
<Tab value="TypeScript">
```typescript
const res = await fetch('http://localhost:7777/lume/ipsw');
console.log(await res.json());
```
</Tab>
</Tabs>
---
## Configuration Management
### Get Configuration
Get current Lume configuration settings.
`GET: /lume/config`
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
curl --connect-timeout 6000 \
--max-time 5000 \
http://localhost:7777/lume/config
```
</Tab>
<Tab value="Python">
```python
import requests
r = requests.get("http://localhost:7777/lume/config", timeout=50)
print(r.json())
```
</Tab>
<Tab value="TypeScript">
```typescript
const res = await fetch('http://localhost:7777/lume/config');
console.log(await res.json());
```
</Tab>
</Tabs>
```json
{
"homeDirectory": "~/.lume",
"cacheDirectory": "~/.lume/cache",
"cachingEnabled": true
}
```
### Update Configuration
Update Lume configuration settings.
`POST: /lume/config`
#### Parameters
| Name | Type | Required | Description |
| -------------- | ------- | -------- | ------------------------- |
| homeDirectory | string | No | Lume home directory path |
| cacheDirectory | string | No | Cache directory path |
| cachingEnabled | boolean | No | Enable or disable caching |
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
curl --connect-timeout 6000 \
--max-time 5000 \
-X POST \
-H "Content-Type: application/json" \
-d '{
"homeDirectory": "~/custom/lume",
"cacheDirectory": "~/custom/lume/cache",
"cachingEnabled": true
}' \
http://localhost:7777/lume/config
```
</Tab>
<Tab value="Python">
```python
import requests
payload = {
"homeDirectory": "~/custom/lume",
"cacheDirectory": "~/custom/lume/cache",
"cachingEnabled": True
}
r = requests.post("http://localhost:7777/lume/config", json=payload, timeout=50)
print(r.json())
```
</Tab>
<Tab value="TypeScript">
```typescript
const payload = {
homeDirectory: '~/custom/lume',
cacheDirectory: '~/custom/lume/cache',
cachingEnabled: true,
};
const res = await fetch('http://localhost:7777/lume/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
console.log(await res.json());
```
</Tab>
</Tabs>
---
## Storage Location Management
### Get VM Storage Locations
List all configured VM storage locations.
`GET: /lume/config/locations`
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
curl --connect-timeout 6000 \
--max-time 5000 \
http://localhost:7777/lume/config/locations
```
</Tab>
<Tab value="Python">
```python
import requests
r = requests.get("http://localhost:7777/lume/config/locations", timeout=50)
print(r.json())
```
</Tab>
<Tab value="TypeScript">
```typescript
const res = await fetch('http://localhost:7777/lume/config/locations');
console.log(await res.json());
```
</Tab>
</Tabs>
```json
[
{
"name": "default",
"path": "~/.lume/vms",
"isDefault": true
},
{
"name": "ssd",
"path": "/Volumes/SSD/lume/vms",
"isDefault": false
}
]
```
### Add VM Storage Location
Add a new VM storage location.
`POST: /lume/config/locations`
#### Parameters
| Name | Type | Required | Description |
| ---- | ------ | -------- | ---------------------------- |
| name | string | Yes | Storage location name |
| path | string | Yes | File system path for storage |
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
curl --connect-timeout 6000 \
--max-time 5000 \
-X POST \
-H "Content-Type: application/json" \
-d '{
"name": "ssd",
"path": "/Volumes/SSD/lume/vms"
}' \
http://localhost:7777/lume/config/locations
```
</Tab>
<Tab value="Python">
```python
import requests
payload = {
"name": "ssd",
"path": "/Volumes/SSD/lume/vms"
}
r = requests.post("http://localhost:7777/lume/config/locations", json=payload, timeout=50)
print(r.json())
```
</Tab>
<Tab value="TypeScript">
```typescript
const payload = {
name: 'ssd',
path: '/Volumes/SSD/lume/vms',
};
const res = await fetch('http://localhost:7777/lume/config/locations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
console.log(await res.json());
```
</Tab>
</Tabs>
### Remove VM Storage Location
Remove a VM storage location.
`DELETE: /lume/config/locations/:name`
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
curl --connect-timeout 6000 \
--max-time 5000 \
-X DELETE \
http://localhost:7777/lume/config/locations/ssd
```
</Tab>
<Tab value="Python">
```python
import requests
r = requests.delete("http://localhost:7777/lume/config/locations/ssd", timeout=50)
print(r.status_code)
```
</Tab>
<Tab value="TypeScript">
```typescript
const res = await fetch('http://localhost:7777/lume/config/locations/ssd', {
method: 'DELETE',
});
console.log(res.status);
```
</Tab>
</Tabs>
### Set Default VM Storage Location
Set a storage location as the default.
`POST: /lume/config/locations/default/:name`
#### Example Request
<Tabs groupId="language" persist items={['Curl', 'Python', 'TypeScript']}>
<Tab value="Curl">
```bash
curl --connect-timeout 6000 \
--max-time 5000 \
-X POST \
http://localhost:7777/lume/config/locations/default/ssd
```
</Tab>
<Tab value="Python">
```python
import requests
r = requests.post("http://localhost:7777/lume/config/locations/default/ssd", timeout=50)
print(r.json())
```
</Tab>
<Tab value="TypeScript">
```typescript
const res = await fetch('http://localhost:7777/lume/config/locations/default/ssd', {
method: 'POST',
});
console.log(await res.json());
```
</Tab>
</Tabs>
```
--------------------------------------------------------------------------------
/libs/lume/src/Server/Handlers.swift:
--------------------------------------------------------------------------------
```swift
import ArgumentParser
import Foundation
import Virtualization
@MainActor
extension Server {
// MARK: - VM Management Handlers
func handleListVMs(storage: String? = nil) async throws -> HTTPResponse {
do {
let vmController = LumeController()
let vms = try vmController.list(storage: storage)
return try .json(vms)
} catch {
print(
"ERROR: Failed to list VMs: \(error.localizedDescription), storage=\(String(describing: storage))"
)
return .badRequest(message: error.localizedDescription)
}
}
func handleGetVM(name: String, storage: String? = nil) async throws -> HTTPResponse {
print("Getting VM details: name=\(name), storage=\(String(describing: storage))")
do {
let vmController = LumeController()
print("Created VM controller, attempting to get VM")
let vm = try vmController.get(name: name, storage: storage)
print("Successfully retrieved VM")
// Check for nil values that might cause crashes
if vm.vmDirContext.config.macAddress == nil {
print("ERROR: VM has nil macAddress")
return .badRequest(message: "VM configuration is invalid (nil macAddress)")
}
print("MacAddress check passed")
// Log that we're about to access details
print("Preparing VM details response")
// Print the full details object for debugging
let details = vm.details
print("VM DETAILS: \(details)")
print(" name: \(details.name)")
print(" os: \(details.os)")
print(" cpuCount: \(details.cpuCount)")
print(" memorySize: \(details.memorySize)")
print(" diskSize: \(details.diskSize)")
print(" display: \(details.display)")
print(" status: \(details.status)")
print(" vncUrl: \(String(describing: details.vncUrl))")
print(" ipAddress: \(String(describing: details.ipAddress))")
print(" locationName: \(details.locationName)")
// Serialize the VM details
print("About to serialize VM details")
let response = try HTTPResponse.json(vm.details)
print("Successfully serialized VM details")
return response
} catch {
// This will catch errors from both vmController.get and the json serialization
print("ERROR: Failed to get VM details: \(error.localizedDescription)")
return .badRequest(message: error.localizedDescription)
}
}
func handleCreateVM(_ body: Data?) async throws -> HTTPResponse {
guard let body = body,
let request = try? JSONDecoder().decode(CreateVMRequest.self, from: body)
else {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: "Invalid request body"))
)
}
do {
let sizes = try request.parse()
let vmController = LumeController()
try await vmController.create(
name: request.name,
os: request.os,
diskSize: sizes.diskSize,
cpuCount: request.cpu,
memorySize: sizes.memory,
display: request.display,
ipsw: request.ipsw,
storage: request.storage
)
return HTTPResponse(
statusCode: .ok,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode([
"message": "VM created successfully", "name": request.name,
])
)
} catch {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: error.localizedDescription))
)
}
}
func handleDeleteVM(name: String, storage: String? = nil) async throws -> HTTPResponse {
do {
let vmController = LumeController()
try await vmController.delete(name: name, storage: storage)
return HTTPResponse(
statusCode: .ok, headers: ["Content-Type": "application/json"], body: Data())
} catch {
return HTTPResponse(
statusCode: .badRequest, headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: error.localizedDescription)))
}
}
func handleCloneVM(_ body: Data?) async throws -> HTTPResponse {
guard let body = body,
let request = try? JSONDecoder().decode(CloneRequest.self, from: body)
else {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: "Invalid request body"))
)
}
do {
let vmController = LumeController()
try vmController.clone(
name: request.name,
newName: request.newName,
sourceLocation: request.sourceLocation,
destLocation: request.destLocation
)
return HTTPResponse(
statusCode: .ok,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode([
"message": "VM cloned successfully",
"source": request.name,
"destination": request.newName,
])
)
} catch {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: error.localizedDescription))
)
}
}
// MARK: - VM Operation Handlers
func handleSetVM(name: String, body: Data?) async throws -> HTTPResponse {
guard let body = body,
let request = try? JSONDecoder().decode(SetVMRequest.self, from: body)
else {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: "Invalid request body"))
)
}
do {
let vmController = LumeController()
let sizes = try request.parse()
try vmController.updateSettings(
name: name,
cpu: request.cpu,
memory: sizes.memory,
diskSize: sizes.diskSize,
display: sizes.display?.string,
storage: request.storage
)
return HTTPResponse(
statusCode: .ok,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(["message": "VM settings updated successfully"])
)
} catch {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: error.localizedDescription))
)
}
}
func handleStopVM(name: String, storage: String? = nil) async throws -> HTTPResponse {
Logger.info(
"Stopping VM", metadata: ["name": name, "storage": String(describing: storage)])
do {
Logger.info("Creating VM controller", metadata: ["name": name])
let vmController = LumeController()
Logger.info("Calling stopVM on controller", metadata: ["name": name])
try await vmController.stopVM(name: name, storage: storage)
Logger.info(
"VM stopped, waiting 5 seconds for locks to clear", metadata: ["name": name])
// Add a delay to ensure locks are fully released before returning
for i in 1...5 {
try? await Task.sleep(nanoseconds: 1_000_000_000)
Logger.info("Lock clearing delay", metadata: ["name": name, "seconds": "\(i)/5"])
}
// Verify the VM is really in a stopped state
Logger.info("Verifying VM is stopped", metadata: ["name": name])
let vm = try? vmController.get(name: name, storage: storage)
if let vm = vm, vm.details.status == "running" {
Logger.info(
"VM still reports as running despite stop operation",
metadata: ["name": name, "severity": "warning"])
} else {
Logger.info(
"Verification complete: VM is in stopped state", metadata: ["name": name])
}
Logger.info("Returning successful response", metadata: ["name": name])
return HTTPResponse(
statusCode: .ok,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(["message": "VM stopped successfully"])
)
} catch {
Logger.error(
"Failed to stop VM",
metadata: [
"name": name,
"error": error.localizedDescription,
"storage": String(describing: storage),
])
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: error.localizedDescription))
)
}
}
func handleRunVM(name: String, body: Data?) async throws -> HTTPResponse {
Logger.info("Running VM", metadata: ["name": name])
// Log the raw body data if available
if let body = body, let bodyString = String(data: body, encoding: .utf8) {
Logger.info("Run VM raw request body", metadata: ["name": name, "body": bodyString])
} else {
Logger.info("No request body or could not decode as string", metadata: ["name": name])
}
do {
Logger.info("Creating VM controller and parsing request", metadata: ["name": name])
let request =
body.flatMap { try? JSONDecoder().decode(RunVMRequest.self, from: $0) }
?? RunVMRequest(
noDisplay: nil, sharedDirectories: nil, recoveryMode: nil, storage: nil)
Logger.info(
"Parsed request",
metadata: [
"name": name,
"noDisplay": String(describing: request.noDisplay),
"sharedDirectories": "\(request.sharedDirectories?.count ?? 0)",
"storage": String(describing: request.storage),
])
Logger.info("Parsing shared directories", metadata: ["name": name])
let dirs = try request.parse()
Logger.info(
"Successfully parsed shared directories",
metadata: ["name": name, "count": "\(dirs.count)"])
// Start VM in background
Logger.info("Starting VM in background", metadata: ["name": name])
startVM(
name: name,
noDisplay: request.noDisplay ?? false,
sharedDirectories: dirs,
recoveryMode: request.recoveryMode ?? false,
storage: request.storage
)
Logger.info("VM start initiated in background", metadata: ["name": name])
// Return response immediately
return HTTPResponse(
statusCode: .accepted,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode([
"message": "VM start initiated",
"name": name,
"status": "pending",
])
)
} catch {
Logger.error(
"Failed to run VM",
metadata: [
"name": name,
"error": error.localizedDescription,
])
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: error.localizedDescription))
)
}
}
// MARK: - Image Management Handlers
func handleIPSW() async throws -> HTTPResponse {
do {
let vmController = LumeController()
let url = try await vmController.getLatestIPSWURL()
return HTTPResponse(
statusCode: .ok,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(["url": url.absoluteString])
)
} catch {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: error.localizedDescription))
)
}
}
func handlePull(_ body: Data?) async throws -> HTTPResponse {
guard let body = body,
let request = try? JSONDecoder().decode(PullRequest.self, from: body)
else {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: "Invalid request body"))
)
}
do {
let vmController = LumeController()
try await vmController.pullImage(
image: request.image,
name: request.name,
registry: request.registry,
organization: request.organization,
storage: request.storage
)
return HTTPResponse(
statusCode: .ok,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode([
"message": "Image pulled successfully",
"image": request.image,
"name": request.name ?? "default",
])
)
} catch {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: error.localizedDescription))
)
}
}
func handlePruneImages() async throws -> HTTPResponse {
do {
let vmController = LumeController()
try await vmController.pruneImages()
return HTTPResponse(
statusCode: .ok,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(["message": "Successfully removed cached images"])
)
} catch {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: error.localizedDescription))
)
}
}
func handlePush(_ body: Data?) async throws -> HTTPResponse {
guard let body = body,
let request = try? JSONDecoder().decode(PushRequest.self, from: body)
else {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: "Invalid request body"))
)
}
// Trigger push asynchronously, return Accepted immediately
Task.detached { @MainActor @Sendable in
do {
let vmController = LumeController()
try await vmController.pushImage(
name: request.name,
imageName: request.imageName,
tags: request.tags,
registry: request.registry,
organization: request.organization,
storage: request.storage,
chunkSizeMb: request.chunkSizeMb,
verbose: false, // Verbose typically handled by server logs
dryRun: false, // Default API behavior is likely non-dry-run
reassemble: false // Default API behavior is likely non-reassemble
)
print(
"Background push completed successfully for image: \(request.imageName):\(request.tags.joined(separator: ","))"
)
} catch {
print(
"Background push failed for image: \(request.imageName):\(request.tags.joined(separator: ",")) - Error: \(error.localizedDescription)"
)
}
}
return HTTPResponse(
statusCode: .accepted,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode([
"message": AnyEncodable("Push initiated in background"),
"name": AnyEncodable(request.name),
"imageName": AnyEncodable(request.imageName),
"tags": AnyEncodable(request.tags),
])
)
}
func handleGetImages(_ request: HTTPRequest) async throws -> HTTPResponse {
let pathAndQuery = request.path.split(separator: "?", maxSplits: 1)
let queryParams =
pathAndQuery.count > 1
? pathAndQuery[1]
.split(separator: "&")
.reduce(into: [String: String]()) { dict, param in
let parts = param.split(separator: "=", maxSplits: 1)
if parts.count == 2 {
dict[String(parts[0])] = String(parts[1])
}
} : [:]
let organization = queryParams["organization"] ?? "trycua"
do {
let vmController = LumeController()
let imageList = try await vmController.getImages(organization: organization)
// Create a response format that matches the CLI output
let response = imageList.local.map {
[
"repository": $0.repository,
"imageId": $0.imageId,
]
}
return HTTPResponse(
statusCode: .ok,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(response)
)
} catch {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: error.localizedDescription))
)
}
}
// MARK: - Config Management Handlers
func handleGetConfig() async throws -> HTTPResponse {
do {
let vmController = LumeController()
let settings = vmController.getSettings()
return try .json(settings)
} catch {
return .badRequest(message: error.localizedDescription)
}
}
struct ConfigRequest: Codable {
let homeDirectory: String?
let cacheDirectory: String?
let cachingEnabled: Bool?
}
func handleUpdateConfig(_ body: Data?) async throws -> HTTPResponse {
guard let body = body,
let request = try? JSONDecoder().decode(ConfigRequest.self, from: body)
else {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: "Invalid request body"))
)
}
do {
let vmController = LumeController()
if let homeDir = request.homeDirectory {
try vmController.setHomeDirectory(homeDir)
}
if let cacheDir = request.cacheDirectory {
try vmController.setCacheDirectory(path: cacheDir)
}
if let cachingEnabled = request.cachingEnabled {
try vmController.setCachingEnabled(cachingEnabled)
}
return HTTPResponse(
statusCode: .ok,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(["message": "Configuration updated successfully"])
)
} catch {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: error.localizedDescription))
)
}
}
func handleGetLocations() async throws -> HTTPResponse {
do {
let vmController = LumeController()
let locations = vmController.getLocations()
return try .json(locations)
} catch {
return .badRequest(message: error.localizedDescription)
}
}
struct LocationRequest: Codable {
let name: String
let path: String
}
func handleAddLocation(_ body: Data?) async throws -> HTTPResponse {
guard let body = body,
let request = try? JSONDecoder().decode(LocationRequest.self, from: body)
else {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: "Invalid request body"))
)
}
do {
let vmController = LumeController()
try vmController.addLocation(name: request.name, path: request.path)
return HTTPResponse(
statusCode: .ok,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode([
"message": "Location added successfully",
"name": request.name,
"path": request.path,
])
)
} catch {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: error.localizedDescription))
)
}
}
func handleRemoveLocation(_ name: String) async throws -> HTTPResponse {
do {
let vmController = LumeController()
try vmController.removeLocation(name: name)
return HTTPResponse(
statusCode: .ok,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(["message": "Location removed successfully"])
)
} catch {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: error.localizedDescription))
)
}
}
func handleSetDefaultLocation(_ name: String) async throws -> HTTPResponse {
do {
let vmController = LumeController()
try vmController.setDefaultLocation(name: name)
return HTTPResponse(
statusCode: .ok,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(["message": "Default location set successfully"])
)
} catch {
return HTTPResponse(
statusCode: .badRequest,
headers: ["Content-Type": "application/json"],
body: try JSONEncoder().encode(APIError(message: error.localizedDescription))
)
}
}
// MARK: - Log Handlers
func handleGetLogs(type: String?, lines: Int?) async throws -> HTTPResponse {
do {
let logType = type?.lowercased() ?? "all"
let infoPath = "/tmp/lume_daemon.log"
let errorPath = "/tmp/lume_daemon.error.log"
let fileManager = FileManager.default
var response: [String: String] = [:]
// Function to read log files
func readLogFile(path: String) -> String? {
guard fileManager.fileExists(atPath: path) else {
return nil
}
do {
let content = try String(contentsOfFile: path, encoding: .utf8)
// If lines parameter is provided, return only the specified number of lines from the end
if let lineCount = lines {
let allLines = content.components(separatedBy: .newlines)
let startIndex = max(0, allLines.count - lineCount)
let lastLines = Array(allLines[startIndex...])
return lastLines.joined(separator: "\n")
}
return content
} catch {
return "Error reading log file: \(error.localizedDescription)"
}
}
// Get logs based on requested type
if logType == "info" || logType == "all" {
response["info"] = readLogFile(path: infoPath) ?? "Info log file not found"
}
if logType == "error" || logType == "all" {
response["error"] = readLogFile(path: errorPath) ?? "Error log file not found"
}
return try .json(response)
} catch {
return .badRequest(message: error.localizedDescription)
}
}
// MARK: - Private Helper Methods
nonisolated private func startVM(
name: String,
noDisplay: Bool,
sharedDirectories: [SharedDirectory] = [],
recoveryMode: Bool = false,
storage: String? = nil
) {
Logger.info(
"Starting VM in detached task",
metadata: [
"name": name,
"noDisplay": "\(noDisplay)",
"recoveryMode": "\(recoveryMode)",
"storage": String(describing: storage),
])
Task.detached { @MainActor @Sendable in
Logger.info("Background task started for VM", metadata: ["name": name])
do {
Logger.info("Creating VM controller in background task", metadata: ["name": name])
let vmController = LumeController()
Logger.info(
"Calling runVM on controller",
metadata: [
"name": name,
"noDisplay": "\(noDisplay)",
])
try await vmController.runVM(
name: name,
noDisplay: noDisplay,
sharedDirectories: sharedDirectories,
recoveryMode: recoveryMode,
storage: storage
)
Logger.info("VM started successfully in background task", metadata: ["name": name])
} catch {
Logger.error(
"Failed to start VM in background task",
metadata: [
"name": name,
"error": error.localizedDescription,
])
}
}
Logger.info("Background task dispatched for VM", metadata: ["name": name])
}
}
```