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
This commit is contained in:
parent
cae05f1504
commit
c21ed5b41e
8 changed files with 1184 additions and 0 deletions
|
|
@ -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 <vmname> --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 <vmname> --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 <vmname> <command>'"
|
||||
- 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 <vmname> --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 <vmname>'"
|
||||
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 <vmname>'"
|
||||
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 <vmname> <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 <vmname> <email>'"
|
||||
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 <vmname> <email>'"
|
||||
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 <vmname> --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 <vmname> <token>'"
|
||||
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 <vmname>'"
|
||||
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
|
||||
# =============================================================================
|
||||
|
|
|
|||
45
packages/tools/official/exe-dev/block.ts
Normal file
45
packages/tools/official/exe-dev/block.ts
Normal file
|
|
@ -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;
|
||||
6
packages/tools/official/exe-dev/index.ts
Normal file
6
packages/tools/official/exe-dev/index.ts
Normal file
|
|
@ -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';
|
||||
123
packages/tools/official/exe-dev/package.json
Normal file
123
packages/tools/official/exe-dev/package.json
Normal file
|
|
@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
652
packages/tools/official/exe-dev/src/index.ts
Normal file
652
packages/tools/official/exe-dev/src/index.ts
Normal file
|
|
@ -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<string, string>;
|
||||
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<string> {
|
||||
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<T>(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<Record<string, never>>({
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(_input: Record<string, never>): Promise<ExeVMList> {
|
||||
const output = await runExeCommand('ls --json');
|
||||
const vms = parseJsonOutput<ExeVM[]>(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<CreateInput>({
|
||||
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<ExeVM> {
|
||||
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<ExeVM>(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<VmNameInput>({
|
||||
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<VmNameInput>({
|
||||
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<ExecInput>({
|
||||
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<ExeCommandResult> {
|
||||
// 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<VmInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
vm: {
|
||||
type: 'string',
|
||||
description: 'Name of the VM',
|
||||
},
|
||||
},
|
||||
required: ['vm'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: VmInput): Promise<ExeShareInfo> {
|
||||
const output = await runExeCommand(`share show ${input.vm} --json`);
|
||||
return parseJsonOutput<ExeShareInfo>(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<VmInput>({
|
||||
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<VmInput>({
|
||||
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<SharePortInput>({
|
||||
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<ShareUserInput>({
|
||||
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<ShareRemoveUserInput>({
|
||||
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<VmInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
vm: {
|
||||
type: 'string',
|
||||
description: 'Name of the VM',
|
||||
},
|
||||
},
|
||||
required: ['vm'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: VmInput): Promise<ExeShareLink> {
|
||||
const output = await runExeCommand(`share add-link ${input.vm} --json`);
|
||||
return parseJsonOutput<ExeShareLink>(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<ShareRemoveLinkInput>({
|
||||
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<Record<string, never>>({
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(_input: Record<string, never>): Promise<ExeUserInfo> {
|
||||
const output = await runExeCommand('whoami --json');
|
||||
return parseJsonOutput<ExeUserInfo>(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<VmInput>({
|
||||
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,
|
||||
};
|
||||
11
packages/tools/official/exe-dev/tsconfig.json
Normal file
11
packages/tools/official/exe-dev/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "@tpmjs/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"incremental": false,
|
||||
"composite": false
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
10
packages/tools/official/exe-dev/tsup.config.ts
Normal file
10
packages/tools/official/exe-dev/tsup.config.ts
Normal file
|
|
@ -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',
|
||||
});
|
||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue