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
JavaScript
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
;