Initial computer typescript library creation

This commit is contained in:
Morgan Dean
2025-06-12 19:55:58 -04:00
parent 5ce6b2636e
commit 3cdc08dabd
71 changed files with 5002 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
root = true
[*]
indent_size = 2
end_of_line = lf
insert_final_newline = true

View File

@@ -0,0 +1 @@
* text=auto eol=lf

View File

@@ -0,0 +1 @@
github: sxzz

View File

@@ -0,0 +1,4 @@
{
extends: ['github>sxzz/renovate-config'],
automerge: true,
}

View File

@@ -0,0 +1,26 @@
name: Release
permissions:
contents: write
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set node
uses: actions/setup-node@v4
with:
node-version: lts/*
- run: npx changelogithub
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,38 @@
name: Unit Test
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
- name: Set node LTS
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: pnpm
- name: Install
run: pnpm install
- name: Build
run: pnpm run build
- name: Lint
run: pnpm run lint
- name: Typecheck
run: pnpm run typecheck
- name: Test
run: pnpm run test

6
libs/computer/typescript/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules
dist
*.log
.DS_Store
.eslintcache

View File

@@ -0,0 +1 @@
v24.2.0

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright © 2025 C/UA
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,119 @@
# C/UA Computer TypeScript Library
The TypeScript library for C/UA Computer - a powerful computer control and automation library.
## Overview
This library is a TypeScript port of the Python computer library, providing the same functionality for controlling virtual machines and computer interfaces. It includes:
- **Computer Class**: Main class for interacting with computers (virtual or host)
- **VM Providers**: Support for different VM providers (Lume, Lumier, Cloud)
- **Computer Interfaces**: OS-specific interfaces for controlling computers (macOS, Linux, Windows)
- **Utilities**: Helper functions for display parsing, memory parsing, logging, and telemetry
## Installation
```bash
npm install @cua/computer
# or
pnpm add @cua/computer
```
## Usage
```typescript
import { Computer } from '@cua/computer';
// Create a new computer instance
const computer = new Computer({
display: '1024x768',
memory: '8GB',
cpu: '4',
osType: 'macos',
image: 'macos-sequoia-cua:latest'
});
// Start the computer
await computer.run();
// Get the computer interface for interaction
const interface = computer.interface;
// Take a screenshot
const screenshot = await interface.getScreenshot();
// Click at coordinates
await interface.click(500, 300);
// Type text
await interface.typeText('Hello, world!');
// Stop the computer
await computer.stop();
```
## Architecture
The library is organized into several key modules:
### Core Components
- `Computer`: Main class that manages VM lifecycle and interfaces
- `ComputerOptions`: Configuration options for computer instances
### Models
- `Display`: Display configuration (width, height)
- `ComputerConfig`: Internal computer configuration
### Providers
- `BaseVMProvider`: Abstract base class for VM providers
- `VMProviderFactory`: Factory for creating provider instances
- Provider types: `LUME`, `LUMIER`, `CLOUD`
### Interfaces
- `BaseComputerInterface`: Abstract base class for OS interfaces
- `InterfaceFactory`: Factory for creating OS-specific interfaces
- Interface models: Key types, mouse buttons, accessibility tree
### Utilities
- `Logger`: Logging with different verbosity levels
- `helpers`: Default computer management and sandboxed execution
- `utils`: Display/memory parsing, timeout utilities
- `telemetry`: Usage tracking and metrics
## Development
- Install dependencies:
```bash
pnpm install
```
- Run the unit tests:
```bash
pnpm test
```
- Build the library:
```bash
pnpm build
```
- Type checking:
```bash
pnpm typecheck
```
## External Dependencies
- `sharp`: For image processing and screenshot manipulation
- Additional provider-specific packages need to be installed separately:
- `@cua/computer-lume`: For Lume provider support
- `@cua/computer-lumier`: For Lumier provider support
- `@cua/computer-cloud`: For Cloud provider support
## License
[MIT](./LICENSE) License 2025 [C/UA](https://github.com/trycua)

View File

@@ -0,0 +1,86 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": [
"build",
"node_modules"
]
},
"formatter": {
"enabled": true,
"useEditorconfig": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 80,
"attributePosition": "auto",
"bracketSpacing": true
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"useSelfClosingElements": "warn",
"noUnusedTemplateLiteral": "warn",
"noNonNullAssertion": "off"
},
"a11y": {
"useMediaCaption": "off",
"useKeyWithClickEvents": "warn",
"useKeyWithMouseEvents": "warn",
"noSvgWithoutTitle": "off",
"useButtonType": "warn",
"noAutofocus": "off"
},
"suspicious": {
"noArrayIndexKey": "off"
},
"correctness": {
"noUnusedVariables": "warn",
"noUnusedFunctionParameters": "warn",
"noUnusedImports": "warn"
},
"complexity": {
"useOptionalChain": "info"
},
"nursery": {
"useSortedClasses": {
"level": "warn",
"fix": "safe",
"options": {
"attributes": [
"className"
],
"functions": [
"cn"
]
}
}
}
}
},
"javascript": {
"formatter": {
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"trailingCommas": "es5",
"semicolons": "always",
"arrowParentheses": "always",
"bracketSameLine": false,
"quoteStyle": "single",
"attributePosition": "auto",
"bracketSpacing": true
}
}
}

View File

@@ -0,0 +1,54 @@
{
"name": "@cua/computer",
"version": "0.0.0",
"packageManager": "pnpm@10.11.0",
"description": "",
"type": "module",
"license": "MIT",
"homepage": "",
"bugs": {
"url": ""
},
"repository": {
"type": "git",
"url": "git+https://github.com/trycua/cua.git"
},
"author": "",
"funding": "",
"files": [
"dist"
],
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./package.json": "./package.json"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"lint": "biome lint --cache .",
"lint:fix": "biome lint --cache --fix .",
"build": "tsdown",
"dev": "tsdown --watch",
"test": "vitest",
"typecheck": "tsc --noEmit",
"release": "bumpp && pnpm publish",
"prepublishOnly": "pnpm run build"
},
"dependencies": {
"sharp": "^0.33.0"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/node": "^22.15.17",
"bumpp": "^10.1.0",
"happy-dom": "^17.4.7",
"tsdown": "^0.11.9",
"tsx": "^4.19.4",
"typescript": "^5.8.3",
"vitest": "^3.1.3"
}
}

2000
libs/computer/typescript/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
onlyBuiltDependencies:
- "@biomejs/biome"
- sharp

View File

@@ -0,0 +1,644 @@
import type { Display, ComputerConfig } from '../models';
import type { BaseComputerInterface } from '../interface/base';
import { InterfaceFactory } from '../interface/factory';
import type { BaseVMProvider } from '../providers/base';
import { VMProviderType } from '../providers/base';
import { VMProviderFactory } from '../providers/factory';
import { Logger, LogLevel } from '../logger';
import { recordComputerInitialization, recordVMStart, recordVMStop } from '../telemetry';
import { setDefaultComputer } from '../helpers';
import { parseDisplayString, parseImageString, sleep, withTimeout } from '../utils';
import sharp from 'sharp';
export type OSType = 'macos' | 'linux' | 'windows';
export interface ComputerOptions {
display?: Display | { width: number; height: number } | string;
memory?: string;
cpu?: string;
osType?: OSType;
name?: string;
image?: string;
sharedDirectories?: string[];
useHostComputerServer?: boolean;
verbosity?: number | LogLevel;
telemetryEnabled?: boolean;
providerType?: VMProviderType | string;
port?: number;
noVNCPort?: number;
host?: string;
storage?: string;
ephemeral?: boolean;
apiKey?: string;
experiments?: string[];
}
/**
* Computer is the main class for interacting with the computer.
*/
export class Computer {
private logger: Logger;
private vmLogger: Logger;
private interfaceLogger: Logger;
private image: string;
private port?: number;
private noVNCPort?: number;
private host: string;
private osType: OSType;
private providerType: VMProviderType | string;
private ephemeral: boolean;
private apiKey?: string;
private experiments: string[];
private storage?: string;
private sharedPath?: string;
private sharedDirectories: string[];
private _telemetryEnabled: boolean;
private _initialized: boolean = false;
private _running: boolean = false;
private verbosity: number | LogLevel;
private useHostComputerServer: boolean;
private config?: ComputerConfig;
private _providerContext?: BaseVMProvider;
private _interface?: BaseComputerInterface;
private _stopEvent?: Promise<void>;
private _keepAliveTask?: Promise<void>;
/**
* Initialize a new Computer instance.
*
* @param options Configuration options for the Computer
*/
constructor(options: ComputerOptions = {}) {
const {
display = '1024x768',
memory = '8GB',
cpu = '4',
osType = 'macos',
name = '',
image = 'macos-sequoia-cua:latest',
sharedDirectories = [],
useHostComputerServer = false,
verbosity = LogLevel.NORMAL,
telemetryEnabled = true,
providerType = VMProviderType.LUME,
port = 7777,
noVNCPort = 8006,
host = process.env.PYLUME_HOST || 'localhost',
storage,
ephemeral = false,
apiKey,
experiments = []
} = options;
this.verbosity = verbosity;
this.logger = new Logger('cua.computer', verbosity);
this.logger.info('Initializing Computer...');
// Store original parameters
this.image = image;
this.port = port;
this.noVNCPort = noVNCPort;
this.host = host;
this.osType = osType;
this.providerType = providerType;
this.ephemeral = ephemeral;
this.apiKey = apiKey;
this.experiments = experiments;
if (this.experiments.includes('app-use')) {
if (this.osType !== 'macos') {
throw new Error('App use experiment is only supported on macOS');
}
}
// The default is currently to use non-ephemeral storage
if (storage && ephemeral && storage !== 'ephemeral') {
throw new Error('Storage path and ephemeral flag cannot be used together');
}
this.storage = ephemeral ? 'ephemeral' : storage;
// For Lumier provider, store the first shared directory path to use
// for VM file sharing
this.sharedPath = undefined;
if (sharedDirectories && sharedDirectories.length > 0) {
this.sharedPath = sharedDirectories[0];
this.logger.info(`Using first shared directory for VM file sharing: ${this.sharedPath}`);
}
// Store telemetry preference
this._telemetryEnabled = telemetryEnabled;
// Configure component loggers with proper hierarchy
this.vmLogger = new Logger('cua.vm', verbosity);
this.interfaceLogger = new Logger('cua.interface', verbosity);
this.useHostComputerServer = useHostComputerServer;
if (!useHostComputerServer) {
const imageInfo = parseImageString(image);
const vmName = name || image.replace(':', '_');
// Convert display parameter to Display object
let displayConfig: Display;
if (typeof display === 'string') {
const { width, height } = parseDisplayString(display);
displayConfig = { width, height };
} else if ('width' in display && 'height' in display) {
displayConfig = display as Display;
} else {
displayConfig = display as Display;
}
this.config = {
image: imageInfo.name,
tag: imageInfo.tag,
name: vmName,
display: displayConfig,
memory,
cpu,
};
}
// Store shared directories config
this.sharedDirectories = sharedDirectories;
// Record initialization in telemetry (if enabled)
if (telemetryEnabled) {
recordComputerInitialization();
} else {
this.logger.debug('Telemetry disabled - skipping initialization tracking');
}
}
/**
* Create a virtual desktop from a list of app names, returning a DioramaComputer
* that proxies Diorama.Interface but uses diorama_cmds via the computer interface.
*
* @param apps List of application names to include in the desktop.
* @returns A proxy object with the Diorama interface, but using diorama_cmds.
*/
createDesktopFromApps(apps: string[]): any {
if (!this.experiments.includes('app-use')) {
throw new Error("App Usage is an experimental feature. Enable it by passing experiments=['app-use'] to Computer()");
}
// DioramaComputer would be imported and used here
throw new Error('DioramaComputer not yet implemented');
}
/**
* Start the computer (async context manager enter).
*/
async __aenter__(): Promise<this> {
await this.run();
return this;
}
/**
* Stop the computer (async context manager exit).
*/
async __aexit__(excType: any, excVal: any, excTb: any): Promise<void> {
await this.disconnect();
}
/**
* Initialize the VM and computer interface.
*/
async run(): Promise<string | undefined> {
// If already initialized, just log and return
if (this._initialized) {
this.logger.info('Computer already initialized, skipping initialization');
return;
}
this.logger.info('Starting computer...');
const startTime = Date.now();
try {
let ipAddress: string;
// If using host computer server
if (this.useHostComputerServer) {
this.logger.info('Using host computer server');
ipAddress = 'localhost';
// Create the interface
this._interface = InterfaceFactory.createInterfaceForOS(
this.osType,
ipAddress
);
this.logger.info('Waiting for host computer server to be ready...');
await this._interface.waitForReady();
this.logger.info('Host computer server ready');
} else {
// Start or connect to VM
this.logger.info(`Starting VM: ${this.image}`);
if (!this._providerContext) {
try {
const providerTypeName = typeof this.providerType === 'object'
? this.providerType
: this.providerType;
this.logger.verbose(`Initializing ${providerTypeName} provider context...`);
// Create VM provider instance with explicit parameters
const providerOptions = {
port: this.port,
host: this.host,
storage: this.storage,
sharedPath: this.sharedPath,
image: this.image,
verbose: this.verbosity >= LogLevel.DEBUG,
ephemeral: this.ephemeral,
noVNCPort: this.noVNCPort,
apiKey: this.apiKey
};
if (!this.config) {
throw new Error('Computer config not initialized');
}
this.config.vm_provider = await VMProviderFactory.createProvider(
this.providerType,
providerOptions
);
this._providerContext = await this.config.vm_provider.__aenter__();
this.logger.verbose('VM provider context initialized successfully');
} catch (error) {
this.logger.error(`Failed to import provider dependencies: ${error}`);
throw error;
}
}
// Run the VM
if (!this.config || !this.config.vm_provider) {
throw new Error('VM provider not initialized');
}
const runOpts = {
display: this.config.display,
memory: this.config.memory,
cpu: this.config.cpu,
shared_directories: this.sharedDirectories
};
this.logger.info(`Running VM ${this.config.name} with options:`, runOpts);
if (this._telemetryEnabled) {
recordVMStart(this.config.name, String(this.providerType));
}
const storageParam = this.ephemeral ? 'ephemeral' : this.storage;
try {
await this.config.vm_provider.runVM(
this.image,
this.config.name,
runOpts,
storageParam
);
} catch (error: any) {
if (error.message?.includes('already running')) {
this.logger.info(`VM ${this.config.name} is already running`);
} else {
throw error;
}
}
// Wait for VM to be ready
try {
this.logger.info('Waiting for VM to be ready...');
await this.waitVMReady();
// Get IP address
ipAddress = await this.getIP();
this.logger.info(`VM is ready with IP: ${ipAddress}`);
} catch (error) {
this.logger.error(`Error waiting for VM: ${error}`);
throw new Error(`VM failed to become ready: ${error}`);
}
}
// Initialize the interface
try {
// Verify we have a valid IP before initializing the interface
if (!ipAddress || ipAddress === 'unknown' || ipAddress === '0.0.0.0') {
throw new Error(`Cannot initialize interface - invalid IP address: ${ipAddress}`);
}
this.logger.info(`Initializing interface for ${this.osType} at ${ipAddress}`);
// Pass authentication credentials if using cloud provider
if (this.providerType === VMProviderType.CLOUD && this.apiKey && this.config?.name) {
this._interface = InterfaceFactory.createInterfaceForOS(
this.osType,
ipAddress,
this.apiKey,
this.config.name
);
} else {
this._interface = InterfaceFactory.createInterfaceForOS(
this.osType,
ipAddress
);
}
// Wait for the WebSocket interface to be ready
this.logger.info('Connecting to WebSocket interface...');
try {
await withTimeout(
this._interface.waitForReady(),
30000,
`Could not connect to WebSocket interface at ${ipAddress}:8000/ws`
);
this.logger.info('WebSocket interface connected successfully');
} catch (error) {
this.logger.error(`Failed to connect to WebSocket interface at ${ipAddress}`);
throw error;
}
// Create an event to keep the VM running in background if needed
if (!this.useHostComputerServer) {
// In TypeScript, we'll use a Promise instead of asyncio.Event
let resolveStop: () => void;
this._stopEvent = new Promise<void>(resolve => {
resolveStop = resolve;
});
this._keepAliveTask = this._stopEvent;
}
this.logger.info('Computer is ready');
// Set the initialization flag
this._initialized = true;
// Set this instance as the default computer for remote decorators
setDefaultComputer(this);
this.logger.info('Computer successfully initialized');
} catch (error) {
throw error;
} finally {
// Log initialization time for performance monitoring
const durationMs = Date.now() - startTime;
this.logger.debug(`Computer initialization took ${durationMs.toFixed(2)}ms`);
}
} catch (error) {
this.logger.error(`Failed to initialize computer: ${error}`);
throw new Error(`Failed to initialize computer: ${error}`);
}
return;
}
/**
* Disconnect from the computer's WebSocket interface.
*/
async disconnect(): Promise<void> {
if (this._interface) {
// Note: The interface close method would need to be implemented
// this._interface.close();
}
}
/**
* Disconnect from the computer's WebSocket interface and stop the computer.
*/
async stop(): Promise<void> {
const startTime = Date.now();
try {
this.logger.info('Stopping Computer...');
// In VM mode, first explicitly stop the VM, then exit the provider context
if (!this.useHostComputerServer && this._providerContext && this.config?.vm_provider) {
try {
this.logger.info(`Stopping VM ${this.config.name}...`);
await this.config.vm_provider.stopVM(
this.config.name,
this.storage
);
} catch (error) {
this.logger.error(`Error stopping VM: ${error}`);
}
this.logger.verbose('Closing VM provider context...');
await this.config.vm_provider.__aexit__(null, null, null);
this._providerContext = undefined;
}
await this.disconnect();
this.logger.info('Computer stopped');
} catch (error) {
this.logger.debug(`Error during cleanup: ${error}`);
} finally {
// Log stop time for performance monitoring
const durationMs = Date.now() - startTime;
this.logger.debug(`Computer stop process took ${durationMs.toFixed(2)}ms`);
if (this._telemetryEnabled && this.config?.name) {
recordVMStop(this.config.name, durationMs);
}
}
}
/**
* Get the IP address of the VM or localhost if using host computer server.
*/
async getIP(maxRetries: number = 15, retryDelay: number = 2): Promise<string> {
// For host computer server, always return localhost immediately
if (this.useHostComputerServer) {
return '127.0.0.1';
}
// Get IP from the provider
if (!this.config?.vm_provider) {
throw new Error('VM provider is not initialized');
}
// Log that we're waiting for the IP
this.logger.info(`Waiting for VM ${this.config.name} to get an IP address...`);
// Call the provider's get_ip method which will wait indefinitely
const storageParam = this.ephemeral ? 'ephemeral' : this.storage;
// Log the image being used
this.logger.info(`Running VM using image: ${this.image}`);
// Call provider.getIP with explicit parameters
const ip = await this.config.vm_provider.getIP(
this.config.name,
storageParam,
retryDelay
);
// Log success
this.logger.info(`VM ${this.config.name} has IP address: ${ip}`);
return ip;
}
/**
* Wait for VM to be ready with an IP address.
*/
async waitVMReady(): Promise<Record<string, any> | undefined> {
if (this.useHostComputerServer) {
return undefined;
}
const timeout = 600; // 10 minutes timeout
const interval = 2.0; // 2 seconds between checks
const startTime = Date.now() / 1000;
let lastStatus: string | undefined;
let attempts = 0;
this.logger.info(`Waiting for VM ${this.config?.name} to be ready (timeout: ${timeout}s)...`);
while ((Date.now() / 1000) - startTime < timeout) {
attempts++;
const elapsed = (Date.now() / 1000) - startTime;
try {
// Keep polling for VM info
if (!this.config?.vm_provider) {
this.logger.error('VM provider is not initialized');
return undefined;
}
const vm = await this.config.vm_provider.getVM(this.config.name);
// Log full VM properties for debugging (every 30 attempts)
if (attempts % 30 === 0) {
this.logger.info(
`VM properties at attempt ${attempts}: ${JSON.stringify(vm)}`
);
}
// Get current status for logging
const currentStatus = vm?.status;
if (currentStatus !== lastStatus) {
this.logger.info(
`VM status changed to: ${currentStatus} (after ${elapsed.toFixed(1)}s)`
);
lastStatus = currentStatus;
}
// Check if VM is ready
if (vm && vm.status === 'running' && vm.ip_address && vm.ip_address !== '0.0.0.0') {
this.logger.info(`VM ${this.config.name} is ready with IP: ${vm.ip_address}`);
return vm;
}
// Wait before next check
await sleep(interval * 1000);
} catch (error) {
this.logger.error(`Error checking VM status: ${error}`);
await sleep(interval * 1000);
}
}
throw new Error(`VM ${this.config?.name} failed to become ready within ${timeout} seconds`);
}
/**
* Update VM settings.
*/
async update(cpu?: number, memory?: string): Promise<void> {
if (this.useHostComputerServer) {
this.logger.warning('Cannot update settings for host computer server');
return;
}
if (!this.config?.vm_provider) {
throw new Error('VM provider is not initialized');
}
await this.config.vm_provider.updateVM(
this.config.name,
cpu,
memory,
this.storage
);
}
/**
* Get the dimensions of a screenshot.
*/
async getScreenshotSize(screenshot: Buffer): Promise<{ width: number; height: number }> {
const metadata = await sharp(screenshot).metadata();
return {
width: metadata.width || 0,
height: metadata.height || 0
};
}
/**
* Get the computer interface for interacting with the VM.
*/
get interface(): BaseComputerInterface {
if (!this._interface) {
throw new Error('Computer interface not initialized. Call run() first.');
}
return this._interface;
}
/**
* Check if telemetry is enabled for this computer instance.
*/
get telemetryEnabled(): boolean {
return this._telemetryEnabled;
}
/**
* Convert normalized coordinates to screen coordinates.
*/
toScreenCoordinates(x: number, y: number): [number, number] {
if (!this.config?.display) {
throw new Error('Display configuration not available');
}
return [
x * this.config.display.width,
y * this.config.display.height
];
}
/**
* Convert screen coordinates to screenshot coordinates.
*/
async toScreenshotCoordinates(x: number, y: number): Promise<[number, number]> {
// In the Python version, this uses the interface to get screenshot dimensions
// For now, we'll assume 1:1 mapping
return [x, y];
}
/**
* Install packages in a virtual environment.
*/
async venvInstall(venvName: string, requirements: string[]): Promise<[string, string]> {
// This would be implemented using the interface to run commands
// TODO: Implement venvInstall
throw new Error('venvInstall not yet implemented');
}
/**
* Execute a shell command in a virtual environment.
*/
async venvCmd(venvName: string, command: string): Promise<[string, string]> {
// This would be implemented using the interface to run commands
// TODO: Implement venvCmd
throw new Error('venvCmd not yet implemented');
}
/**
* Execute function in a virtual environment using source code extraction.
*/
async venvExec(venvName: string, pythonFunc: Function, ...args: any[]): Promise<any> {
// This would be implemented using the interface to run Python code
// TODO: Implement venvExec
throw new Error('venvExec not yet implemented');
}
}

View File

@@ -0,0 +1,2 @@
// Re-export the Computer class and related types
export * from './computer';

View File

@@ -0,0 +1,76 @@
/**
* Helper functions and decorators for the Computer module.
*/
import type { Computer } from './computer';
// Global reference to the default computer instance
let _defaultComputer: Computer | null = null;
/**
* Set the default computer instance to be used by the remote decorator.
*
* @param computer The computer instance to use as default
*/
export function setDefaultComputer(computer: Computer): void {
_defaultComputer = computer;
}
/**
* Get the default computer instance.
*
* @returns The default computer instance or null
*/
export function getDefaultComputer(): Computer | null {
return _defaultComputer;
}
/**
* Decorator that wraps a function to be executed remotely via computer.venvExec
*
* @param venvName Name of the virtual environment to execute in
* @param computer The computer instance to use, or "default" to use the globally set default
* @param maxRetries Maximum number of retries for the remote execution
*/
export function sandboxed(
venvName: string = 'default',
computer: Computer | 'default' = 'default',
maxRetries: number = 3
) {
return function <T extends (...args: any[]) => any>(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: Parameters<T>): Promise<ReturnType<T>> {
// Determine which computer instance to use
const comp = computer === 'default' ? _defaultComputer : computer;
if (!comp) {
throw new Error(
'No computer instance available. Either specify a computer instance or call setDefaultComputer() first.'
);
}
for (let i = 0; i < maxRetries; i++) {
try {
return await comp.venvExec(venvName, originalMethod, ...args);
} catch (error) {
console.error(`Attempt ${i + 1} failed:`, error);
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, 1000));
} else {
throw error;
}
}
}
// This should never be reached, but satisfies TypeScript's control flow analysis
throw new Error('Unexpected: maxRetries loop completed without returning or throwing');
};
return descriptor;
};
}

View File

@@ -0,0 +1,28 @@
// Core components
export { Computer } from "./computer";
export type { ComputerOptions, OSType } from "./computer";
// Models
export type { Display, ComputerConfig } from "./models";
// Provider components
export { VMProviderType, BaseVMProviderImpl } from "./providers";
export type { BaseVMProvider } from "./providers";
export { VMProviderFactory } from "./providers";
export type { VMProviderOptions } from "./providers";
// Interface components
export type { BaseComputerInterface } from "./interface";
export { InterfaceFactory } from "./interface";
export type { InterfaceOptions } from "./interface";
export { Key } from "./interface";
export type {
KeyType,
MouseButton,
NavigationKey,
SpecialKey,
ModifierKey,
FunctionKey,
AccessibilityWindow,
AccessibilityTree
} from "./interface";

View File

@@ -0,0 +1,41 @@
import type { KeyType, MouseButton, AccessibilityTree } from './models';
/**
* Base interface for computer control implementations.
*/
export interface BaseComputerInterface {
/**
* Wait for the interface to be ready.
*/
waitForReady(): Promise<void>;
/**
* Get a screenshot of the current screen.
*/
getScreenshot(): Promise<Buffer>;
/**
* Move the mouse to the specified coordinates.
*/
moveMouse(x: number, y: number): Promise<void>;
/**
* Click the mouse at the current position.
*/
click(button?: MouseButton): Promise<void>;
/**
* Type text at the current cursor position.
*/
typeText(text: string): Promise<void>;
/**
* Press a key.
*/
pressKey(key: KeyType): Promise<void>;
/**
* Get the accessibility tree.
*/
getAccessibilityTree(): Promise<AccessibilityTree>;
}

View File

@@ -0,0 +1,56 @@
import type { BaseComputerInterface } from './base';
import type { OSType } from '../computer';
export interface InterfaceOptions {
ipAddress: string;
apiKey?: string;
vmName?: string;
}
/**
* Factory for creating OS-specific computer interfaces.
*/
export class InterfaceFactory {
/**
* Create an interface for the specified OS.
*
* @param os The operating system type ('macos', 'linux', 'windows')
* @param ipAddress The IP address to connect to
* @param apiKey Optional API key for cloud providers
* @param vmName Optional VM name for cloud providers
* @returns An instance of the appropriate computer interface
*/
static createInterfaceForOS(
os: OSType,
ipAddress: string,
apiKey?: string,
vmName?: string
): BaseComputerInterface {
const options: InterfaceOptions = {
ipAddress,
apiKey,
vmName
};
switch (os) {
case 'macos':
// Dynamic import would be used in real implementation
// TODO: Implement macOS interface
throw new Error('macOS interface not yet implemented');
case 'linux':
// Dynamic import would be used in real implementation
// TODO: Implement Linux interface
throw new Error('Linux interface not yet implemented');
case 'windows':
// Dynamic import would be used in real implementation
// TODO: Implement Windows interface
throw new Error('Windows interface not yet implemented');
default:
// TODO: Implement interface for this OS
throw new Error(`Interface for OS ${os} not implemented`);
}
}
}

View File

@@ -0,0 +1,5 @@
export * from "./base";
export * from "./factory";
export * from "./models";
export * from "./macos";
export * from "./linux";

View File

@@ -0,0 +1,47 @@
import type { BaseComputerInterface } from './base';
import type { KeyType, MouseButton, AccessibilityTree } from './models';
/**
* Linux-specific implementation of the computer interface.
*/
export class LinuxComputerInterface implements BaseComputerInterface {
private ip_address: string;
constructor(ip_address: string) {
this.ip_address = ip_address;
}
async waitForReady(): Promise<void> {
// Implementation will go here
}
async getScreenshot(): Promise<Buffer> {
// Implementation will go here
return Buffer.from([]);
}
async moveMouse(x: number, y: number): Promise<void> {
// Implementation will go here
}
async click(button: MouseButton = 'left'): Promise<void> {
// Implementation will go here
}
async typeText(text: string): Promise<void> {
// Implementation will go here
}
async pressKey(key: KeyType): Promise<void> {
// Implementation will go here
}
async getAccessibilityTree(): Promise<AccessibilityTree> {
// Implementation will go here
return {
success: false,
frontmost_application: '',
windows: []
};
}
}

View File

@@ -0,0 +1,47 @@
import type { BaseComputerInterface } from './base';
import type { KeyType, MouseButton, AccessibilityTree } from './models';
/**
* macOS-specific implementation of the computer interface.
*/
export class MacOSComputerInterface implements BaseComputerInterface {
private ip_address: string;
constructor(ip_address: string) {
this.ip_address = ip_address;
}
async waitForReady(): Promise<void> {
// Implementation will go here
}
async getScreenshot(): Promise<Buffer> {
// Implementation will go here
return Buffer.from([]);
}
async moveMouse(x: number, y: number): Promise<void> {
// Implementation will go here
}
async click(button: MouseButton = 'left'): Promise<void> {
// Implementation will go here
}
async typeText(text: string): Promise<void> {
// Implementation will go here
}
async pressKey(key: KeyType): Promise<void> {
// Implementation will go here
}
async getAccessibilityTree(): Promise<AccessibilityTree> {
// Implementation will go here
return {
success: false,
frontmost_application: '',
windows: []
};
}
}

View File

@@ -0,0 +1,96 @@
/**
* Navigation key literals
*/
export type NavigationKey = 'pagedown' | 'pageup' | 'home' | 'end' | 'left' | 'right' | 'up' | 'down';
/**
* Special key literals
*/
export type SpecialKey = 'enter' | 'esc' | 'tab' | 'space' | 'backspace' | 'del';
/**
* Modifier key literals
*/
export type ModifierKey = 'ctrl' | 'alt' | 'shift' | 'win' | 'command' | 'option';
/**
* Function key literals
*/
export type FunctionKey = 'f1' | 'f2' | 'f3' | 'f4' | 'f5' | 'f6' | 'f7' | 'f8' | 'f9' | 'f10' | 'f11' | 'f12';
/**
* Keyboard keys that can be used with press_key.
*/
export enum Key {
// Navigation
PAGE_DOWN = 'pagedown',
PAGE_UP = 'pageup',
HOME = 'home',
END = 'end',
LEFT = 'left',
RIGHT = 'right',
UP = 'up',
DOWN = 'down',
// Special keys
RETURN = 'enter',
ENTER = 'enter',
ESCAPE = 'esc',
ESC = 'esc',
TAB = 'tab',
SPACE = 'space',
BACKSPACE = 'backspace',
DELETE = 'del',
// Modifier keys
ALT = 'alt',
CTRL = 'ctrl',
SHIFT = 'shift',
WIN = 'win',
COMMAND = 'command',
OPTION = 'option',
// Function keys
F1 = 'f1',
F2 = 'f2',
F3 = 'f3',
F4 = 'f4',
F5 = 'f5',
F6 = 'f6',
F7 = 'f7',
F8 = 'f8',
F9 = 'f9',
F10 = 'f10',
F11 = 'f11',
F12 = 'f12'
}
/**
* Combined key type
*/
export type KeyType = Key | NavigationKey | SpecialKey | ModifierKey | FunctionKey | string;
/**
* Key type for mouse actions
*/
export type MouseButton = 'left' | 'right' | 'middle';
/**
* Information about a window in the accessibility tree.
*/
export interface AccessibilityWindow {
app_name: string;
pid: number;
frontmost: boolean;
has_windows: boolean;
windows: Array<Record<string, any>>;
}
/**
* Complete accessibility tree information.
*/
export interface AccessibilityTree {
success: boolean;
frontmost_application: string;
windows: AccessibilityWindow[];
}

View File

@@ -0,0 +1,50 @@
/**
* Logger implementation for the Computer library.
*/
export enum LogLevel {
DEBUG = 10,
VERBOSE = 15,
INFO = 20,
NORMAL = 20,
WARNING = 30,
ERROR = 40,
}
export class Logger {
private name: string;
private verbosity: number;
constructor(name: string, verbosity: number | LogLevel = LogLevel.NORMAL) {
this.name = name;
this.verbosity = typeof verbosity === 'number' ? verbosity : verbosity;
}
private log(level: LogLevel, message: string, ...args: any[]): void {
if (level >= this.verbosity) {
const timestamp = new Date().toISOString();
const levelName = LogLevel[level];
console.log(`[${timestamp}] [${this.name}] [${levelName}] ${message}`, ...args);
}
}
debug(message: string, ...args: any[]): void {
this.log(LogLevel.DEBUG, message, ...args);
}
info(message: string, ...args: any[]): void {
this.log(LogLevel.INFO, message, ...args);
}
verbose(message: string, ...args: any[]): void {
this.log(LogLevel.VERBOSE, message, ...args);
}
warning(message: string, ...args: any[]): void {
this.log(LogLevel.WARNING, message, ...args);
}
error(message: string, ...args: any[]): void {
this.log(LogLevel.ERROR, message, ...args);
}
}

View File

@@ -0,0 +1,21 @@
/**
* Display configuration for the computer.
*/
export interface Display {
width: number;
height: number;
scale_factor?: number;
}
/**
* Computer configuration model.
*/
export interface ComputerConfig {
image: string;
tag: string;
name: string;
display: Display;
memory: string;
cpu: string;
vm_provider?: any; // Will be properly typed when implemented
}

View File

@@ -0,0 +1,140 @@
/**
* Types of VM providers available.
*/
export enum VMProviderType {
LUME = 'lume',
LUMIER = 'lumier',
CLOUD = 'cloud',
UNKNOWN = 'unknown'
}
/**
* Base interface for VM providers.
* All VM provider implementations must implement this interface.
*/
export interface BaseVMProvider {
/**
* Get the provider type.
*/
readonly providerType: VMProviderType;
/**
* Get VM information by name.
*
* @param name Name of the VM to get information for
* @param storage Optional storage path override
* @returns Dictionary with VM information including status, IP address, etc.
*/
getVM(name: string, storage?: string): Promise<Record<string, any>>;
/**
* List all available VMs.
*/
listVMs(): Promise<Array<Record<string, any>>>;
/**
* Run a VM by name with the given options.
*
* @param image VM image to run
* @param name Name for the VM
* @param runOpts Run options for the VM
* @param storage Optional storage path
* @returns VM run response
*/
runVM(
image: string,
name: string,
runOpts: Record<string, any>,
storage?: string
): Promise<Record<string, any>>;
/**
* Stop a VM by name.
*
* @param name Name of the VM to stop
* @param storage Optional storage path
*/
stopVM(name: string, storage?: string): Promise<void>;
/**
* Get the IP address of a VM.
*
* @param name Name of the VM
* @param storage Optional storage path
* @param retryDelay Delay between retries in seconds
* @returns IP address of the VM
*/
getIP(name: string, storage?: string, retryDelay?: number): Promise<string>;
/**
* Update VM settings.
*
* @param name Name of the VM
* @param cpu New CPU allocation
* @param memory New memory allocation
* @param storage Optional storage path
*/
updateVM(
name: string,
cpu?: number,
memory?: string,
storage?: string
): Promise<void>;
/**
* Context manager enter method
*/
__aenter__(): Promise<this>;
/**
* Context manager exit method
*/
__aexit__(
excType: any,
excVal: any,
excTb: any
): Promise<void>;
}
/**
* Abstract base class for VM providers that implements context manager
*/
export abstract class BaseVMProviderImpl implements BaseVMProvider {
abstract readonly providerType: VMProviderType;
abstract getVM(name: string, storage?: string): Promise<Record<string, any>>;
abstract listVMs(): Promise<Array<Record<string, any>>>;
abstract runVM(
image: string,
name: string,
runOpts: Record<string, any>,
storage?: string
): Promise<Record<string, any>>;
abstract stopVM(name: string, storage?: string): Promise<void>;
abstract getIP(name: string, storage?: string, retryDelay?: number): Promise<string>;
abstract updateVM(
name: string,
cpu?: number,
memory?: string,
storage?: string
): Promise<void>;
async __aenter__(): Promise<this> {
// Default implementation - can be overridden
return this;
}
/**
* Async context manager exit.
*
* This method is called when exiting an async context manager block.
* It handles proper cleanup of resources, including stopping any running containers.
*/
async __aexit__(
_excType: any,
_excVal: any,
_excTb: any
): Promise<void> {
// Default implementation - can be overridden
}
}

View File

@@ -0,0 +1,5 @@
/**
* Cloud VM provider implementation.
*/
export { CloudProvider } from "./provider";

View File

@@ -0,0 +1,68 @@
/**
* Cloud VM provider implementation.
*
* This provider is a placeholder for cloud-based VM provisioning.
* It will be implemented to support cloud VM services in the future.
*/
import { BaseVMProviderImpl, VMProviderType } from '../base';
export interface CloudProviderOptions {
verbose?: boolean;
apiKey?: string;
region?: string;
[key: string]: any;
}
export class CloudProvider extends BaseVMProviderImpl {
readonly providerType = VMProviderType.CLOUD;
private verbose: boolean;
private options: CloudProviderOptions;
constructor(options: CloudProviderOptions = {}) {
super();
this.verbose = options.verbose || false;
this.options = options;
}
// TODO: Implement getVM for cloud provider
async getVM(name: string, storage?: string): Promise<Record<string, any>> {
throw new Error('CloudProvider is not fully implemented yet. Please use LUME or LUMIER provider instead.');
}
// TODO: Implement listVMs for cloud provider
async listVMs(): Promise<Array<Record<string, any>>> {
throw new Error('CloudProvider is not fully implemented yet. Please use LUME or LUMIER provider instead.');
}
// TODO: Implement runVM for cloud provider
async runVM(
image: string,
name: string,
runOpts: Record<string, any>,
storage?: string
): Promise<Record<string, any>> {
throw new Error('CloudProvider is not fully implemented yet. Please use LUME or LUMIER provider instead.');
}
// TODO: Implement stopVM for cloud provider
async stopVM(name: string, storage?: string): Promise<void> {
throw new Error('CloudProvider is not fully implemented yet. Please use LUME or LUMIER provider instead.');
}
// TODO: Implement getIP for cloud provider
async getIP(name: string, storage?: string, retryDelay?: number): Promise<string> {
throw new Error('CloudProvider is not fully implemented yet. Please use LUME or LUMIER provider instead.');
}
// TODO: Implement updateVM for cloud provider
async updateVM(
name: string,
cpu?: number,
memory?: string,
storage?: string
): Promise<void> {
throw new Error('CloudProvider is not fully implemented yet. Please use LUME or LUMIER provider instead.');
}
}

View File

@@ -0,0 +1,150 @@
/**
* Factory for creating VM providers.
*/
import type { BaseVMProvider } from './base';
import { VMProviderType } from './base';
export interface VMProviderOptions {
port?: number;
host?: string;
binPath?: string;
storage?: string;
sharedPath?: string;
image?: string;
verbose?: boolean;
ephemeral?: boolean;
noVNCPort?: number;
[key: string]: any; // Allow additional provider-specific options
}
export class VMProviderFactory {
/**
* Create a VM provider instance based on the provider type.
*
* @param providerType The type of provider to create
* @param options Provider-specific options
* @returns The created VM provider instance
* @throws Error if the provider type is not supported or dependencies are missing
*/
static async createProvider(
providerType: VMProviderType | string,
options: VMProviderOptions = {}
): Promise<BaseVMProvider> {
// Convert string to enum if needed
let type: VMProviderType;
if (typeof providerType === 'string') {
const normalizedType = providerType.toLowerCase();
if (Object.values(VMProviderType).includes(normalizedType as VMProviderType)) {
type = normalizedType as VMProviderType;
} else {
type = VMProviderType.UNKNOWN;
}
} else {
type = providerType;
}
// Extract common options with defaults
const {
port = 7777,
host = 'localhost',
binPath,
storage,
sharedPath,
image,
verbose = false,
ephemeral = false,
noVNCPort,
...additionalOptions
} = options;
switch (type) {
case VMProviderType.LUME: {
try {
// Dynamic import for Lume provider
const { LumeProvider, HAS_LUME } = await import('./lume');
if (!HAS_LUME) {
throw new Error(
'The required dependencies for LumeProvider are not available. ' +
'Please ensure curl is installed and in your PATH.'
);
}
return new LumeProvider({
port,
host,
storage,
verbose,
ephemeral
});
} catch (error) {
if (error instanceof Error && error.message.includes('Cannot find module')) {
throw new Error(
'The LumeProvider module is not available. ' +
'Please install it with: npm install @cua/computer-lume'
);
}
throw error;
}
}
case VMProviderType.LUMIER: {
try {
// Dynamic import for Lumier provider
const { LumierProvider, HAS_LUMIER } = await import('./lumier');
if (!HAS_LUMIER) {
throw new Error(
'Docker is required for LumierProvider. ' +
'Please install Docker for Apple Silicon and Lume CLI before using this provider.'
);
}
return new LumierProvider({
port,
host,
storage,
sharedPath,
image: image || 'macos-sequoia-cua:latest',
verbose,
ephemeral,
noVNCPort
});
} catch (error) {
if (error instanceof Error && error.message.includes('Cannot find module')) {
throw new Error(
'The LumierProvider module is not available. ' +
'Docker and Lume CLI are required for LumierProvider. ' +
'Please install Docker for Apple Silicon and run the Lume installer script.'
);
}
throw error;
}
}
case VMProviderType.CLOUD: {
try {
// Dynamic import for Cloud provider
const { CloudProvider } = await import('./cloud');
return new CloudProvider({
verbose,
...additionalOptions
});
} catch (error) {
if (error instanceof Error && error.message.includes('Cannot find module')) {
throw new Error(
'The CloudProvider is not fully implemented yet. ' +
'Please use LUME or LUMIER provider instead.'
);
}
throw error;
}
}
default:
throw new Error(`Unsupported provider type: ${providerType}`);
}
}
}

View File

@@ -0,0 +1,12 @@
/**
* Export all provider-related modules.
*/
export * from './base';
export * from './factory';
export * from './lume_api';
// Export provider implementations
export * from './lume';
export * from './lumier';
export * from './cloud';

View File

@@ -0,0 +1,16 @@
/**
* Lume VM provider implementation.
*/
export let HAS_LUME = false;
try {
// Check if curl is available
const { execSync } = require('child_process');
execSync('which curl', { stdio: 'ignore' });
HAS_LUME = true;
} catch {
HAS_LUME = false;
}
export { LumeProvider } from './provider';

View File

@@ -0,0 +1,182 @@
/**
* Lume VM provider implementation using curl commands.
*
* This provider uses direct curl commands to interact with the Lume API,
* removing the dependency on the pylume Python package.
*/
import { BaseVMProviderImpl, VMProviderType } from '../base';
import type { LumeRunOptions } from '../lume_api';
import {
HAS_CURL,
lumeApiGet,
lumeApiRun,
lumeApiStop,
lumeApiUpdate,
lumeApiPull,
parseMemory
} from '../lume_api';
export interface LumeProviderOptions {
port?: number;
host?: string;
storage?: string;
verbose?: boolean;
ephemeral?: boolean;
}
export class LumeProvider extends BaseVMProviderImpl {
readonly providerType = VMProviderType.LUME;
private host: string;
private port: number;
private storage?: string;
private verbose: boolean;
private ephemeral: boolean;
constructor(options: LumeProviderOptions = {}) {
super();
if (!HAS_CURL) {
throw new Error(
'curl is required for LumeProvider. ' +
'Please ensure it is installed and in your PATH.'
);
}
this.host = options.host || 'localhost';
this.port = options.port || 7777;
this.storage = options.storage;
this.verbose = options.verbose || false;
this.ephemeral = options.ephemeral || false;
}
async getVM(name: string, storage?: string): Promise<Record<string, any>> {
return lumeApiGet(
name,
storage || this.storage,
this.host,
this.port,
this.verbose
);
}
async listVMs(): Promise<Array<Record<string, any>>> {
const response = await lumeApiGet(
'',
this.storage,
this.host,
this.port,
this.verbose
);
// The response should be an array of VMs
if (Array.isArray(response)) {
return response;
}
// If it's an object with a vms property
if (response.vms && Array.isArray(response.vms)) {
return response.vms;
}
// Otherwise return empty array
return [];
}
async runVM(
image: string,
name: string,
runOpts: LumeRunOptions,
storage?: string
): Promise<Record<string, any>> {
// Ensure the image is available
if (this.verbose) {
console.log(`Pulling image ${image} if needed...`);
}
try {
await lumeApiPull(image, this.host, this.port, this.verbose);
} catch (error) {
if (this.verbose) {
console.log(`Failed to pull image: ${error}`);
}
}
// Run the VM
return lumeApiRun(
image,
name,
runOpts,
storage || this.storage,
this.host,
this.port,
this.verbose
);
}
async stopVM(name: string, storage?: string): Promise<void> {
await lumeApiStop(
name,
storage || this.storage,
this.host,
this.port,
this.verbose
);
// If ephemeral, the VM should be automatically deleted after stopping
if (this.ephemeral && this.verbose) {
console.log(`VM ${name} stopped and removed (ephemeral mode)`);
}
}
async getIP(name: string, storage?: string, retryDelay: number = 1): Promise<string> {
const maxRetries = 30;
let retries = 0;
while (retries < maxRetries) {
try {
const vmInfo = await this.getVM(name, storage);
if (vmInfo.ip && vmInfo.ip !== '') {
return vmInfo.ip;
}
if (vmInfo.status === 'stopped' || vmInfo.status === 'error') {
throw new Error(`VM ${name} is in ${vmInfo.status} state`);
}
} catch (error) {
if (retries === maxRetries - 1) {
throw error;
}
}
retries++;
await new Promise(resolve => setTimeout(resolve, retryDelay * 1000));
}
throw new Error(`Failed to get IP for VM ${name} after ${maxRetries} retries`);
}
async updateVM(
name: string,
cpu?: number,
memory?: string,
storage?: string
): Promise<void> {
// Validate memory format if provided
if (memory) {
parseMemory(memory); // This will throw if invalid
}
await lumeApiUpdate(
name,
cpu,
memory,
storage || this.storage,
this.host,
this.port,
this.verbose
);
}
}

View File

@@ -0,0 +1,265 @@
/**
* Lume API utilities for interacting with the Lume VM API.
* This module provides low-level API functions used by the Lume provider.
*/
import { exec, execSync } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
export let HAS_CURL = false;
// Check for curl availability
try {
execSync("which curl", { stdio: "ignore" });
HAS_CURL = true;
} catch {
HAS_CURL = false;
}
/**
* Parse memory string to bytes.
* Supports formats like "2GB", "512MB", "1024KB", etc.
* Defaults to 1GB
*/
export function parseMemory(memory = "1GB"): number {
const match = memory.match(/^(\d+(?:\.\d+)?)\s*([KMGT]?B?)$/i);
if (!match) {
throw new Error(`Invalid memory format: ${memory}`);
}
const value = parseFloat(match[1]!);
const unit = match[2]!.toUpperCase();
const multipliers: Record<string, number> = {
B: 1,
KB: 1024,
MB: 1024 * 1024,
GB: 1024 * 1024 * 1024,
TB: 1024 * 1024 * 1024 * 1024,
K: 1024,
M: 1024 * 1024,
G: 1024 * 1024 * 1024,
T: 1024 * 1024 * 1024 * 1024,
};
return Math.floor(value * (multipliers[unit] || 1));
}
/**
* Execute a curl command and return the result.
*/
async function executeCurl(
command: string
): Promise<{ stdout: string; stderr: string }> {
try {
const { stdout, stderr } = await execAsync(command);
return { stdout, stderr };
} catch (error: any) {
throw new Error(`Curl command failed: ${error.message}`);
}
}
/**
* Get VM information using Lume API.
*/
export async function lumeApiGet(
vmName: string = "",
storage?: string,
host: string = "localhost",
port: number = 7777,
debug: boolean = false
): Promise<Record<string, any>> {
let url = `http://${host}:${port}/vms`;
if (vmName) {
url += `/${encodeURIComponent(vmName)}`;
}
const params = new URLSearchParams();
if (storage) {
params.append("storage", storage);
}
if (params.toString()) {
url += `?${params.toString()}`;
}
const command = `curl -s -X GET "${url}"`;
if (debug) {
console.log(`Executing: ${command}`);
}
const { stdout } = await executeCurl(command);
try {
return JSON.parse(stdout);
} catch (error) {
throw new Error(`Failed to parse API response: ${stdout}`);
}
}
/**
* Options for running a VM using the Lume API.
*/
export interface LumeRunOptions {
/** CPU cores to allocate to the VM */
cpu?: number;
/** Memory to allocate to the VM (e.g., "8GB", "512MB") */
memory?: string;
/** Display configuration for the VM */
display?: {
width?: number;
height?: number;
dpi?: number;
color_depth?: number;
};
/** Environment variables to set in the VM */
env?: Record<string, string>;
/** Directories to share with the VM */
shared_directories?: Record<string, string>;
/** Network configuration */
network?: {
type?: string;
bridge?: string;
nat?: boolean;
};
/** Whether to run the VM in headless mode */
headless?: boolean;
/** Whether to enable GPU acceleration */
gpu?: boolean;
/** Storage location for the VM */
storage?: string;
/** Custom VM configuration options */
vm_options?: Record<string, any>;
/** Additional provider-specific options */
[key: string]: any;
}
/**
* Run a VM using Lume API.
*/
export async function lumeApiRun(
image: string,
name: string,
runOpts: LumeRunOptions,
storage?: string,
host: string = "localhost",
port: number = 7777,
debug: boolean = false
): Promise<Record<string, any>> {
const url = `http://${host}:${port}/vms/run`;
const body: LumeRunOptions = {
image,
name,
...runOpts,
};
if (storage) {
body.storage = storage;
}
const command = `curl -s -X POST "${url}" -H "Content-Type: application/json" -d '${JSON.stringify(
body
)}'`;
if (debug) {
console.log(`Executing: ${command}`);
}
const { stdout } = await executeCurl(command);
try {
return JSON.parse(stdout);
} catch (error) {
throw new Error(`Failed to parse API response: ${stdout}`);
}
}
/**
* Stop a VM using Lume API.
*/
export async function lumeApiStop(
vmName: string,
storage?: string,
host: string = "localhost",
port: number = 7777,
debug: boolean = false
): Promise<void> {
const url = `http://${host}:${port}/vms/${encodeURIComponent(vmName)}/stop`;
const params = new URLSearchParams();
if (storage) {
params.append("storage", storage);
}
const fullUrl = params.toString() ? `${url}?${params.toString()}` : url;
const command = `curl -s -X POST "${fullUrl}"`;
if (debug) {
console.log(`Executing: ${command}`);
}
await executeCurl(command);
}
/**
* Update VM settings using Lume API.
*/
export async function lumeApiUpdate(
vmName: string,
cpu?: number,
memory?: string,
storage?: string,
host: string = "localhost",
port: number = 7777,
debug: boolean = false
): Promise<void> {
const url = `http://${host}:${port}/vms/${encodeURIComponent(vmName)}/update`;
const body: LumeRunOptions = {};
if (cpu !== undefined) {
body.cpu = cpu;
}
if (memory !== undefined) {
body.memory = memory;
}
if (storage) {
body.storage = storage;
}
const command = `curl -s -X POST "${url}" -H "Content-Type: application/json" -d '${JSON.stringify(
body
)}'`;
if (debug) {
console.log(`Executing: ${command}`);
}
await executeCurl(command);
}
/**
* Pull a VM image using Lume API.
*/
export async function lumeApiPull(
image: string,
host: string = "localhost",
port: number = 7777,
debug: boolean = false
): Promise<void> {
const url = `http://${host}:${port}/images/pull`;
const body: LumeRunOptions = { image };
const command = `curl -s -X POST "${url}" -H "Content-Type: application/json" -d '${JSON.stringify(
body
)}'`;
if (debug) {
console.log(`Executing: ${command}`);
}
await executeCurl(command);
}

View File

@@ -0,0 +1,16 @@
/**
* Lumier VM provider implementation.
*/
export let HAS_LUMIER = false;
try {
// Check if Docker is available
const { execSync } = require("child_process");
execSync("which docker", { stdio: "ignore" });
HAS_LUMIER = true;
} catch {
HAS_LUMIER = false;
}
export { LumierProvider } from "./provider";

View File

@@ -0,0 +1,401 @@
/**
* Lumier VM provider implementation.
*
* This provider uses Docker containers running the Lumier image to create
* macOS and Linux VMs. It handles VM lifecycle operations through Docker
* commands and container management.
*/
import { exec } from "child_process";
import { promisify } from "util";
import { BaseVMProviderImpl, VMProviderType } from "../base";
import {
lumeApiGet,
lumeApiRun,
lumeApiStop,
lumeApiUpdate,
} from "../lume_api";
const execAsync = promisify(exec);
export interface LumierProviderOptions {
port?: number;
host?: string;
storage?: string;
sharedPath?: string;
image?: string;
verbose?: boolean;
ephemeral?: boolean;
noVNCPort?: number;
}
export class LumierProvider extends BaseVMProviderImpl {
readonly providerType = VMProviderType.LUMIER;
private host: string;
private apiPort: number;
private vncPort?: number;
private ephemeral: boolean;
private storage?: string;
private sharedPath?: string;
private image: string;
private verbose: boolean;
private containerName?: string;
private containerId?: string;
constructor(options: LumierProviderOptions = {}) {
super();
this.host = options.host || "localhost";
this.apiPort = options.port || 7777;
this.vncPort = options.noVNCPort;
this.ephemeral = options.ephemeral || false;
// Handle ephemeral storage
if (this.ephemeral) {
this.storage = "ephemeral";
} else {
this.storage = options.storage;
}
this.sharedPath = options.sharedPath;
this.image = options.image || "macos-sequoia-cua:latest";
this.verbose = options.verbose || false;
}
/**
* Parse memory string to MB integer.
*/
private parseMemory(memoryStr: string | number): number {
if (typeof memoryStr === "number") {
return memoryStr;
}
const match = memoryStr.match(/^(\d+)([A-Za-z]*)$/);
if (match) {
const value = parseInt(match[1] || "0");
const unit = match[2]?.toUpperCase() || "";
if (unit === "GB" || unit === "G") {
return value * 1024;
} else if (unit === "MB" || unit === "M" || unit === "") {
return value;
}
}
console.warn(
`Could not parse memory string '${memoryStr}', using 8GB default`
);
return 8192; // Default to 8GB
}
/**
* Check if a Docker container exists.
*/
private async containerExists(name: string): Promise<boolean> {
try {
await execAsync(`docker inspect ${name}`);
return true;
} catch {
return false;
}
}
/**
* Get container status.
*/
private async getContainerStatus(name: string): Promise<string> {
try {
const { stdout } = await execAsync(
`docker inspect -f '{{.State.Status}}' ${name}`
);
return stdout.trim();
} catch {
return "not_found";
}
}
/**
* Start the Lumier container.
*/
private async startContainer(
name: string,
cpu: number = 4,
memory: string = "8GB",
runOpts: Record<string, any> = {}
): Promise<void> {
const memoryMB = this.parseMemory(memory);
// Build Docker run command
let dockerCmd = `docker run -d --name ${name}`;
// Add resource limits
dockerCmd += ` --cpus=${cpu}`;
dockerCmd += ` --memory=${memoryMB}m`;
// Add port mappings
dockerCmd += ` -p ${this.apiPort}:7777`;
if (this.vncPort) {
dockerCmd += ` -p ${this.vncPort}:8006`;
}
// Add storage volume if not ephemeral
if (this.storage && this.storage !== "ephemeral") {
dockerCmd += ` -v ${this.storage}:/storage`;
}
// Add shared path if specified
if (this.sharedPath) {
dockerCmd += ` -v ${this.sharedPath}:/shared`;
}
// Add environment variables
if (runOpts.env) {
for (const [key, value] of Object.entries(runOpts.env)) {
dockerCmd += ` -e ${key}=${value}`;
}
}
// Add the image
dockerCmd += ` ${this.image}`;
if (this.verbose) {
console.log(`Starting container with command: ${dockerCmd}`);
}
try {
const { stdout } = await execAsync(dockerCmd);
this.containerId = stdout.trim();
this.containerName = name;
} catch (error: any) {
throw new Error(`Failed to start container: ${error.message}`);
}
}
/**
* Wait for the API to be ready.
*/
private async waitForAPI(maxRetries: number = 30): Promise<void> {
for (let i = 0; i < maxRetries; i++) {
try {
await lumeApiGet("", undefined, this.host, this.apiPort, false);
return;
} catch {
if (i === maxRetries - 1) {
throw new Error("API failed to become ready");
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
}
async getVM(name: string, storage?: string): Promise<Record<string, any>> {
// First check if container exists
const containerStatus = await this.getContainerStatus(
this.containerName || name
);
if (containerStatus === "not_found") {
throw new Error(`Container ${name} not found`);
}
// If container is not running, return basic status
if (containerStatus !== "running") {
return {
name,
status: containerStatus,
ip: "",
cpu: 0,
memory: "0MB",
};
}
// Get VM info from API
return lumeApiGet(
name,
storage || this.storage,
this.host,
this.apiPort,
this.verbose
);
}
async listVMs(): Promise<Array<Record<string, any>>> {
// Check if our container is running
if (!this.containerName) {
return [];
}
const containerStatus = await this.getContainerStatus(this.containerName);
if (containerStatus !== "running") {
return [];
}
// Get VMs from API
const response = await lumeApiGet(
"",
this.storage,
this.host,
this.apiPort,
this.verbose
);
if (Array.isArray(response)) {
return response;
}
if (response.vms && Array.isArray(response.vms)) {
return response.vms;
}
return [];
}
async runVM(
image: string,
name: string,
runOpts: Record<string, any>,
storage?: string
): Promise<Record<string, any>> {
// Check if container already exists
const exists = await this.containerExists(name);
if (exists) {
const status = await this.getContainerStatus(name);
if (status === "running") {
// Container already running, just run VM through API
return lumeApiRun(
image,
name,
runOpts,
storage || this.storage,
this.host,
this.apiPort,
this.verbose
);
} else {
// Start existing container
await execAsync(`docker start ${name}`);
}
} else {
// Create and start new container
await this.startContainer(
name,
runOpts.cpu || 4,
runOpts.memory || "8GB",
runOpts
);
}
// Wait for API to be ready
await this.waitForAPI();
// Run VM through API
return lumeApiRun(
image,
name,
runOpts,
storage || this.storage,
this.host,
this.apiPort,
this.verbose
);
}
async stopVM(name: string, storage?: string): Promise<void> {
// First stop VM through API
try {
await lumeApiStop(
name,
storage || this.storage,
this.host,
this.apiPort,
this.verbose
);
} catch (error) {
if (this.verbose) {
console.log(`Failed to stop VM through API: ${error}`);
}
}
// Stop the container
if (this.containerName) {
try {
await execAsync(`docker stop ${this.containerName}`);
// Remove container if ephemeral
if (this.ephemeral) {
await execAsync(`docker rm ${this.containerName}`);
if (this.verbose) {
console.log(
`Container ${this.containerName} stopped and removed (ephemeral mode)`
);
}
}
} catch (error: any) {
throw new Error(`Failed to stop container: ${error.message}`);
}
}
}
async getIP(
name: string,
storage?: string,
retryDelay: number = 1
): Promise<string> {
const maxRetries = 30;
for (let i = 0; i < maxRetries; i++) {
try {
const vmInfo = await this.getVM(name, storage);
if (vmInfo.ip && vmInfo.ip !== "") {
return vmInfo.ip;
}
if (vmInfo.status === "stopped" || vmInfo.status === "error") {
throw new Error(`VM ${name} is in ${vmInfo.status} state`);
}
} catch (error) {
if (i === maxRetries - 1) {
throw error;
}
}
await new Promise((resolve) => setTimeout(resolve, retryDelay * 1000));
}
throw new Error(
`Failed to get IP for VM ${name} after ${maxRetries} retries`
);
}
async updateVM(
name: string,
cpu?: number,
memory?: string,
storage?: string
): Promise<void> {
await lumeApiUpdate(
name,
cpu,
memory,
storage || this.storage,
this.host,
this.apiPort,
this.verbose
);
}
async __aexit__(excType: any, excVal: any, excTb: any): Promise<void> {
// Clean up container if ephemeral
if (this.ephemeral && this.containerName) {
try {
await execAsync(`docker stop ${this.containerName}`);
await execAsync(`docker rm ${this.containerName}`);
} catch {
// Ignore errors during cleanup
}
}
}
}

View File

@@ -0,0 +1,115 @@
/**
* Telemetry tracking for Computer usage.
*/
interface TelemetryEvent {
event: string;
timestamp: Date;
properties?: Record<string, any>;
}
export class TelemetryManager {
private enabled: boolean = true;
private events: TelemetryEvent[] = [];
setEnabled(enabled: boolean): void {
this.enabled = enabled;
}
isEnabled(): boolean {
return this.enabled;
}
track(event: string, properties?: Record<string, any>): void {
if (!this.enabled) {
return;
}
const telemetryEvent: TelemetryEvent = {
event,
timestamp: new Date(),
properties,
};
this.events.push(telemetryEvent);
// In a real implementation, this would send to a telemetry service
// For now, just log to debug
if (process.env.NODE_ENV === "development") {
console.debug("[Telemetry]", event, properties);
}
}
getEvents(): TelemetryEvent[] {
return [...this.events];
}
clear(): void {
this.events = [];
}
}
// Singleton instance
const telemetryManager = new TelemetryManager();
/**
* Record computer initialization event
*/
export function recordComputerInitialization(): void {
telemetryManager.track("computer_initialized", {
timestamp: new Date().toISOString(),
version: process.env.npm_package_version || "unknown",
});
}
/**
* Record VM start event
*/
export function recordVMStart(vmName: string, provider: string): void {
telemetryManager.track("vm_started", {
vm_name: vmName,
provider,
timestamp: new Date().toISOString(),
});
}
/**
* Record VM stop event
*/
export function recordVMStop(vmName: string, duration: number): void {
telemetryManager.track("vm_stopped", {
vm_name: vmName,
duration_ms: duration,
timestamp: new Date().toISOString(),
});
}
/**
* Record interface action
*/
export function recordInterfaceAction(
action: string,
details?: Record<string, any>
): void {
telemetryManager.track("interface_action", {
action,
...details,
timestamp: new Date().toISOString(),
});
}
/**
* Set telemetry enabled/disabled
*/
export function setTelemetryEnabled(enabled: boolean): void {
telemetryManager.setEnabled(enabled);
}
/**
* Check if telemetry is enabled
*/
export function isTelemetryEnabled(): boolean {
return telemetryManager.isEnabled();
}
export { telemetryManager };

View File

@@ -0,0 +1,118 @@
/**
* Utility functions for the Computer module.
*/
/**
* Sleep for a specified number of milliseconds
* @param ms Number of milliseconds to sleep
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Parse a display string in format "WIDTHxHEIGHT"
* @param display Display string to parse
* @returns Object with width and height
*/
export function parseDisplayString(display: string): {
width: number;
height: number;
} {
const match = display.match(/^(\d+)x(\d+)$/);
if (!match || !match[1] || !match[2]) {
throw new Error(
"Display string must be in format 'WIDTHxHEIGHT' (e.g. '1024x768')"
);
}
return {
width: parseInt(match[1], 10),
height: parseInt(match[2], 10),
};
}
/**
* Validate image format (should be in format "image:tag")
* @param image Image string to validate
* @returns Object with image name and tag
*/
export function parseImageString(image: string): { name: string; tag: string } {
const parts = image.split(":");
if (parts.length !== 2 || !parts[0] || !parts[1]) {
throw new Error("Image must be in the format <image_name>:<tag>");
}
return {
name: parts[0],
tag: parts[1],
};
}
/**
* Convert bytes to human-readable format
* @param bytes Number of bytes
* @returns Human-readable string
*/
export function formatBytes(bytes: number): string {
const units = ["B", "KB", "MB", "GB", "TB"];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
/**
* Parse memory string (e.g., "8GB") to bytes
* @param memory Memory string
* @returns Number of bytes
*/
export function parseMemoryString(memory: string): number {
const match = memory.match(/^(\d+)(B|KB|MB|GB|TB)?$/i);
if (!match || !match[1] || !match[2]) {
throw new Error("Invalid memory format. Use format like '8GB' or '1024MB'");
}
const value = parseInt(match[1], 10);
const unit = match[2].toUpperCase();
const multipliers: Record<string, number> = {
B: 1,
KB: 1024,
MB: 1024 * 1024,
GB: 1024 * 1024 * 1024,
TB: 1024 * 1024 * 1024 * 1024,
};
return value * (multipliers[unit] || 1);
}
/**
* Create a timeout promise that rejects after specified milliseconds
* @param ms Timeout in milliseconds
* @param message Optional error message
*/
export function timeout<T>(ms: number, message?: string): Promise<T> {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(message || `Timeout after ${ms}ms`));
}, ms);
});
}
/**
* Race a promise against a timeout
* @param promise The promise to race
* @param ms Timeout in milliseconds
* @param message Optional timeout error message
*/
export async function withTimeout<T>(
promise: Promise<T>,
ms: number,
message?: string
): Promise<T> {
return Promise.race([promise, timeout<T>(ms, message)]);
}

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["es2023"],
"moduleDetection": "force",
"module": "preserve",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"types": ["node"],
"allowSyntheticDefaultImports": true,
"strict": true,
"noUnusedLocals": true,
"declaration": true,
"emitDeclarationOnly": true,
"esModuleInterop": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,10 @@
import { defineConfig } from "tsdown";
export default defineConfig([
{
entry: ["./src/index.ts"],
platform: "node",
dts: true,
external: ["child_process", "util"],
},
]);

View File

@@ -0,0 +1,3 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({})