From c21ed5b41e6a5c8a35c1e1537d5392d7f70be0b3 Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Thu, 15 Jan 2026 09:49:08 +1000 Subject: [PATCH] feat(exe-dev): add exe.dev VM management tools v0.2.3 Add 15 MCP tools for managing exe.dev virtual machines: - list, create, deleteVm, restart - VM lifecycle management - exec - execute commands on VMs via SSH - shareShow, shareSetPublic, shareSetPrivate - visibility control - sharePort - configure HTTP proxy port - shareAddUser, shareRemoveUser - user access management - shareAddLink, shareRemoveLink - shareable link management - whoami - user account info - shelleyInstall - install Shelley agent Key fixes in v0.2.3: - Properly quote SSH args to prevent local shell interpretation - Add -- separator for commands to prevent flag parsing issues - Support base64-encoded SSH key via EXE_DEV_SSH_KEY env var --- packages/tools/official/blocks.yml | 321 +++++++++ packages/tools/official/exe-dev/block.ts | 45 ++ packages/tools/official/exe-dev/index.ts | 6 + packages/tools/official/exe-dev/package.json | 123 ++++ packages/tools/official/exe-dev/src/index.ts | 652 ++++++++++++++++++ packages/tools/official/exe-dev/tsconfig.json | 11 + .../tools/official/exe-dev/tsup.config.ts | 10 + pnpm-lock.yaml | 16 + 8 files changed, 1184 insertions(+) create mode 100644 packages/tools/official/exe-dev/block.ts create mode 100644 packages/tools/official/exe-dev/index.ts create mode 100644 packages/tools/official/exe-dev/package.json create mode 100644 packages/tools/official/exe-dev/src/index.ts create mode 100644 packages/tools/official/exe-dev/tsconfig.json create mode 100644 packages/tools/official/exe-dev/tsup.config.ts diff --git a/packages/tools/official/blocks.yml b/packages/tools/official/blocks.yml index b45584f..1dde4ed 100644 --- a/packages/tools/official/blocks.yml +++ b/packages/tools/official/blocks.yml @@ -394,6 +394,33 @@ domain: fields: [wasm, format, size] description: "WebAssembly compiled artifact" + # ------------------------------------------------------------------------- + # exe.dev VM management entities + # ------------------------------------------------------------------------- + exe_vm: + fields: [name, image, status, createdAt, url] + description: "An exe.dev virtual machine instance" + + exe_vm_list: + fields: [vms, count] + description: "List of exe.dev VMs" + + exe_share_info: + fields: [vm, isPublic, port, users, links] + description: "Sharing configuration for an exe.dev VM" + + exe_share_link: + fields: [token, url, createdAt] + description: "A shareable link for VM access" + + exe_user_info: + fields: [email, sshKeys] + description: "exe.dev user account information" + + exe_command_result: + fields: [stdout, stderr, exitCode] + description: "Result of executing a command on an exe.dev VM" + # ------------------------------------------------------------------------- # Agent & workflow entities # ------------------------------------------------------------------------- @@ -5235,6 +5262,300 @@ blocks: description: "The cancelled job ID" measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance, readme_documentation] + # --------------------------------------------------------------------------- + # Q) exe.dev VM Management (15 tools) + # --------------------------------------------------------------------------- + exe.list: + type: utility + description: "List all exe.dev virtual machines for the authenticated user. Returns VM names, images, status, and URLs." + path: "exe-dev" + domain_rules: + - id: ssh_execution + description: "Must execute 'ssh exe.dev ls --json' command" + - id: json_parsing + description: "Must parse JSON output into structured VM list" + inputs: [] + outputs: + - name: vms + type: exe_vm_list + description: "List of VMs with their details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + exe.create: + type: utility + description: "Create a new exe.dev virtual machine. Supports custom images, environment variables, and Shelley prompts." + path: "exe-dev" + domain_rules: + - id: ssh_execution + description: "Must execute 'ssh exe.dev new' with appropriate flags" + - id: option_building + description: "Must properly format --name, --image, --env, --prompt flags" + inputs: + - name: name + type: string + optional: true + description: "VM name (auto-generated if not provided)" + - name: image + type: string + optional: true + description: "Container image (default: exeuntu)" + - name: env + type: object + optional: true + description: "Environment variables as key-value pairs" + - name: prompt + type: string + optional: true + description: "Initial prompt to send to Shelley after creation" + - name: command + type: string + optional: true + description: "Container command mode: auto, none, or custom command" + outputs: + - name: vm + type: exe_vm + description: "Created VM details including name and URL" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + exe.delete: + type: utility + description: "Delete an exe.dev virtual machine. This permanently removes the VM and its data." + path: "exe-dev" + domain_rules: + - id: ssh_execution + description: "Must execute 'ssh exe.dev rm --json'" + inputs: + - name: name + type: string + description: "Name of the VM to delete" + outputs: + - name: deleted + type: boolean + description: "Whether the VM was successfully deleted" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + exe.restart: + type: utility + description: "Restart an exe.dev virtual machine. Useful for applying changes or recovering from issues." + path: "exe-dev" + domain_rules: + - id: ssh_execution + description: "Must execute 'ssh exe.dev restart --json'" + inputs: + - name: name + type: string + description: "Name of the VM to restart" + outputs: + - name: restarted + type: boolean + description: "Whether the VM was successfully restarted" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + exe.exec: + type: utility + description: "Execute a command on an exe.dev VM via SSH. Returns stdout, stderr, and exit code." + path: "exe-dev" + domain_rules: + - id: ssh_execution + description: "Must execute 'ssh exe.dev ssh '" + - id: output_capture + description: "Must capture and return stdout, stderr, and exit code" + inputs: + - name: vm + type: string + description: "Name of the VM to execute on" + - name: command + type: string + description: "Command to execute on the VM" + outputs: + - name: result + type: exe_command_result + description: "Command execution result with stdout, stderr, exitCode" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + exe.shareShow: + type: utility + description: "Show the sharing configuration for an exe.dev VM including public status, port, users, and share links." + path: "exe-dev" + domain_rules: + - id: ssh_execution + description: "Must execute 'ssh exe.dev share show --json'" + inputs: + - name: vm + type: string + description: "Name of the VM" + outputs: + - name: share + type: exe_share_info + description: "Sharing configuration for the VM" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + exe.shareSetPublic: + type: utility + description: "Make an exe.dev VM's HTTP proxy publicly accessible without authentication." + path: "exe-dev" + domain_rules: + - id: ssh_execution + description: "Must execute 'ssh exe.dev share set-public '" + inputs: + - name: vm + type: string + description: "Name of the VM to make public" + outputs: + - name: success + type: boolean + description: "Whether the operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + exe.shareSetPrivate: + type: utility + description: "Make an exe.dev VM's HTTP proxy private, requiring authentication to access." + path: "exe-dev" + domain_rules: + - id: ssh_execution + description: "Must execute 'ssh exe.dev share set-private '" + inputs: + - name: vm + type: string + description: "Name of the VM to make private" + outputs: + - name: success + type: boolean + description: "Whether the operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + exe.sharePort: + type: utility + description: "Configure the HTTP proxy port for an exe.dev VM. Traffic to the VM's URL will be forwarded to this port." + path: "exe-dev" + domain_rules: + - id: ssh_execution + description: "Must execute 'ssh exe.dev share port '" + inputs: + - name: vm + type: string + description: "Name of the VM" + - name: port + type: number + description: "Port number to proxy to (e.g., 8080)" + outputs: + - name: success + type: boolean + description: "Whether the port was successfully configured" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + exe.shareAddUser: + type: utility + description: "Grant a user access to an exe.dev VM by email. Sends an invitation with optional custom message." + path: "exe-dev" + domain_rules: + - id: ssh_execution + description: "Must execute 'ssh exe.dev share add '" + inputs: + - name: vm + type: string + description: "Name of the VM" + - name: email + type: string + description: "Email address of user to invite" + - name: message + type: string + optional: true + description: "Custom invitation message" + outputs: + - name: success + type: boolean + description: "Whether the user was successfully added" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + exe.shareRemoveUser: + type: utility + description: "Revoke a user's access to an exe.dev VM by email." + path: "exe-dev" + domain_rules: + - id: ssh_execution + description: "Must execute 'ssh exe.dev share remove '" + inputs: + - name: vm + type: string + description: "Name of the VM" + - name: email + type: string + description: "Email address of user to remove" + outputs: + - name: success + type: boolean + description: "Whether the user was successfully removed" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + exe.shareAddLink: + type: utility + description: "Generate a shareable link for an exe.dev VM that can be shared with anyone." + path: "exe-dev" + domain_rules: + - id: ssh_execution + description: "Must execute 'ssh exe.dev share add-link --json'" + inputs: + - name: vm + type: string + description: "Name of the VM" + outputs: + - name: link + type: exe_share_link + description: "Generated share link with token and URL" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + exe.shareRemoveLink: + type: utility + description: "Revoke a shareable link for an exe.dev VM." + path: "exe-dev" + domain_rules: + - id: ssh_execution + description: "Must execute 'ssh exe.dev share remove-link '" + inputs: + - name: vm + type: string + description: "Name of the VM" + - name: token + type: string + description: "Token of the share link to remove" + outputs: + - name: success + type: boolean + description: "Whether the link was successfully removed" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + exe.whoami: + type: utility + description: "Get the current exe.dev user's account information including email and SSH keys." + path: "exe-dev" + domain_rules: + - id: ssh_execution + description: "Must execute 'ssh exe.dev whoami --json'" + inputs: [] + outputs: + - name: user + type: exe_user_info + description: "User account information" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + exe.shelleyInstall: + type: utility + description: "Install or upgrade the Shelley agent on an exe.dev VM to the latest version." + path: "exe-dev" + domain_rules: + - id: ssh_execution + description: "Must execute 'ssh exe.dev shelley install '" + inputs: + - name: vm + type: string + description: "Name of the VM to install Shelley on" + outputs: + - name: success + type: boolean + description: "Whether Shelley was successfully installed/upgraded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + # ============================================================================= # VALIDATORS - Which validators to run against each block # ============================================================================= diff --git a/packages/tools/official/exe-dev/block.ts b/packages/tools/official/exe-dev/block.ts new file mode 100644 index 0000000..b0d07cd --- /dev/null +++ b/packages/tools/official/exe-dev/block.ts @@ -0,0 +1,45 @@ +/** + * Block metadata for exe.dev tools + * This file provides metadata for the blocks validator + */ +import { + create, + deleteVm, + exec, + list, + restart, + shareAddLink, + shareAddUser, + sharePort, + shareRemoveLink, + shareRemoveUser, + shareSetPrivate, + shareSetPublic, + shareShow, + shelleyInstall, + whoami, +} from './src/index.js'; + +export const block = { + name: 'exe-dev', + description: 'Manage exe.dev virtual machines via SSH', + tools: { + list, + create, + deleteVm, + restart, + exec, + shareShow, + shareSetPublic, + shareSetPrivate, + sharePort, + shareAddUser, + shareRemoveUser, + shareAddLink, + shareRemoveLink, + whoami, + shelleyInstall, + }, +}; + +export default block; diff --git a/packages/tools/official/exe-dev/index.ts b/packages/tools/official/exe-dev/index.ts new file mode 100644 index 0000000..ce69085 --- /dev/null +++ b/packages/tools/official/exe-dev/index.ts @@ -0,0 +1,6 @@ +/** + * exe.dev VM Management Tools + * Re-exports from src/index.ts for blocks validator compatibility + */ +export * from './src/index.js'; +export { default } from './src/index.js'; diff --git a/packages/tools/official/exe-dev/package.json b/packages/tools/official/exe-dev/package.json new file mode 100644 index 0000000..e2df774 --- /dev/null +++ b/packages/tools/official/exe-dev/package.json @@ -0,0 +1,123 @@ +{ + "name": "@tpmjs/tools-exe-dev", + "version": "0.2.3", + "description": "Manage exe.dev virtual machines. Create, list, delete, restart VMs, manage sharing, and execute commands via SSH.", + "type": "module", + "keywords": [ + "tpmjs", + "exe.dev", + "vm", + "virtual-machine", + "ssh", + "cloud", + "ai" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rm -rf dist .turbo" + }, + "devDependencies": { + "@tpmjs/tsconfig": "workspace:*", + "tsup": "^8.5.1", + "typescript": "^5.9.3" + }, + "dependencies": { + "ai": "6.0.23" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/tpmjs/tpmjs.git", + "directory": "packages/tools/official/exe-dev" + }, + "homepage": "https://tpmjs.com", + "license": "MIT", + "tpmjs": { + "category": "ops", + "frameworks": [ + "vercel-ai" + ], + "env": [ + { + "name": "EXE_DEV_SSH_KEY", + "description": "Base64-encoded SSH private key for exe.dev authentication. Generate with: cat ~/.ssh/id_ed25519 | base64", + "required": false + } + ], + "tools": [ + { + "name": "list", + "description": "List all exe.dev virtual machines for the authenticated user." + }, + { + "name": "create", + "description": "Create a new exe.dev virtual machine with optional image, env vars, and Shelley prompt." + }, + { + "name": "deleteVm", + "description": "Delete an exe.dev virtual machine permanently." + }, + { + "name": "restart", + "description": "Restart an exe.dev virtual machine." + }, + { + "name": "exec", + "description": "Execute a command on an exe.dev VM via SSH." + }, + { + "name": "shareShow", + "description": "Show the sharing configuration for an exe.dev VM." + }, + { + "name": "shareSetPublic", + "description": "Make an exe.dev VM's HTTP proxy publicly accessible." + }, + { + "name": "shareSetPrivate", + "description": "Make an exe.dev VM's HTTP proxy private." + }, + { + "name": "sharePort", + "description": "Configure the HTTP proxy port for an exe.dev VM." + }, + { + "name": "shareAddUser", + "description": "Grant a user access to an exe.dev VM by email." + }, + { + "name": "shareRemoveUser", + "description": "Revoke a user's access to an exe.dev VM." + }, + { + "name": "shareAddLink", + "description": "Generate a shareable link for an exe.dev VM." + }, + { + "name": "shareRemoveLink", + "description": "Revoke a shareable link for an exe.dev VM." + }, + { + "name": "whoami", + "description": "Get the current exe.dev user's account information." + }, + { + "name": "shelleyInstall", + "description": "Install or upgrade Shelley agent on an exe.dev VM." + } + ] + } +} diff --git a/packages/tools/official/exe-dev/src/index.ts b/packages/tools/official/exe-dev/src/index.ts new file mode 100644 index 0000000..db18359 --- /dev/null +++ b/packages/tools/official/exe-dev/src/index.ts @@ -0,0 +1,652 @@ +/** + * exe.dev VM Management Tools for TPMJS + * Manage virtual machines on exe.dev via SSH commands. + * + * Authentication: Set EXE_DEV_SSH_KEY env var with your base64-encoded SSH private key. + * To encode your key: cat ~/.ssh/id_ed25519 | base64 + * + * Alternatively, if running locally with SSH already configured, the tools will + * fall back to your default SSH config. + */ + +import { exec as nodeExec } from 'node:child_process'; +import { chmodSync, existsSync, unlinkSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { promisify } from 'node:util'; +import { jsonSchema, tool } from 'ai'; + +const execAsync = promisify(nodeExec); + +// Temp key file path (unique per process to avoid conflicts) +const KEY_FILE_PATH = join(tmpdir(), `exe-dev-key-${process.pid}`); + +// ============================================================================ +// Types +// ============================================================================ + +export interface ExeVM { + name: string; + image?: string; + status?: string; + createdAt?: string; + url?: string; +} + +export interface ExeVMList { + vms: ExeVM[]; + count: number; +} + +export interface ExeShareInfo { + vm: string; + isPublic: boolean; + port?: number; + users: string[]; + links: ExeShareLink[]; +} + +export interface ExeShareLink { + token: string; + url: string; + createdAt?: string; +} + +export interface ExeUserInfo { + email: string; + sshKeys: string[]; +} + +export interface ExeCommandResult { + stdout: string; + stderr: string; + exitCode: number; +} + +// Input types +interface CreateInput { + name?: string; + image?: string; + env?: Record; + prompt?: string; + command?: string; +} + +interface VmNameInput { + name: string; +} + +interface ExecInput { + vm: string; + command: string; +} + +interface VmInput { + vm: string; +} + +interface SharePortInput { + vm: string; + port: number; +} + +interface ShareUserInput { + vm: string; + email: string; + message?: string; +} + +interface ShareRemoveUserInput { + vm: string; + email: string; +} + +interface ShareRemoveLinkInput { + vm: string; + token: string; +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Setup SSH key from environment variable if available. + * Returns the SSH command options to use the key, or empty string for default SSH config. + */ +function setupSshKey(): string { + const base64Key = process.env.EXE_DEV_SSH_KEY; + + if (!base64Key) { + // No env var set, fall back to default SSH config + return ''; + } + + try { + // Decode base64 key + const keyContent = Buffer.from(base64Key, 'base64').toString('utf-8'); + + // Write to temp file with secure permissions + writeFileSync(KEY_FILE_PATH, keyContent, { mode: 0o600 }); + + // Double-check permissions (some systems may ignore mode in writeFileSync) + chmodSync(KEY_FILE_PATH, 0o600); + + // Return SSH options to use this key + return `-i ${KEY_FILE_PATH} -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null`; + } catch (error) { + throw new Error( + `Failed to setup SSH key from EXE_DEV_SSH_KEY: ${error instanceof Error ? error.message : 'Unknown error'}` + ); + } +} + +/** + * Cleanup the temporary SSH key file if it exists. + */ +function cleanupSshKey(): void { + try { + if (existsSync(KEY_FILE_PATH)) { + unlinkSync(KEY_FILE_PATH); + } + } catch { + // Ignore cleanup errors + } +} + +/** + * Build the SSH command with appropriate options. + */ +function buildSshCommand(args: string): string { + const sshOptions = setupSshKey(); + // Quote the entire args to prevent local shell interpretation + const escapedArgs = args.replace(/'/g, "'\\''"); + if (sshOptions) { + return `ssh ${sshOptions} exe.dev '${escapedArgs}'`; + } + return `ssh exe.dev '${escapedArgs}'`; +} + +async function runExeCommand(args: string): Promise { + try { + const command = buildSshCommand(args); + const { stdout, stderr } = await execAsync(command, { + timeout: 60000, // 60 second timeout + }); + if (stderr && !stdout) { + throw new Error(stderr); + } + return stdout.trim(); + } catch (error) { + if (error instanceof Error) { + const message = error.message || 'Command failed'; + throw new Error(`exe.dev command failed: ${message}`); + } + throw error; + } finally { + cleanupSshKey(); + } +} + +async function runExeCommandWithExit( + args: string +): Promise<{ stdout: string; stderr: string; exitCode: number }> { + try { + const command = buildSshCommand(args); + const { stdout, stderr } = await execAsync(command, { + timeout: 60000, + }); + return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 }; + } catch (error: unknown) { + const execError = error as { stdout?: string; stderr?: string; code?: number }; + return { + stdout: execError.stdout?.trim() || '', + stderr: + execError.stderr?.trim() || (error instanceof Error ? error.message : 'Unknown error'), + exitCode: execError.code || 1, + }; + } finally { + cleanupSshKey(); + } +} + +function parseJsonOutput(output: string): T { + try { + return JSON.parse(output) as T; + } catch { + throw new Error(`Failed to parse exe.dev output as JSON: ${output}`); + } +} + +// ============================================================================ +// Tools +// ============================================================================ + +/** + * List all exe.dev VMs + */ +export const list = tool({ + description: + 'List all exe.dev virtual machines for the authenticated user. Returns VM names, images, status, and URLs. Requires SSH access to exe.dev.', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }), + async execute(_input: Record): Promise { + const output = await runExeCommand('ls --json'); + const vms = parseJsonOutput(output); + return { + vms, + count: vms.length, + }; + }, +}); + +/** + * Create a new exe.dev VM + */ +export const create = tool({ + description: + 'Create a new exe.dev virtual machine. Supports custom images, environment variables, and Shelley prompts. Requires SSH access to exe.dev.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + name: { + type: 'string', + description: 'VM name (auto-generated if not provided)', + }, + image: { + type: 'string', + description: 'Container image (default: exeuntu)', + }, + env: { + type: 'object', + description: 'Environment variables as key-value pairs', + additionalProperties: { type: 'string' }, + }, + prompt: { + type: 'string', + description: 'Initial prompt to send to Shelley after creation', + }, + command: { + type: 'string', + description: 'Container command mode: auto, none, or custom command', + }, + }, + required: [], + additionalProperties: false, + }), + async execute(input: CreateInput): Promise { + const args: string[] = ['new', '--json']; + + if (input.name) { + args.push(`--name=${input.name}`); + } + if (input.image) { + args.push(`--image=${input.image}`); + } + if (input.command) { + args.push(`--command=${input.command}`); + } + if (input.prompt) { + args.push(`--prompt="${input.prompt.replace(/"/g, '\\"')}"`); + } + if (input.env) { + for (const [key, value] of Object.entries(input.env)) { + args.push(`--env ${key}=${value}`); + } + } + + const output = await runExeCommand(args.join(' ')); + return parseJsonOutput(output); + }, +}); + +/** + * Delete an exe.dev VM + */ +export const deleteVm = tool({ + description: + 'Delete an exe.dev virtual machine. This permanently removes the VM and its data. Requires SSH access to exe.dev.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + name: { + type: 'string', + description: 'Name of the VM to delete', + }, + }, + required: ['name'], + additionalProperties: false, + }), + async execute(input: VmNameInput): Promise<{ deleted: boolean; name: string }> { + await runExeCommand(`rm ${input.name} --json`); + return { deleted: true, name: input.name }; + }, +}); + +/** + * Restart an exe.dev VM + */ +export const restart = tool({ + description: + 'Restart an exe.dev virtual machine. Useful for applying changes or recovering from issues. Requires SSH access to exe.dev.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + name: { + type: 'string', + description: 'Name of the VM to restart', + }, + }, + required: ['name'], + additionalProperties: false, + }), + async execute(input: VmNameInput): Promise<{ restarted: boolean; name: string }> { + await runExeCommand(`restart ${input.name} --json`); + return { restarted: true, name: input.name }; + }, +}); + +/** + * Execute a command on an exe.dev VM + */ +export const exec = tool({ + description: + 'Execute a command on an exe.dev VM via SSH. Returns stdout, stderr, and exit code. Requires SSH access to exe.dev.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + vm: { + type: 'string', + description: 'Name of the VM to execute on', + }, + command: { + type: 'string', + description: 'Command to execute on the VM', + }, + }, + required: ['vm', 'command'], + additionalProperties: false, + }), + async execute(input: ExecInput): Promise { + // The command is passed directly - buildSshCommand handles the quoting + const result = await runExeCommandWithExit(`ssh ${input.vm} -- ${input.command}`); + return result; + }, +}); + +/** + * Show share configuration for a VM + */ +export const shareShow = tool({ + description: + 'Show the sharing configuration for an exe.dev VM including public status, port, users, and share links. Requires SSH access to exe.dev.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + vm: { + type: 'string', + description: 'Name of the VM', + }, + }, + required: ['vm'], + additionalProperties: false, + }), + async execute(input: VmInput): Promise { + const output = await runExeCommand(`share show ${input.vm} --json`); + return parseJsonOutput(output); + }, +}); + +/** + * Make a VM public + */ +export const shareSetPublic = tool({ + description: + "Make an exe.dev VM's HTTP proxy publicly accessible without authentication. Requires SSH access to exe.dev.", + inputSchema: jsonSchema({ + type: 'object', + properties: { + vm: { + type: 'string', + description: 'Name of the VM to make public', + }, + }, + required: ['vm'], + additionalProperties: false, + }), + async execute(input: VmInput): Promise<{ success: boolean; vm: string }> { + await runExeCommand(`share set-public ${input.vm}`); + return { success: true, vm: input.vm }; + }, +}); + +/** + * Make a VM private + */ +export const shareSetPrivate = tool({ + description: + "Make an exe.dev VM's HTTP proxy private, requiring authentication to access. Requires SSH access to exe.dev.", + inputSchema: jsonSchema({ + type: 'object', + properties: { + vm: { + type: 'string', + description: 'Name of the VM to make private', + }, + }, + required: ['vm'], + additionalProperties: false, + }), + async execute(input: VmInput): Promise<{ success: boolean; vm: string }> { + await runExeCommand(`share set-private ${input.vm}`); + return { success: true, vm: input.vm }; + }, +}); + +/** + * Set proxy port for a VM + */ +export const sharePort = tool({ + description: + "Configure the HTTP proxy port for an exe.dev VM. Traffic to the VM's URL will be forwarded to this port. Requires SSH access to exe.dev.", + inputSchema: jsonSchema({ + type: 'object', + properties: { + vm: { + type: 'string', + description: 'Name of the VM', + }, + port: { + type: 'number', + description: 'Port number to proxy to (e.g., 8080)', + }, + }, + required: ['vm', 'port'], + additionalProperties: false, + }), + async execute(input: SharePortInput): Promise<{ success: boolean; vm: string; port: number }> { + await runExeCommand(`share port ${input.vm} ${input.port}`); + return { success: true, vm: input.vm, port: input.port }; + }, +}); + +/** + * Add a user to a VM + */ +export const shareAddUser = tool({ + description: + 'Grant a user access to an exe.dev VM by email. Sends an invitation with optional custom message. Requires SSH access to exe.dev.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + vm: { + type: 'string', + description: 'Name of the VM', + }, + email: { + type: 'string', + description: 'Email address of user to invite', + }, + message: { + type: 'string', + description: 'Custom invitation message', + }, + }, + required: ['vm', 'email'], + additionalProperties: false, + }), + async execute(input: ShareUserInput): Promise<{ success: boolean; vm: string; email: string }> { + let cmd = `share add ${input.vm} ${input.email}`; + if (input.message) { + cmd += ` --message="${input.message.replace(/"/g, '\\"')}"`; + } + await runExeCommand(cmd); + return { success: true, vm: input.vm, email: input.email }; + }, +}); + +/** + * Remove a user from a VM + */ +export const shareRemoveUser = tool({ + description: "Revoke a user's access to an exe.dev VM by email. Requires SSH access to exe.dev.", + inputSchema: jsonSchema({ + type: 'object', + properties: { + vm: { + type: 'string', + description: 'Name of the VM', + }, + email: { + type: 'string', + description: 'Email address of user to remove', + }, + }, + required: ['vm', 'email'], + additionalProperties: false, + }), + async execute( + input: ShareRemoveUserInput + ): Promise<{ success: boolean; vm: string; email: string }> { + await runExeCommand(`share remove ${input.vm} ${input.email}`); + return { success: true, vm: input.vm, email: input.email }; + }, +}); + +/** + * Create a share link for a VM + */ +export const shareAddLink = tool({ + description: + 'Generate a shareable link for an exe.dev VM that can be shared with anyone. Requires SSH access to exe.dev.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + vm: { + type: 'string', + description: 'Name of the VM', + }, + }, + required: ['vm'], + additionalProperties: false, + }), + async execute(input: VmInput): Promise { + const output = await runExeCommand(`share add-link ${input.vm} --json`); + return parseJsonOutput(output); + }, +}); + +/** + * Remove a share link from a VM + */ +export const shareRemoveLink = tool({ + description: 'Revoke a shareable link for an exe.dev VM. Requires SSH access to exe.dev.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + vm: { + type: 'string', + description: 'Name of the VM', + }, + token: { + type: 'string', + description: 'Token of the share link to remove', + }, + }, + required: ['vm', 'token'], + additionalProperties: false, + }), + async execute( + input: ShareRemoveLinkInput + ): Promise<{ success: boolean; vm: string; token: string }> { + await runExeCommand(`share remove-link ${input.vm} ${input.token}`); + return { success: true, vm: input.vm, token: input.token }; + }, +}); + +/** + * Get current user info + */ +export const whoami = tool({ + description: + "Get the current exe.dev user's account information including email and SSH keys. Requires SSH access to exe.dev.", + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + required: [], + additionalProperties: false, + }), + async execute(_input: Record): Promise { + const output = await runExeCommand('whoami --json'); + return parseJsonOutput(output); + }, +}); + +/** + * Install Shelley on a VM + */ +export const shelleyInstall = tool({ + description: + 'Install or upgrade the Shelley agent on an exe.dev VM to the latest version. Requires SSH access to exe.dev.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + vm: { + type: 'string', + description: 'Name of the VM to install Shelley on', + }, + }, + required: ['vm'], + additionalProperties: false, + }), + async execute(input: VmInput): Promise<{ success: boolean; vm: string }> { + await runExeCommand(`shelley install ${input.vm}`); + return { success: true, vm: input.vm }; + }, +}); + +// ============================================================================ +// Default Export +// ============================================================================ + +export default { + list, + create, + deleteVm, + restart, + exec, + shareShow, + shareSetPublic, + shareSetPrivate, + sharePort, + shareAddUser, + shareRemoveUser, + shareAddLink, + shareRemoveLink, + whoami, + shelleyInstall, +}; diff --git a/packages/tools/official/exe-dev/tsconfig.json b/packages/tools/official/exe-dev/tsconfig.json new file mode 100644 index 0000000..6521d56 --- /dev/null +++ b/packages/tools/official/exe-dev/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@tpmjs/tsconfig/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "incremental": false, + "composite": false + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/tools/official/exe-dev/tsup.config.ts b/packages/tools/official/exe-dev/tsup.config.ts new file mode 100644 index 0000000..9ff9463 --- /dev/null +++ b/packages/tools/official/exe-dev/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + dts: true, + clean: true, + sourcemap: true, + target: 'es2022', +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5718eac..36ee7f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1529,6 +1529,22 @@ importers: specifier: ^5.9.3 version: 5.9.3 + packages/tools/official/exe-dev: + dependencies: + ai: + specifier: 6.0.23 + version: 6.0.23(zod@4.3.5) + devDependencies: + '@tpmjs/tsconfig': + specifier: workspace:* + version: link:../../../config/tsconfig + tsup: + specifier: ^8.5.1 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + packages/tools/official/executive-brief: dependencies: ai: