UNPKG

smooth-operator-agent-tools

Version:

Node.js client library for Smooth Operator Agent Tools - a toolkit for programmers developing Computer Use Agents on Windows systems

354 lines 15.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.SmoothOperatorClient = void 0; const child_process_1 = require("child_process"); const fs = __importStar(require("fs")); // Keep fs for port file handling and path checks const path = __importStar(require("path")); const os = __importStar(require("os")); const http = __importStar(require("http")); const util_1 = require("util"); // Keep promisify for fs functions const screenshot_api_1 = require("./api/screenshot-api"); const system_api_1 = require("./api/system-api"); const mouse_api_1 = require("./api/mouse-api"); const keyboard_api_1 = require("./api/keyboard-api"); const chrome_api_1 = require("./api/chrome-api"); const automation_api_1 = require("./api/automation-api"); const code_api_1 = require("./api/code-api"); // Promisify fs functions needed for startServer const fsExists = (0, util_1.promisify)(fs.exists); const fsReadFile = (0, util_1.promisify)(fs.readFile); const fsUnlink = (0, util_1.promisify)(fs.unlink); const LOG_PREFIX = 'Smooth Operator Client:'; // Define the log prefix // Helper function to recursively convert object keys to camelCase function keysToCamelCase(obj) { if (Array.isArray(obj)) { // If it's an array, map over its elements and apply the function recursively return obj.map(v => keysToCamelCase(v)); } else if (obj !== null && typeof obj === 'object' && obj.constructor === Object) { // If it's a plain object, reduce its keys to create a new object with camelCase keys return Object.keys(obj).reduce((result, key) => { // Convert the first character to lower case const camelCaseKey = key.charAt(0).toLowerCase() + key.slice(1); // Recursively apply the function to the value result[camelCaseKey] = keysToCamelCase(obj[key]); return result; }, {}); // Start with an empty object } // Return primitives and non-plain objects as is return obj; } /** * Main client for the Smooth Operator Agent Tools API */ class SmoothOperatorClient { // Removed static initializer block for installation /** * Creates a new instance of the SmoothOperatorClient * @param apiKey Optional: API key for authentication. Most methods don't require an API Key, but for some, especially the ones that use AI, you need to provide a Screengrasp.com API Key * @param baseUrl Optional: Base URL of the API. By Default the url is automatically determined by calling startServer(), alternatively you can also just point to an already running Server instance by providing its base url here. */ constructor(apiKey, baseUrl) { this.baseUrl = null; this.serverProcess = null; this.disposed = false; this.apiKey = null; // Added to store API key this.apiKey = apiKey || null; // Store the API key this.baseUrl = baseUrl || null; this.httpClient = http; // Initialize API categories this.screenshot = new screenshot_api_1.ScreenshotApi(this); this.system = new system_api_1.SystemApi(this); this.mouse = new mouse_api_1.MouseApi(this); this.keyboard = new keyboard_api_1.KeyboardApi(this); this.chrome = new chrome_api_1.ChromeApi(this); this.automation = new automation_api_1.AutomationApi(this); this.code = new code_api_1.CodeApi(this); } /** * Starts the Smooth Operator Agent Tools Server * @throws Error when server is already running or base URL is already set manually * @throws Error when server files cannot be extracted or accessed */ async startServer() { if (this.baseUrl !== null) { throw new Error("Cannot start server when base URL has been already set."); } if (SmoothOperatorClient.LOG_TIMING) { console.log(`${new Date().toISOString()} - Starting server...`); } // Determine installation folder path (assuming postinstall script ran) const installationFolder = SmoothOperatorClient.getInstallationFolder(); if (!(await fsExists(installationFolder))) { // This should ideally not happen if postinstall ran correctly throw new Error(`Installation folder not found at ${installationFolder}. Please ensure the package installed correctly.`); } if (SmoothOperatorClient.LOG_TIMING) { console.log(`${new Date().toISOString()} - Using installation folder: ${installationFolder}`); } // Generate random port number filename const random = Math.floor(Math.random() * (100000000 - 1000000) + 1000000); const portNumberFileName = `portnr_${random}.txt`; const portNumberFilePath = path.join(installationFolder, portNumberFileName); // Delete the port number file if it exists from a previous run if (await fsExists(portNumberFilePath)) { await fsUnlink(portNumberFilePath); } // Start the server process const serverExePath = path.join(installationFolder, 'smooth-operator-server.exe'); const args = [ '/silent', '/close-with-parent-process', '/managed-by-lib', '/apikey=no_api_key_provided', `/portnrfile=${portNumberFileName}` ]; // On non-Windows platforms, use Wine to run the .exe const isWindows = process.platform === 'win32'; let spawnCommand; let spawnArgs; if (isWindows) { spawnCommand = serverExePath; spawnArgs = args; } else { spawnCommand = 'wine'; spawnArgs = [serverExePath, ...args]; } this.serverProcess = (0, child_process_1.spawn)(spawnCommand, spawnArgs, { cwd: installationFolder, detached: false }); if (!this.serverProcess || !this.serverProcess.pid) { throw new Error("Failed to start the server process."); } if (SmoothOperatorClient.LOG_TIMING) { console.log(`${new Date().toISOString()} - Server process started.`); } // Wait for the port number file to be created const maxWaitTimeMs = 30000; // 30 seconds max wait let waitedMs = 0; while (!(await fsExists(portNumberFilePath)) && waitedMs < maxWaitTimeMs) { await new Promise(resolve => setTimeout(resolve, 100)); waitedMs += 100; } if (!(await fsExists(portNumberFilePath))) { this.stopServer(); throw new Error("Server failed to report port number within the timeout period."); } // Read the port number const portNumber = (await fsReadFile(portNumberFilePath, 'utf8')).trim(); this.baseUrl = `http://localhost:${portNumber}`; await fsUnlink(portNumberFilePath); if (SmoothOperatorClient.LOG_TIMING) { console.log(`${new Date().toISOString()} - Server reported back it is running at port ${portNumber}.`); } // Check if server is running waitedMs = 0; while (true) { const startTime = Date.now(); try { const result = await this.get('/tools-api/ping'); if (result === 'pong') { break; // Server is ready for requests } } catch (_a) { // No problem, just means server is not ready yet } const elapsedMs = Date.now() - startTime; waitedMs += elapsedMs; if (waitedMs > maxWaitTimeMs) { throw new Error("Server failed to become responsive within the timeout period."); } await new Promise(resolve => setTimeout(resolve, 100)); waitedMs += 100; if (waitedMs > maxWaitTimeMs) { throw new Error("Server failed to become responsive within the timeout period."); } } if (SmoothOperatorClient.LOG_TIMING) { console.log(`${new Date().toISOString()} - Server ping successful, server is running.`); } } /** * Helper method to get the expected installation folder path. * This logic should match the postinstall script. * @returns The installation folder path */ static getInstallationFolder() { let appDataBase; const platform = os.platform(); if (platform === 'win32') { appDataBase = process.env.APPDATA || path.join(os.homedir(), '.smooth-operator-data'); // Use APPDATA or fallback } else { // Fallback for non-windows (macOS/Linux) - matches postinstall script appDataBase = path.join(os.homedir(), '.smooth-operator-data'); console.warn(`${LOG_PREFIX} Unsupported platform: ${platform}. Using fallback installation directory: ${appDataBase}`); } return path.join(appDataBase, 'SmoothOperator', 'AgentToolsServer'); } /** * Stops the Smooth Operator Agent Tools Server if it was started by this client */ stopServer() { if (this.serverProcess && !this.serverProcess.killed) { try { this.serverProcess.kill(); // Wait up to 5 seconds for the process to exit const exitPromise = new Promise((resolve) => { if (this.serverProcess) { this.serverProcess.on('exit', () => resolve()); setTimeout(resolve, 5000); } else { resolve(); } }); // We don't await this promise since stopServer is synchronous exitPromise.then(() => { if (this.serverProcess) { this.serverProcess = null; } }); } catch (error) { // Ignore errors when trying to kill the process } } } /** * Sends a GET request to the specified endpoint * @param endpoint API endpoint * @returns Deserialized response */ async get(endpoint) { if (!this.baseUrl) { throw new Error("BaseUrl is not set. You must call startServer() first, or provide a baseUrl in the constructor."); } return new Promise((resolve, reject) => { const url = `${this.baseUrl}${endpoint}`; const options = { headers: {} }; if (this.apiKey) { options.headers['Authorization'] = `Bearer ${this.apiKey}`; } const req = http.get(url, options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { try { const rawResult = JSON.parse(data); // Convert keys from PascalCase (C#) to camelCase (TS) before resolving const result = keysToCamelCase(rawResult); resolve(result); } catch (error) { reject(new Error(`Failed to parse response: ${error}`)); } } else { reject(new Error(`Request failed with status code ${res.statusCode}`)); } }); }); req.on('error', (error) => { reject(error); }); req.end(); }); } /** * Sends a POST request to the specified endpoint * @param endpoint API endpoint * @param data Request data * @returns Deserialized response */ async post(endpoint, data = null) { if (!this.baseUrl) { throw new Error("BaseUrl is not set. You must call startServer() first, or provide a baseUrl in the constructor."); } return new Promise((resolve, reject) => { const url = `${this.baseUrl}${endpoint}`; const postData = JSON.stringify(data || {}); const options = { method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) } }; if (this.apiKey) { options.headers['Authorization'] = `Bearer ${this.apiKey}`; } const req = http.request(url, options, (res) => { let responseData = ''; res.on('data', (chunk) => { responseData += chunk; }); res.on('end', () => { if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { try { const rawResult = JSON.parse(responseData); // Convert keys from PascalCase (C#) to camelCase (TS) before resolving const result = keysToCamelCase(rawResult); resolve(result); } catch (error) { reject(new Error(`Failed to parse response: ${error}`)); } } else { reject(new Error(`Request failed with status code ${res.statusCode}`)); } }); }); req.on('error', (error) => { reject(error); }); req.write(postData); // Explicitly write the data req.end(); // Finalize the request }); } } exports.SmoothOperatorClient = SmoothOperatorClient; SmoothOperatorClient.LOG_TIMING = true; //# sourceMappingURL=smooth-operator-client.js.map