# Directory Structure
```
├── .github
│ └── workflows
│ ├── javascript-checks.yml
│ ├── python-checks.yml
│ ├── release.yml
│ └── rust-checks.yml
├── javascript
│ ├── .gitignore
│ ├── .prettierrc
│ ├── example
│ │ ├── index.ts
│ │ ├── package.json
│ │ └── README.md
│ ├── forevervm
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── get-binary.js
│ │ │ └── run.js
│ │ └── tsconfig.json
│ ├── mcp-server
│ │ ├── .gitignore
│ │ ├── .prettierrc
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── index.ts
│ │ │ └── install
│ │ │ ├── claude.ts
│ │ │ ├── goose.ts
│ │ │ ├── index.ts
│ │ │ └── windsurf.ts
│ │ ├── tmp.json
│ │ └── tsconfig.json
│ ├── package-lock.json
│ ├── package.json
│ ├── sdk
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── env.ts
│ │ │ ├── index.ts
│ │ │ ├── repl.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── tsconfig.base.json
│ ├── tsup.config.ts
│ └── vitest.config.ts
├── LICENSE.md
├── publishing.md
├── python
│ ├── .gitignore
│ ├── forevervm
│ │ ├── forevervm
│ │ │ └── __init__.py
│ │ ├── pyproject.toml
│ │ └── uv.lock
│ └── sdk
│ ├── forevervm_sdk
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── repl.py
│ │ └── types.py
│ ├── pyproject.toml
│ ├── README.md
│ ├── tests
│ │ ├── __init__.py
│ │ └── test_connect.py
│ └── uv.lock
├── README.md
├── rust
│ ├── .gitignore
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── forevervm
│ │ ├── .gitignore
│ │ ├── Cargo.lock
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ └── src
│ │ ├── commands
│ │ │ ├── auth.rs
│ │ │ ├── machine.rs
│ │ │ ├── mod.rs
│ │ │ └── repl.rs
│ │ ├── config.rs
│ │ ├── lib.rs
│ │ ├── main.rs
│ │ └── util.rs
│ └── forevervm-sdk
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── README.md
│ ├── src
│ │ ├── api
│ │ │ ├── api_types.rs
│ │ │ ├── http_api.rs
│ │ │ ├── id_types.rs
│ │ │ ├── mod.rs
│ │ │ ├── protocol.rs
│ │ │ └── token.rs
│ │ ├── client
│ │ │ ├── error.rs
│ │ │ ├── mod.rs
│ │ │ ├── repl.rs
│ │ │ ├── typed_socket.rs
│ │ │ └── util.rs
│ │ ├── lib.rs
│ │ └── util.rs
│ └── tests
│ └── basic_sdk_tests.rs
└── scripts
├── .gitignore
├── .prettierrc
├── bump-versions.ts
├── package-lock.json
├── package.json
├── publish.ts
├── README.md
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/javascript/mcp-server/.gitignore:
--------------------------------------------------------------------------------
```
build
```
--------------------------------------------------------------------------------
/rust/.gitignore:
--------------------------------------------------------------------------------
```
target
```
--------------------------------------------------------------------------------
/rust/forevervm/.gitignore:
--------------------------------------------------------------------------------
```
target
```
--------------------------------------------------------------------------------
/scripts/.gitignore:
--------------------------------------------------------------------------------
```
node_modules
```
--------------------------------------------------------------------------------
/javascript/.gitignore:
--------------------------------------------------------------------------------
```
node_modules/
dist/
*.log
.DS_Store
tsconfig.tsbuildinfo
```
--------------------------------------------------------------------------------
/python/.gitignore:
--------------------------------------------------------------------------------
```
*.egg-info
.ropeproject
.ruff_cache
__pycache__
build
dist
venv
```
--------------------------------------------------------------------------------
/javascript/mcp-server/.prettierrc:
--------------------------------------------------------------------------------
```
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"semi": false
}
```
--------------------------------------------------------------------------------
/scripts/.prettierrc:
--------------------------------------------------------------------------------
```
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"semi": false
}
```
--------------------------------------------------------------------------------
/javascript/.prettierrc:
--------------------------------------------------------------------------------
```
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"semi": false,
"quoteProps": "consistent"
}
```
--------------------------------------------------------------------------------
/javascript/forevervm/README.md:
--------------------------------------------------------------------------------
```markdown
../../README.md
```
--------------------------------------------------------------------------------
/javascript/sdk/README.md:
--------------------------------------------------------------------------------
```markdown
../../README.md
```
--------------------------------------------------------------------------------
/rust/forevervm/README.md:
--------------------------------------------------------------------------------
```markdown
# foreverVM CLI
This is a CLI for [foreverVM](https://forevervm.com). It allows you to start foreverVMs and run a REPL on them.
```
--------------------------------------------------------------------------------
/rust/forevervm-sdk/README.md:
--------------------------------------------------------------------------------
```markdown
# foreverVM SDK
This is an SDK for [foreverVM](https://forevervm.com). It allows you to start foreverVMs and run a REPL on them.
```
--------------------------------------------------------------------------------
/javascript/example/README.md:
--------------------------------------------------------------------------------
```markdown
# foreverVM Example
Example of using foreverVM to run Python REPL commands.
You will need an API token (if you need one, reach out to [[email protected]](mailto:[email protected])).
Once you have an API token, run this
```sh
FOREVERVM_TOKEN="******" npm start
```
```
--------------------------------------------------------------------------------
/scripts/README.md:
--------------------------------------------------------------------------------
```markdown
# Helper scripts
- `bump-versions.ts`: Bump the version of all packages in the project.
Usage:
`npx tsx bump-versions.ts info` prints all of the current versions in the project and whether they
match the published version.
`npx tsx bump-versions.ts bump [patch|minor|major] [--dry-run]` bumps the version of all packages in the project.
It first finds the maximum of the current versions in the project and currently published versions, and
then bumps it by the specified type.
```
--------------------------------------------------------------------------------
/javascript/mcp-server/README.md:
--------------------------------------------------------------------------------
```markdown
# ForeverVM MCP Server
MCP Server for ForeverVM, enabling Claude to execute code in a Python REPL.
## Tools
1. `create-python-repl`
- Create a Python REPL.
- Returns: ID of the new REPL.
2. `run-python-in-repl`
- Execute code in a Python REPL.
- Required Inputs:
- `code` (string): code that the Python REPL will run.
- `replId` (string): ID of the REPL to run the code on.
- Returns: Result of the code executed.
## Usage with Claude Desktop
Run the following command:
```bash
npx forevervm-mcp install --claude
```
For other MCP clients, see [the docs](https://forevervm.com/docs/guides/forevervm-mcp-server/).
## Installing locally (for development only)
In the MCP client, set the command to `npm` and the arguments to:
```json
["--prefix", "<path/to/this/directory>", "run", "start", "run"]
```
```
--------------------------------------------------------------------------------
/python/sdk/README.md:
--------------------------------------------------------------------------------
```markdown
[foreverVM](https://forevervm.com)
==================================
[](https://github.com/jamsocket/forevervm)
[](https://discord.gg/N5sEpsuhh9)
| repo | version |
|-----------------------------------------------------|-----------------------------|
| [cli](https://github.com/jamsocket/forevervm) | [](https://pypi.org/project/forevervm/) |
| [sdk](https://github.com/jamsocket/forevervm) | [](https://pypi.org/project/forevervm-sdk/) |
foreverVM provides an API for running arbitrary, stateful Python code securely.
The core concepts in foreverVM are **machines** and **instructions**.
**Machines** represent a stateful Python process. You interact with a machine by running **instructions**
(Python statements and expressions) on it, and receiving the results. A machine processes one instruction
at a time.
Getting started
---------------
You will need an API token (if you need one, reach out to [[email protected]](mailto:[email protected])).
The easiest way to try out foreverVM is using the CLI. First, you will need to log in:
```bash
uvx forevervm login
```
Once logged in, you can open a REPL interface with a new machine:
```bash
uvx forevervm repl
```
When foreverVM starts your machine, it gives it an ID that you can later use to reconnect to it. You can reconnect to a machine like this:
```bash
uvx forevervm repl [machine_name]
```
You can list your machines (in reverse order of creation) like this:
```bash
uvx forevervm machine list
```
You don't need to terminate machines -- foreverVM will automatically swap them from memory to disk when they are idle, and then
automatically swap them back when needed. This is what allows foreverVM to run repls “forever”.
Using the API
-------------
```python
import os
from forevervm_sdk import ForeverVM
token = os.getenv('FOREVERVM_TOKEN')
if not token:
raise ValueError('FOREVERVM_TOKEN is not set')
# Initialize foreverVM
fvm = ForeverVM(token)
# Connect to a new machine
with fvm.repl() as repl:
# Execute some code
exec_result = repl.exec('4 + 4')
# Get the result
print('result:', exec_result.result)
# Execute code with output
exec_result = repl.exec('for i in range(10):\n print(i)')
for output in exec_result.output:
print(output["stream"], output["data"])
```
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
[foreverVM](https://forevervm.com)
==================================
[](https://github.com/jamsocket/forevervm)
[](https://discord.gg/N5sEpsuhh9)
| repo | version |
|-----------------------------------------------------|------------------------------|
| [cli](https://github.com/jamsocket/forevervm) | [](https://www.npmjs.com/package/forevervm) |
| [sdk](https://github.com/jamsocket/forevervm) | [](https://www.npmjs.com/package/@forevervm/sdk) |
foreverVM provides an API for running arbitrary, stateful Python code securely.
The core concepts in foreverVM are **machines** and **instructions**.
**Machines** represent a stateful Python process. You interact with a machine by running **instructions**
(Python statements and expressions) on it, and receiving the results. A machine processes one instruction
at a time.
Getting started
---------------
You will need an API token (if you need one, reach out to [[email protected]](mailto:[email protected])).
The easiest way to try out foreverVM is using the CLI. First, you will need to log in:
```bash
npx forevervm login
```
Once logged in, you can open a REPL interface with a new machine:
```bash
npx forevervm repl
```
When foreverVM starts your machine, it gives it an ID that you can later use to reconnect to it. You can reconnect to a machine like this:
```bash
npx forevervm repl [machine_name]
```
You can list your machines (in reverse order of creation) like this:
```bash
npx forevervm machine list
```
You don't need to terminate machines -- foreverVM will automatically swap them from memory to disk when they are idle, and then
automatically swap them back when needed. This is what allows foreverVM to run repls “forever”.
Using the API
-------------
```typescript
import { ForeverVM } from '@forevervm/sdk'
const token = process.env.FOREVERVM_TOKEN
if (!token) {
throw new Error('FOREVERVM_TOKEN is not set')
}
// Initialize foreverVM
const fvm = new ForeverVM({ token })
// Connect to a new machine.
const repl = fvm.repl()
// Execute some code
let execResult = repl.exec('4 + 4')
// Get the result
console.log('result:', await execResult.result)
// We can also print stdout and stderr
execResult = repl.exec('for i in range(10):\n print(i)')
for await (const output of execResult.output) {
console.log(output.stream, output.data)
}
process.exit(0)
```
Working with Tags
----------------
You can create machines with tags and filter machines by tags:
```typescript
import { ForeverVM } from '@forevervm/sdk'
const fvm = new ForeverVM({ token: process.env.FOREVERVM_TOKEN })
// Create a machine with tags
const machineResponse = await fvm.createMachine({
tags: {
env: 'production',
owner: 'user123',
project: 'demo'
}
})
// List machines filtered by tags
const productionMachines = await fvm.listMachines({
tags: { env: 'production' }
})
```
Memory Limits
----------------
You can create machines with memory limits by specifying the memory size in megabytes:
```typescript
// Create a machine with 512MB memory limit
const machineResponse = await fvm.createMachine({
memory_mb: 512,
})
```
```
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
```markdown
MIT License
Copyright (c) 2025 Drifting in Space Corp.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
--------------------------------------------------------------------------------
/javascript/mcp-server/tmp.json:
--------------------------------------------------------------------------------
```json
```
--------------------------------------------------------------------------------
/python/sdk/tests/__init__.py:
--------------------------------------------------------------------------------
```python
```
--------------------------------------------------------------------------------
/rust/forevervm/src/commands/mod.rs:
--------------------------------------------------------------------------------
```rust
pub mod auth;
pub mod machine;
pub mod repl;
```
--------------------------------------------------------------------------------
/rust/forevervm-sdk/src/lib.rs:
--------------------------------------------------------------------------------
```rust
#![deny(clippy::unwrap_used)]
pub mod api;
pub mod client;
pub mod util;
```
--------------------------------------------------------------------------------
/rust/Cargo.toml:
--------------------------------------------------------------------------------
```toml
[workspace]
resolver = "2"
members = [
"forevervm",
"forevervm-sdk",
]
```
--------------------------------------------------------------------------------
/python/sdk/forevervm_sdk/config.py:
--------------------------------------------------------------------------------
```python
API_BASE_URL = "https://api.forevervm.com"
DEFAULT_INSTRUCTION_TIMEOUT_SECONDS = 15
```
--------------------------------------------------------------------------------
/javascript/forevervm/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
}
```
--------------------------------------------------------------------------------
/javascript/sdk/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
}
```
--------------------------------------------------------------------------------
/javascript/vitest.config.ts:
--------------------------------------------------------------------------------
```typescript
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
includeSource: ['src/**/*.{js,ts}'],
},
})
```
--------------------------------------------------------------------------------
/rust/forevervm/src/lib.rs:
--------------------------------------------------------------------------------
```rust
#![deny(clippy::unwrap_used)]
pub mod commands;
pub mod config;
pub mod util;
pub const DEFAULT_SERVER_URL: &str = "https://api.forevervm.com";
```
--------------------------------------------------------------------------------
/javascript/sdk/src/env.ts:
--------------------------------------------------------------------------------
```typescript
export let env: { [key: string]: string | undefined } = {}
if (typeof process !== 'undefined') env = process.env
else if ('env' in import.meta) env = (import.meta as any).env
```
--------------------------------------------------------------------------------
/python/forevervm/pyproject.toml:
--------------------------------------------------------------------------------
```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "forevervm"
version = "0.1.35"
[project.scripts]
forevervm = "forevervm:run_binary"
```
--------------------------------------------------------------------------------
/javascript/tsup.config.ts:
--------------------------------------------------------------------------------
```typescript
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
define: { 'import.meta.vitest': 'undefined' },
clean: true,
dts: true,
sourcemap: true,
treeshake: true,
splitting: false,
})
```
--------------------------------------------------------------------------------
/scripts/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"types": ["node"]
},
"include": ["./**/*.ts"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/javascript/mcp-server/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/javascript/forevervm/src/run.js:
--------------------------------------------------------------------------------
```javascript
#!/usr/bin/env node
import { spawnSync } from 'node:child_process'
import { getBinary } from './get-binary.js'
async function runBinary() {
let binpath = await getBinary()
spawnSync(binpath, process.argv.slice(2), {
stdio: 'inherit',
stderr: 'inherit',
env: { ...process.env, FOREVERVM_RUNNER: 'npx' },
})
}
runBinary()
```
--------------------------------------------------------------------------------
/javascript/example/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "forevervm-python-repl-example",
"version": "1.0.0",
"private": true,
"description": "Example of using foreverVM to run Python REPL commands",
"type": "module",
"scripts": {
"start": "tsx index.ts"
},
"dependencies": {
"@forevervm/sdk": "file:../sdk"
},
"devDependencies": {
"@types/node": "^22.12.0",
"tsx": "^4.19.2"
}
}
```
--------------------------------------------------------------------------------
/python/sdk/pyproject.toml:
--------------------------------------------------------------------------------
```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "forevervm-sdk"
version = "0.1.35"
dependencies = [
"httpx>=0.28.1",
"httpx-ws>=0.7.1",
]
description = "Developer SDK for foreverVM"
readme = "README.md"
[dependency-groups]
dev = [
"mypy>=1.15.0",
"pytest>=8.3.4",
"pytest-asyncio>=0.25.3",
"ruff>=0.9.6",
]
```
--------------------------------------------------------------------------------
/scripts/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "forevervm-scripts",
"private": true,
"type": "module",
"scripts": {
"format": "prettier --write .",
"bump": "npx tsx bump-versions.ts bump",
"publish": "npx tsx publish.ts",
"check-format": "prettier --check ."
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"@types/node": "^22.10.10",
"chalk": "^5.4.1",
"commander": "^13.1.0",
"prettier": "^3.4.2"
}
}
```
--------------------------------------------------------------------------------
/javascript/tsconfig.base.json:
--------------------------------------------------------------------------------
```json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"declaration": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true,
"types": ["vitest/importMeta"]
},
"include": ["src"]
}
```
--------------------------------------------------------------------------------
/rust/forevervm-sdk/src/api/mod.rs:
--------------------------------------------------------------------------------
```rust
use serde::{Deserialize, Serialize};
use std::fmt::Display;
pub mod api_types;
pub mod http_api;
pub mod id_types;
pub mod protocol;
pub mod token;
#[derive(Debug, Serialize, Deserialize)]
pub struct ApiErrorResponse {
pub code: String,
pub id: Option<String>,
}
impl std::error::Error for ApiErrorResponse {}
impl Display for ApiErrorResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Api error: {{ code: {}, id: {:?} }}", self.code, self.id)
}
}
```
--------------------------------------------------------------------------------
/rust/forevervm-sdk/src/util.rs:
--------------------------------------------------------------------------------
```rust
use std::env;
use regex::Regex;
pub fn validate_email(email: &str) -> bool {
let email_regex = Regex::new(r#"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"#)
.expect("Static verified regex should always compile");
email_regex.is_match(email)
}
pub fn validate_account_name(account_name: &str) -> bool {
if account_name.len() < 3 || account_name.len() > 16 {
return false;
}
account_name
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
}
pub fn get_runner() -> Option<String> {
env::var("FOREVERVM_RUNNER").ok()
}
```
--------------------------------------------------------------------------------
/javascript/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@forevervm/monorepo",
"private": true,
"scripts": {
"build": "npm run build --workspaces",
"postinstall": "npm run build --workspaces",
"typecheck": "npm run typecheck --workspaces",
"format": "npm run format --workspaces",
"check-format": "npm run check-format --workspaces",
"publish": "npm publish --workspaces",
"version": "npm version --workspaces"
},
"workspaces": [
"sdk",
"forevervm",
"mcp-server"
],
"devDependencies": {
"prettier": "^3.4.2",
"tsup": "^8.3.5",
"typescript": "^5.7.3",
"vitest": "^3.0.4"
}
}
```
--------------------------------------------------------------------------------
/javascript/forevervm/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "forevervm",
"description": "CLI for foreverVM",
"version": "0.1.35",
"author": "Jamsocket",
"type": "module",
"bin": {
"forevervm": "src/run.js"
},
"bugs": "https://github.com/jamsocket/forevervm/issues",
"homepage": "https://github.com/jamsocket/forevervm",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/jamsocket/forevervm.git"
},
"scripts": {
"build": "exit 0",
"test": "exit 0",
"typecheck": "exit 0",
"format": "prettier --write \"src/**/*.js\"",
"check-format": "prettier --check src/**/*.js"
}
}
```
--------------------------------------------------------------------------------
/javascript/example/index.ts:
--------------------------------------------------------------------------------
```typescript
import { ForeverVM } from "@forevervm/sdk";
const token = process.env.FOREVERVM_TOKEN;
if (!token) {
throw new Error("FOREVERVM_TOKEN is not set");
}
// Initialize foreverVM
const fvm = new ForeverVM({ token });
// Connect to a new machine.
const repl = fvm.repl();
// Execute some code
let execResult = repl.exec("4 + 4");
// Get the result
console.log("result:", await execResult.result);
// We can also print stdout and stderr
execResult = repl.exec("for i in range(10):\n print(i)");
for await (const output of execResult.output) {
console.log(output.stream, output.data);
}
process.exit(0);
```
--------------------------------------------------------------------------------
/publishing.md:
--------------------------------------------------------------------------------
```markdown
1. Bump versions:
```bash
npm --prefix scripts i
npm --prefix scripts run bump patch # or minor or major
```
2. Open a PR called "vX.Y.Z" where X.Y.Z is the new version
Go through the PR process and merge the PR.
3. Run the [release workflow](https://github.com/jamsocket/forevervm/actions/workflows/release.yml)
As the release version, put `vX.Y.Z` where X.Y.Z is the new version. (Note the leading `v`)
4. Complete the [draft release](https://github.com/jamsocket/forevervm/releases). Use "generate release notes" to automatically generate release notes.
5. Publish the packages:
```bash
npm --prefix scripts run publish
```
```
--------------------------------------------------------------------------------
/javascript/mcp-server/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "forevervm-mcp",
"version": "0.1.35",
"description": "",
"main": "index.js",
"type": "module",
"bin": {
"forevervm-mcp": "./build/index.js"
},
"files": [
"build"
],
"scripts": {
"build": "tsc && shx chmod +x build/*.js",
"prepublishOnly": "npm run build",
"format": "prettier --write .",
"typecheck": "tsc --noEmit",
"start": "tsx src/index.ts",
"check-format": "prettier --check src/**/*.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@forevervm/sdk": "^0.1.22",
"@modelcontextprotocol/sdk": "^1.3.1",
"commander": "^13.1.0",
"prettier": "^3.4.2",
"shx": "^0.3.4",
"yaml": "^2.7.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@types/node": "^22.10.7",
"tsx": "^4.19.2",
"typescript": "^5.7.3"
}
}
```
--------------------------------------------------------------------------------
/rust/forevervm-sdk/Cargo.toml:
--------------------------------------------------------------------------------
```toml
[package]
name = "forevervm-sdk"
version = "0.1.35"
edition = "2021"
license = "MIT OR Apache-2.0"
homepage = "https://forevervm.com/"
repository = "https://github.com/jamsocket/forevervm"
readme = "README.md"
description = "foreverVM SDK. Allows you to start foreverVMs and run a REPL on them."
[dependencies]
async-stream = "0.3.6"
chrono = { version = "0.4.39", features = ["serde"] }
futures-util = "0.3.31"
regex = "1.11.1"
reqwest = { version = "0.12.12", default-features = false, features = ["rustls-tls", "json", "stream"] }
rustls = "0.23.21"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.137"
thiserror = "2.0.11"
tokio = "1.43.0"
tokio-tungstenite = { version = "0.26.1", features = ["rustls-tls-webpki-roots"] }
tracing = "0.1.41"
tungstenite = "0.26.1"
url = "2.5.4"
[dev-dependencies]
tokio = { version = "1.43.0", features = ["macros"] }
```
--------------------------------------------------------------------------------
/rust/forevervm/Cargo.toml:
--------------------------------------------------------------------------------
```toml
[package]
name = "forevervm"
version = "0.1.35"
edition = "2021"
license = "MIT OR Apache-2.0"
homepage = "https://forevervm.com/"
repository = "https://github.com/jamsocket/forevervm"
readme = "README.md"
description = "foreverVM CLI. Allows you to start foreverVMs and run a REPL on them."
[dependencies]
anyhow = "1.0.95"
chrono = { version = "0.4.39", features = ["serde"] }
clap = { version = "4.4", features = ["derive", "env"] }
colorize = "0.1.0"
dialoguer = { version = "0.11.0", features = ["password"] }
dirs = "6.0.0"
forevervm-sdk = { path = "../forevervm-sdk", version = "0.1.21" }
reqwest = { version = "0.12.12", default-features = false, features = ["rustls-tls"] }
rustyline = "15.0.0"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.137"
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread"] }
url = { version = "2.5.4", features = ["serde"] }
```
--------------------------------------------------------------------------------
/javascript/mcp-server/src/install/index.ts:
--------------------------------------------------------------------------------
```typescript
import { getForeverVMOptions } from '../index.js'
import { installForClaude } from './claude.js'
import { installForGoose } from './goose.js'
import { installForWindsurf } from './windsurf.js'
export function installForeverVM(options: { claude: boolean; windsurf: boolean; goose: boolean }) {
const forevervmOptions = getForeverVMOptions()
if (!forevervmOptions?.token) {
console.error(
'ForeverVM token not found. Please set up ForeverVM first by running `npx forevervm login` or `npx forevervm signup`.',
)
process.exit(1)
}
if (!options.claude && !options.windsurf && !options.goose) {
console.log(
'Select at least one MCP client to install. Available options: --claude, --windsurf, --goose',
)
process.exit(1)
}
if (options.claude) {
installForClaude()
}
if (options.goose) {
installForGoose()
}
if (options.windsurf) {
installForWindsurf()
}
}
```
--------------------------------------------------------------------------------
/rust/forevervm-sdk/src/client/util.rs:
--------------------------------------------------------------------------------
```rust
use super::ClientError;
use crate::api::token::ApiToken;
use tungstenite::handshake::client::generate_key;
use tungstenite::http::{
header::{AUTHORIZATION, CONNECTION, HOST, SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_VERSION, UPGRADE},
Request,
};
pub fn authorized_request(url: reqwest::Url, token: ApiToken) -> Result<Request<()>, ClientError> {
let hostname = url.host().ok_or(ClientError::InvalidUrl)?.to_string();
Ok(Request::builder()
.uri(url.to_string())
.header(AUTHORIZATION, format!("Bearer {token}"))
.header(HOST, hostname)
.header(CONNECTION, "Upgrade")
.header(UPGRADE, "websocket")
// ref: https://github.com/snapview/tungstenite-rs/blob/c16778797b2eeb118aa064aa5b483f90c3989627/src/client.rs#L240
.header(SEC_WEBSOCKET_VERSION, "13")
.header(SEC_WEBSOCKET_KEY, generate_key())
.header("x-forevervm-sdk", "rust")
.body(())?)
}
```
--------------------------------------------------------------------------------
/rust/forevervm-sdk/src/client/error.rs:
--------------------------------------------------------------------------------
```rust
use crate::api::ApiErrorResponse;
pub type Result<T> = std::result::Result<T, ClientError>;
#[derive(thiserror::Error, Debug)]
pub enum ClientError {
#[error("Api error: {0}")]
ApiError(#[from] ApiErrorResponse),
#[error("Reqwest error: {0}")]
ReqwestError(#[from] reqwest::Error),
#[error("Server responded with code {code} and message {message}")]
ServerResponseError { code: u16, message: String },
#[error("Invalid URL")]
InvalidUrl,
#[error("Error parsing url: {0}")]
UrlError(#[from] url::ParseError),
#[error("Error deserializing response: {0}")]
DeserializeError(#[from] serde_json::Error),
#[error("Error from Tungstenite: {0}")]
TungsteniteError(#[from] tungstenite::Error),
#[error("Http")]
HttpError(#[from] tungstenite::http::Error),
#[error("Instruction interrupted")]
InstructionInterrupted,
#[error("Other error: {0}")]
Other(String),
}
```
--------------------------------------------------------------------------------
/.github/workflows/rust-checks.yml:
--------------------------------------------------------------------------------
```yaml
name: Rust Checks
on:
pull_request:
branches: [ main ]
paths:
- '.github/workflows/rust-checks.yml'
- 'rust/**'
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15
environment: tests
defaults:
run:
working-directory: ./rust
steps:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: "true"
workspaces: |
rust
- name: Check formatting
run: cargo fmt --all -- --check
- name: Run clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose --all
env:
FOREVERVM_API_BASE: ${{ secrets.FOREVERVM_API_BASE }}
FOREVERVM_TOKEN: ${{ secrets.FOREVERVM_TOKEN }}
```
--------------------------------------------------------------------------------
/rust/forevervm-sdk/src/api/http_api.rs:
--------------------------------------------------------------------------------
```rust
use super::{api_types::ApiMachine, id_types::MachineName};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize)]
pub struct WhoamiResponse {
pub account: String,
}
#[derive(Serialize, Deserialize)]
pub struct CreateMachineResponse {
pub machine_name: MachineName,
}
#[derive(Serialize, Deserialize)]
pub struct ListMachinesResponse {
pub machines: Vec<ApiMachine>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct CreateMachineRequest {
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub tags: HashMap<String, String>,
/// Memory size in MB. If not specified, a default value will be used.
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_mb: Option<u32>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ListMachinesRequest {
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub tags: HashMap<String, String>,
}
```
--------------------------------------------------------------------------------
/javascript/sdk/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@forevervm/sdk",
"version": "0.1.35",
"description": "Developer SDK for foreverVM",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"import": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"require": "./dist/index.cjs"
}
},
"scripts": {
"build": "tsup",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"format": "prettier --write \"src/**/*.ts\"",
"check-format": "prettier --check src/**/*.ts",
"prepublishOnly": "npm run build"
},
"files": [
"dist",
"src"
],
"author": "Jamsocket",
"license": "MIT",
"devDependencies": {
"@types/node": "^22.10.7",
"@types/ws": "^8.5.13"
},
"dependencies": {
"isomorphic-ws": "^5.0.0",
"tsup": "^8.3.6",
"typescript": "^5.7.3",
"vitest": "^3.0.4",
"ws": "^8.18.0"
}
}
```
--------------------------------------------------------------------------------
/.github/workflows/python-checks.yml:
--------------------------------------------------------------------------------
```yaml
name: Python Checks
on:
pull_request:
branches: [ main ]
paths:
- 'python/**'
- '.github/workflows/python-checks.yml'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Run ruff format check
working-directory: ./python
run: uvx ruff format --check .
- name: Run ruff lint
working-directory: ./python
run: uvx ruff check .
test:
runs-on: ubuntu-latest
environment: tests
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Create virtual environment
working-directory: python/sdk
run: uv venv
- name: Install dependencies
working-directory: python/sdk
run: |
uv pip install -e .[dev]
- name: Run tests
working-directory: python/sdk
env:
FOREVERVM_API_BASE: ${{ secrets.FOREVERVM_API_BASE }}
FOREVERVM_TOKEN: ${{ secrets.FOREVERVM_TOKEN }}
run: uv run pytest tests/
```
--------------------------------------------------------------------------------
/python/sdk/forevervm_sdk/types.py:
--------------------------------------------------------------------------------
```python
from typing import Any, Dict, List, Literal, Optional, TypedDict
class RequestOptions(TypedDict, total=False):
timeout: int
class WhoamiResponse(TypedDict):
account: str
class CreateMachineRequest(TypedDict, total=False):
tags: Dict[str, str]
memory_mb: Optional[int]
class CreateMachineResponse(TypedDict):
machine_name: str
class Machine(TypedDict):
name: str
created_at: str
running: bool
has_pending_instructions: bool
expires_at: Optional[str]
tags: Dict[str, str]
class ListMachinesResponse(TypedDict):
machines: List[Machine]
class ExecResultBase(TypedDict):
runtime_ms: int
class ExecResultValue(ExecResultBase):
value: Optional[str]
data: Optional[Dict[str, Any]]
class ExecResultError(ExecResultBase):
error: str
ExecResult = ExecResultValue | ExecResultError
class ExecResponse(TypedDict):
instruction_seq: int
machine: str
interrupted: bool
class ExecResultResponse(TypedDict):
instruction_id: int
result: ExecResult
class StandardOutput(TypedDict):
stream: Literal["stdout"] | Literal["stderr"]
data: str
```
--------------------------------------------------------------------------------
/rust/forevervm/src/util.rs:
--------------------------------------------------------------------------------
```rust
use chrono::Duration;
use std::{env, fmt::Display};
pub enum ApproximateDuration {
Days(i64),
Hours(i64),
Minutes(i64),
Seconds(i64),
}
impl Display for ApproximateDuration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ApproximateDuration::Days(days) => write!(f, "{} days", days),
ApproximateDuration::Hours(hours) => write!(f, "{} hours", hours),
ApproximateDuration::Minutes(minutes) => write!(f, "{} minutes", minutes),
ApproximateDuration::Seconds(seconds) => write!(f, "{} seconds", seconds),
}
}
}
impl From<Duration> for ApproximateDuration {
fn from(duration: Duration) -> Self {
let days = duration.num_days();
if days > 3 {
return Self::Days(days);
}
let hours = duration.num_hours();
if hours > 3 {
return Self::Hours(hours);
}
let minutes = duration.num_minutes();
if minutes > 3 {
return Self::Minutes(minutes);
}
Self::Seconds(duration.num_seconds())
}
}
pub fn get_runner() -> String {
let allowlist = ["npx", "uvx", "cargo"];
match env::var("FOREVERVM_RUNNER") {
Ok(value) if allowlist.contains(&value.as_str()) => value,
_ => "cargo".to_string(),
}
}
```
--------------------------------------------------------------------------------
/rust/forevervm-sdk/src/api/protocol.rs:
--------------------------------------------------------------------------------
```rust
//! Types for the WebSocket protocol.
use super::{
api_types::{ApiExecResultResponse, Instruction},
id_types::{InstructionSeq, MachineName, MachineOutputSeq, RequestSeq},
ApiErrorResponse,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MessageToServer {
Exec {
instruction: Instruction,
request_id: RequestSeq,
},
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum MessageLevel {
Info,
Warn,
Error,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MessageFromServer {
Connected {
machine_name: MachineName,
},
ExecReceived {
seq: InstructionSeq,
request_id: RequestSeq,
},
Result(ApiExecResultResponse),
Output {
chunk: StandardOutput,
instruction_id: InstructionSeq,
},
Error(ApiErrorResponse),
/// Use to send log / diagnostic messages to the client.
Message {
message: String,
level: MessageLevel,
},
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum StandardOutputStream {
#[serde(rename = "stdout")]
Stdout,
#[serde(rename = "stderr")]
Stderr,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct StandardOutput {
pub stream: StandardOutputStream,
pub data: String,
pub seq: MachineOutputSeq,
}
```
--------------------------------------------------------------------------------
/rust/forevervm-sdk/src/api/token.rs:
--------------------------------------------------------------------------------
```rust
use serde::{Deserialize, Serialize};
use std::{fmt::Display, str::FromStr};
const SEPARATOR: &str = ".";
#[derive(Debug, Clone)]
pub struct ApiToken {
pub id: String,
pub token: String,
}
impl ApiToken {
pub fn new(token: String) -> Result<Self, ApiTokenError> {
let (id, token) = token
.split_once(SEPARATOR)
.ok_or(ApiTokenError::InvalidFormat)?;
Ok(Self {
id: id.to_string(),
token: token.to_string(),
})
}
}
impl Serialize for ApiToken {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for ApiToken {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(serde::de::Error::custom)
}
}
#[derive(thiserror::Error, Debug)]
pub enum ApiTokenError {
#[error("Invalid token format")]
InvalidFormat,
}
impl FromStr for ApiToken {
type Err = ApiTokenError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (id, token) = s
.split_once(SEPARATOR)
.ok_or(ApiTokenError::InvalidFormat)?;
Ok(Self {
id: id.to_string(),
token: token.to_string(),
})
}
}
impl Display for ApiToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}{}", self.id, SEPARATOR, self.token)
}
}
```
--------------------------------------------------------------------------------
/rust/forevervm-sdk/src/client/typed_socket.rs:
--------------------------------------------------------------------------------
```rust
use super::ClientError;
use futures_util::{
stream::{SplitSink, SplitStream},
SinkExt, StreamExt,
};
use serde::{de::DeserializeOwned, Serialize};
use std::marker::PhantomData;
use tokio::net::TcpStream;
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
use tungstenite::{client::IntoClientRequest, Message};
pub async fn websocket_connect<Send: Serialize, Recv: DeserializeOwned>(
req: impl IntoClientRequest + Unpin,
) -> Result<(WebSocketSend<Send>, WebSocketRecv<Recv>), ClientError> {
let (socket, _) = tokio_tungstenite::connect_async(req).await?;
let (socket_send, socket_recv) = socket.split();
Ok((
WebSocketSend {
socket_send,
_phantom: PhantomData,
},
WebSocketRecv {
socket_recv,
_phantom: PhantomData,
},
))
}
pub struct WebSocketSend<Send: Serialize> {
socket_send: SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,
_phantom: PhantomData<Send>,
}
impl<Send: Serialize> WebSocketSend<Send> {
pub async fn send(&mut self, msg: &Send) -> Result<(), ClientError> {
self.socket_send
.send(Message::Text(serde_json::to_string(msg)?.into()))
.await?;
Ok(())
}
}
pub struct WebSocketRecv<Recv: DeserializeOwned> {
socket_recv: SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>,
_phantom: PhantomData<Recv>,
}
impl<Recv: DeserializeOwned> WebSocketRecv<Recv> {
pub async fn recv(&mut self) -> Result<Option<Recv>, ClientError> {
let Some(msg) = self.socket_recv.next().await else {
return Ok(None);
};
Ok(serde_json::from_str(&msg?.into_text()?)?)
}
}
```
--------------------------------------------------------------------------------
/javascript/mcp-server/src/install/claude.ts:
--------------------------------------------------------------------------------
```typescript
import path from 'path'
import os from 'os'
import fs from 'fs'
function getClaudeConfigFilePath(): string {
const homeDir = os.homedir()
if (process.platform === 'win32') {
// Windows path
return path.join(
process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'),
'Claude',
'claude_desktop_config.json',
)
} else {
// macOS & Linux path
return path.join(
homeDir,
'Library',
'Application Support',
'Claude',
'claude_desktop_config.json',
)
}
}
export function installForClaude() {
const configFilePath = getClaudeConfigFilePath()
// Ensure the parent directory exists
const configDir = path.dirname(configFilePath)
if (!fs.existsSync(configDir)) {
console.error(
`Claude config directory does not exist (tried ${configDir}). Unable to install ForeverVM for Claude Desktop.`,
)
process.exit(1)
}
let config: any = {}
// If the file exists, read and parse the existing config
if (fs.existsSync(configFilePath)) {
try {
const fileContent = fs.readFileSync(configFilePath, 'utf8')
config = JSON.parse(fileContent)
} catch (error) {
console.error('Failed to read or parse existing Claude config:', error)
process.exit(1)
}
}
config.mcpServers = config.mcpServers || {}
config.mcpServers.forevervm = {
command: 'npx',
args: ['--yes', 'forevervm-mcp', 'run'],
}
try {
fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2) + '\n', 'utf8')
console.log(`✅ Claude Desktop configuration updated successfully at: ${configFilePath}`)
} catch (error) {
console.error('❌ Failed to write to Claude Desktop config file:', error)
process.exit(1)
}
}
```
--------------------------------------------------------------------------------
/javascript/mcp-server/src/install/windsurf.ts:
--------------------------------------------------------------------------------
```typescript
import path from 'path'
import os from 'os'
import fs from 'fs'
function getWindsurfConfigFilePath(): string {
// Ref: https://docs.codeium.com/windsurf/mcp
const homeDir = os.homedir()
if (process.platform === 'win32') {
// NOTE: the official docs don't say where to put the file on Windows, so currently we don't support that.
console.error(
'Automatic installation is not supported on Windows, follow the instructions here instead: https://docs.codeium.com/windsurf/mcp',
)
process.exit(1)
}
return path.join(homeDir, '.codeium', 'windsurf', 'mcp_config.json')
}
export function installForWindsurf() {
const configFilePath = getWindsurfConfigFilePath()
// Ensure the parent directory exists
const configDir = path.dirname(configFilePath)
if (!fs.existsSync(configDir)) {
console.error(
`Windsurf config directory does not exist (tried ${configDir}). Unable to install ForeverVM for Windsurf.`,
)
process.exit(1)
}
let config: any = {}
// If the file exists, read and parse the existing config
if (fs.existsSync(configFilePath)) {
try {
const fileContent = fs.readFileSync(configFilePath, 'utf8')
config = JSON.parse(fileContent)
} catch (error) {
console.error('Failed to read or parse existing Windsurf config:', error)
process.exit(1)
}
}
config.mcpServers = config.mcpServers || {}
config.mcpServers.forevervm = {
command: 'npx',
args: ['--yes', 'forevervm-mcp', 'run'],
}
try {
fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2) + '\n', 'utf8')
console.log(`✅ Windsurf configuration updated successfully at: ${configFilePath}`)
} catch (error) {
console.error('❌ Failed to write to Windsurf config file:', error)
process.exit(1)
}
}
```
--------------------------------------------------------------------------------
/rust/forevervm/src/commands/machine.rs:
--------------------------------------------------------------------------------
```rust
use crate::{config::ConfigManager, util::ApproximateDuration};
use chrono::Utc;
use colorize::AnsiColor;
use forevervm_sdk::api::http_api::{CreateMachineRequest, ListMachinesRequest};
pub async fn machine_list(tags: std::collections::HashMap<String, String>) -> anyhow::Result<()> {
let client = ConfigManager::new()?.client()?;
let request = ListMachinesRequest { tags };
let machines = client.list_machines(request).await?;
println!("Machines:");
for machine in machines.machines {
let expires_at = if let Some(expires_at) = machine.expires_at {
expires_at.to_string()
} else {
"never".to_string()
};
let status = if machine.has_pending_instruction {
"has_work".to_string()
} else {
"idle".to_string()
};
let age = ApproximateDuration::from(Utc::now() - machine.created_at);
println!("{}", machine.name.to_string().b_green());
println!(
" Created: {} ago ({})",
age.to_string().b_yellow(),
machine.created_at.to_string().b_yellow()
);
println!(" Expires: {}", expires_at.b_yellow());
println!(" Status: {}", status.b_yellow());
println!(" Running: {}", machine.running.to_string().b_yellow());
for (key, value) in machine.tags.into_iter() {
println!(" Tag: {} = {}", key.b_yellow(), value.b_yellow());
}
println!();
}
Ok(())
}
pub async fn machine_new(tags: std::collections::HashMap<String, String>) -> anyhow::Result<()> {
let client = ConfigManager::new()?.client()?;
let request = CreateMachineRequest {
tags,
memory_mb: None,
};
let machine = client.create_machine(request).await?;
println!(
"Created machine {}",
machine.machine_name.to_string().b_green()
);
Ok(())
}
```
--------------------------------------------------------------------------------
/javascript/forevervm/src/get-binary.js:
--------------------------------------------------------------------------------
```javascript
import * as fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import packageJson from '../package.json' with { type: 'json' }
function getSuffix(osType, osArch) {
if (osType === 'win32' && osArch === 'x64') return 'win-x64.exe.gz'
if (osType === 'linux' && osArch === 'x64') return 'linux-x64.gz'
if (osType === 'linux' && osArch === 'arm64') return 'linux-arm64.gz'
if (osType === 'darwin' && osArch === 'x64') return 'macos-x64.gz'
if (osType === 'darwin' && osArch === 'arm64') return 'macos-arm64.gz'
throw new Error(`Unsupported platform: ${osType} ${osArch}`)
}
function binaryUrl(version, osType, osArch) {
const suffix = getSuffix(osType, osArch)
const url = `https://github.com/jamsocket/forevervm/releases/download/v${version}/forevervm-${suffix}`
return url
}
async function downloadFile(url, filePath) {
const zlib = await import('node:zlib')
const { pipeline } = await import('node:stream/promises')
const res = await fetch(url)
if (res.status === 404) {
throw new Error(
`Tried to download ${url} but the file was not found. It may have been removed.`,
)
} else if (res.status !== 200) {
throw new Error(`Error downloading ${url}: server returned ${res.status}`)
}
const handle = await fs.open(filePath, 'w', 0o770)
await pipeline(res.body, zlib.createGunzip(), handle.createWriteStream())
await handle.close()
return filePath
}
export async function getBinary() {
let version = packageJson.version
let bindir = path.normalize(
path.join(
os.homedir(),
'.cache',
'forevervm',
`${process.platform}-${process.arch}-${version}`,
),
)
await fs.mkdir(bindir, { recursive: true })
let binpath = path.join(bindir, 'forevervm')
try {
await fs.stat(binpath)
return binpath
} catch {}
let url = binaryUrl(version, process.platform, process.arch)
await downloadFile(url, binpath)
return binpath
}
```
--------------------------------------------------------------------------------
/javascript/mcp-server/src/install/goose.ts:
--------------------------------------------------------------------------------
```typescript
import path from 'path'
import os from 'os'
import fs from 'fs'
import YAML from 'yaml'
function getGooseConfigFilePath(): string {
// Goose calls MCP servers "extensions"
// Ref: https://block.github.io/goose/docs/getting-started/using-extensions
// Currently, only CLI Goose uses file-based configuration; desktop Goose uses
// Electron Local Storage. This is actively in transition
// Ref: https://github.com/block/goose/discussions/890#discussioncomment-12093422
const homeDir = os.homedir()
return path.join(homeDir, '.config', 'goose', 'config.yaml')
}
export function installForGoose() {
const configFilePath = getGooseConfigFilePath()
// Ensure the parent directory exists
const configDir = path.dirname(configFilePath)
if (!fs.existsSync(configDir)) {
console.error(
`Goose config directory does not exist (tried ${configDir}). Unable to install ForeverVM for Goose.`,
)
process.exit(1)
}
let config: YAML.Document
// If the file exists, read and parse the existing config
if (fs.existsSync(configFilePath)) {
try {
const fileContent = fs.readFileSync(configFilePath, 'utf8')
config = YAML.parseDocument(fileContent)
} catch (error) {
console.error('Failed to read or parse existing Goose config:', error)
process.exit(1)
}
} else {
console.error(`Goose config file not found at: ${configFilePath}`)
process.exit(1)
}
if (!config.has('extensions')) {
config.set('extensions', {})
}
config.setIn(['extensions', 'forevervm'], {
cmd: 'npx',
args: ['--yes', 'forevervm-mcp', 'run'],
enabled: true,
envs: {},
name: 'forevervm',
type: 'stdio',
})
try {
fs.writeFileSync(configFilePath, YAML.stringify(config), 'utf8')
console.log(`✅ Goose configuration updated successfully at: ${configFilePath}`)
} catch (error) {
console.error('❌ Failed to write to Goose config file:', error)
process.exit(1)
}
}
```
--------------------------------------------------------------------------------
/.github/workflows/javascript-checks.yml:
--------------------------------------------------------------------------------
```yaml
name: JavaScript Checks
on:
pull_request:
branches: [ main ]
paths:
- 'javascript/**'
- '.github/workflows/javascript-checks.yml'
jobs:
build-and-format:
runs-on: ubuntu-latest
defaults:
run:
working-directory: javascript
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: javascript/package-lock.json
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Format
run: npm run check-format
- name: Check Types
run: npm run typecheck
test-node-22:
runs-on: ubuntu-latest
environment: tests
defaults:
run:
working-directory: javascript
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: javascript/package-lock.json
- name: Install dependencies
run: npm ci
- name: Run tests
working-directory: javascript/sdk
env:
FOREVERVM_API_BASE: ${{ secrets.FOREVERVM_API_BASE }}
FOREVERVM_TOKEN: ${{ secrets.FOREVERVM_TOKEN }}
run: npm test
test-node-18:
runs-on: ubuntu-latest
environment: tests
defaults:
run:
working-directory: javascript
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: javascript/package-lock.json
- name: Install dependencies
run: npm ci
- name: Run tests
working-directory: javascript/sdk
env:
FOREVERVM_API_BASE: ${{ secrets.FOREVERVM_API_BASE }}
FOREVERVM_TOKEN: ${{ secrets.FOREVERVM_TOKEN }}
run: npm test
```
--------------------------------------------------------------------------------
/javascript/sdk/src/types.ts:
--------------------------------------------------------------------------------
```typescript
export interface StandardOutput {
stream: 'stdout' | 'stderr'
data: string
seq: number
}
export interface ConnectedMessageFromServer {
type: 'connected'
machine_name: string
}
export interface ExecMessageFromServer {
type: 'exec_received'
seq: number // TODO: rename to instruction_id
request_id: number
}
export interface ResultMessageFromServer {
type: 'result'
instruction_id: number
result: ExecResponse
}
export interface OutputMessageFromServer {
type: 'output'
chunk: StandardOutput
instruction_id: number
}
export interface ErrorMessageFromServer {
type: 'error'
code: string
id: string
}
export type MessageFromServer =
| ConnectedMessageFromServer
| ExecMessageFromServer
| ResultMessageFromServer
| OutputMessageFromServer
| ErrorMessageFromServer
export interface WhoamiResponse {
account: string
}
export interface CreateMachineRequest {
tags?: Record<string, string>
/**
* Memory size in MB. If not specified, a default value will be used.
*/
memory_mb?: number
}
export interface CreateMachineResponse {
machine_name: string
}
export interface Machine {
name: string
created_at: string
running: boolean
has_pending_instructions: boolean
expires_at?: string
tags?: Record<string, string>
}
export interface ListMachinesRequest {
tags?: Record<string, string>
}
export interface ListMachinesResponse {
machines: Machine[]
}
export interface ApiExecRequest {
instruction: {
code: string
max_duration_seconds?: number
}
code: string
max_duration_seconds?: number
interrupt: boolean
}
export interface ApiExecResponse {
instruction_seq?: number
interrupted: boolean
}
export interface ExecResponse {
value?: string | null
data?: { [key: string]: unknown }
error?: string
runtime_ms: number
}
export interface ApiExecResultResponse {
instruction_seq: number
result: ExecResponse
value?: string | null
error?: string
runtime_ms: number
}
export type ApiExecResultStreamResponse = OutputMessageFromServer | ResultMessageFromServer
```
--------------------------------------------------------------------------------
/python/forevervm/forevervm/__init__.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python3
import os
import platform
import sys
import subprocess
import gzip
import shutil
from pathlib import Path
import urllib.request
import urllib.error
from importlib.metadata import version
sys_platform = sys.platform
uname_machine = platform.uname().machine
def get_suffix(os_type, os_arch):
suffixes = {
("win32", "AMD64"): "win-x64.exe.gz",
("linux", "x86_64"): "linux-x64.gz",
("linux", "aarch64"): "linux-arm64.gz",
("darwin", "x86_64"): "macos-x64.gz",
("darwin", "arm64"): "macos-arm64.gz",
}
if (os_type, os_arch) not in suffixes:
raise RuntimeError(f"Unsupported platform: {os_type} {os_arch}")
return suffixes[(os_type, os_arch)]
def binary_url(version, os_type, os_arch):
suffix = get_suffix(os_type, os_arch)
return f"https://github.com/jamsocket/forevervm/releases/download/v{version}/forevervm-{suffix}"
def download_file(url, file_path):
try:
response = urllib.request.urlopen(url)
if response.status == 404:
raise RuntimeError(f"File not found at {url}. It may have been removed.")
with gzip.open(response) as gz, open(file_path, "wb") as f:
shutil.copyfileobj(gz, f)
os.chmod(file_path, 0o770)
return file_path
except urllib.error.HTTPError as e:
raise RuntimeError(f"Error downloading {url}: server returned {e.code}")
def get_binary():
forevervm_version = version("forevervm")
bindir = (
Path.home()
/ ".cache"
/ "forevervm"
/ f"{sys_platform}-{uname_machine}-{forevervm_version}"
)
bindir.mkdir(parents=True, exist_ok=True)
binpath = bindir / "forevervm"
if binpath.exists():
return str(binpath)
url = binary_url(forevervm_version, sys_platform, uname_machine)
download_file(url, binpath)
return str(binpath)
def run_binary():
binpath = get_binary()
env = os.environ.copy()
env["FOREVERVM_RUNNER"] = "uvx"
subprocess.run([binpath] + sys.argv[1:], env=env)
if __name__ == "__main__":
run_binary()
```
--------------------------------------------------------------------------------
/rust/forevervm-sdk/src/api/api_types.rs:
--------------------------------------------------------------------------------
```rust
use super::id_types::{InstructionSeq, MachineName};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub struct ApiMachine {
pub name: MachineName,
pub created_at: DateTime<Utc>,
pub running: bool,
pub has_pending_instruction: bool,
pub expires_at: Option<DateTime<Utc>>,
#[serde(default)]
pub tags: HashMap<String, String>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ApiExecRequest {
pub instruction: Instruction,
/// If true, this interrupts any currently-pending or running instruction.
#[serde(default)]
pub interrupt: bool,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ApiExecResponse {
pub instruction_seq: Option<InstructionSeq>,
#[serde(default, skip_serializing_if = "bool_is_false")]
pub interrupted: bool,
pub machine: Option<MachineName>,
}
fn bool_is_false(b: &bool) -> bool {
!*b
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ApiExecResultResponse {
pub instruction_id: InstructionSeq,
pub result: ExecResult,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Instruction {
pub code: String,
#[serde(default = "default_timeout_seconds")]
pub timeout_seconds: i32,
}
fn default_timeout_seconds() -> i32 {
15
}
impl Instruction {
pub fn new(code: &str) -> Self {
Self {
code: code.to_string(),
timeout_seconds: 15,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[serde(untagged, rename_all = "snake_case")]
pub enum ExecResultType {
Error {
error: String,
},
Value {
value: Option<String>,
data: Option<serde_json::Value>,
},
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct ExecResult {
#[serde(flatten)]
pub result: ExecResultType,
pub runtime_ms: u64,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ApiSignupRequest {
pub email: String,
pub account_name: String,
}
```
--------------------------------------------------------------------------------
/rust/forevervm-sdk/src/api/id_types.rs:
--------------------------------------------------------------------------------
```rust
use serde::{Deserialize, Serialize};
use std::fmt::Display;
/* Instruction sequence number ========================================================= */
/// Sequence number of an instruction for a machine. This is not globally unique, but
/// is unique per machine.
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Default, Ord, PartialOrd,
)]
pub struct InstructionSeq(pub i64);
impl From<InstructionSeq> for i64 {
fn from(val: InstructionSeq) -> Self {
val.0
}
}
impl From<i64> for InstructionSeq {
fn from(id: i64) -> Self {
Self(id)
}
}
impl Display for InstructionSeq {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl InstructionSeq {
pub fn next(&self) -> InstructionSeq {
InstructionSeq(self.0 + 1)
}
}
/* Request sequence number ============================================================= */
/// Sequence number of an instruction for a machine. This is not globally unique, but
/// is unique per machine.
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Default, Ord, PartialOrd,
)]
pub struct RequestSeq(pub u32);
impl From<u32> for RequestSeq {
fn from(id: u32) -> Self {
Self(id)
}
}
/* Machine output sequence number ====================================================== */
/// Sequence number of output from a machine. This is not globally unique, but unique within
/// a (machine, instruction) pair. (In other words, it is reset to zero between each instruction.)
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
pub struct MachineOutputSeq(pub i64);
impl From<MachineOutputSeq> for i64 {
fn from(val: MachineOutputSeq) -> Self {
val.0
}
}
impl From<i64> for MachineOutputSeq {
fn from(id: i64) -> Self {
Self(id)
}
}
impl MachineOutputSeq {
pub fn next(&self) -> MachineOutputSeq {
MachineOutputSeq(self.0 + 1)
}
pub fn zero() -> Self {
Self(0)
}
}
/* Machine unique name ================================================================= */
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct MachineName(pub String);
impl From<MachineName> for String {
fn from(val: MachineName) -> Self {
val.0
}
}
impl From<String> for MachineName {
fn from(name: String) -> Self {
Self(name)
}
}
impl Display for MachineName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
```
--------------------------------------------------------------------------------
/rust/forevervm/src/config.rs:
--------------------------------------------------------------------------------
```rust
use crate::DEFAULT_SERVER_URL;
use anyhow::{Context, Result};
use dirs::home_dir;
use forevervm_sdk::{api::token::ApiToken, client::ForeverVMClient};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use url::Url;
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Config {
pub token: Option<ApiToken>,
pub server_url: Option<Url>,
}
impl Config {
pub fn server_url(&self) -> Result<Url> {
if let Some(url) = &self.server_url {
return Ok(url.clone());
}
Ok(DEFAULT_SERVER_URL.parse()?)
}
}
pub struct ConfigManager {
config_path: PathBuf,
}
impl ConfigManager {
pub fn new() -> Result<Self> {
let home_dir = home_dir().context("Failed to get home directory")?;
let config_path = home_dir
.join(".config")
.join("forevervm")
.join("config.json");
Ok(Self { config_path })
}
pub fn client(&self) -> Result<ForeverVMClient> {
let config = self.load()?;
if let Some(token) = &config.token {
Ok(ForeverVMClient::new(config.server_url()?, token.clone()))
} else {
Err(anyhow::anyhow!("Not logged in"))
}
}
pub fn load(&self) -> Result<Config> {
if !self.config_path.exists() {
if let Some(parent) = self.config_path.parent() {
std::fs::create_dir_all(parent)?;
}
return Ok(Config::default());
}
let config_str =
std::fs::read_to_string(&self.config_path).context("Failed to read config file")?;
let config = serde_json::from_str(&config_str).context("Failed to parse config file")?;
Ok(config)
}
pub fn save(&self, config: &Config) -> Result<()> {
if let Some(parent) = self.config_path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut config_str =
serde_json::to_string_pretty(config).context("Failed to serialize config")?;
config_str.push('\n');
std::fs::write(&self.config_path, config_str).context("Failed to write config file")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::metadata(&self.config_path)?;
let mut permissions = metadata.permissions();
permissions.set_mode(0o600); // Read/write for owner only
std::fs::set_permissions(&self.config_path, permissions)?;
}
Ok(())
}
pub fn get_path(&self) -> &Path {
&self.config_path
}
}
```
--------------------------------------------------------------------------------
/rust/forevervm/src/commands/repl.rs:
--------------------------------------------------------------------------------
```rust
use crate::config::ConfigManager;
use colorize::AnsiColor;
use forevervm_sdk::api::{
api_types::{ExecResultType, Instruction},
http_api::CreateMachineRequest,
id_types::MachineName,
};
use rustyline::{error::ReadlineError, DefaultEditor};
use std::time::Duration;
pub async fn machine_repl(
machine_name: Option<MachineName>,
instruction_timeout: Duration,
) -> anyhow::Result<()> {
let client = ConfigManager::new()?.client()?;
let machine_name = if let Some(machine_name) = machine_name {
machine_name
} else {
let machine = client
.create_machine(CreateMachineRequest::default())
.await?;
machine.machine_name
};
let mut repl = client.repl(&machine_name).await?;
println!("Connected to {}", machine_name.to_string().b_green());
let mut rl = DefaultEditor::new()?;
loop {
let readline = rl.readline(">>> ");
match readline {
Ok(line) => {
rl.add_history_entry(line.as_str())?;
let instruction = Instruction {
code: line,
timeout_seconds: instruction_timeout.as_secs() as i32,
};
let result = repl.exec_instruction(instruction).await;
match result {
Ok(mut result) => {
while let Some(output) = result.next().await {
println!("{}", output.data);
}
let result = result.result().await;
match result {
Ok(result) => match result.result {
ExecResultType::Error { error } => {
eprintln!("Error: {}", error);
}
ExecResultType::Value {
value: Some(value),
data: _,
} => {
println!("{}", value);
}
ExecResultType::Value {
value: None,
data: _,
} => {}
},
Err(err) => {
eprintln!("Error: {}", err);
}
}
}
Err(err) => {
eprintln!("Error: {}", err);
}
}
}
Err(ReadlineError::Interrupted) => {
break;
}
Err(err) => {
eprintln!("Error: {}", err);
break;
}
}
}
Ok(())
}
```
--------------------------------------------------------------------------------
/python/sdk/forevervm_sdk/repl.py:
--------------------------------------------------------------------------------
```python
from collections import deque
from warnings import warn
import re
from typing import Any
import httpx
from httpx_ws import WebSocketSession, connect_ws
from forevervm_sdk.config import DEFAULT_INSTRUCTION_TIMEOUT_SECONDS
from .config import API_BASE_URL
from .types import ExecResult, StandardOutput
class ReplException(Exception):
pass
class ReplExecResult:
_request_id = -1
_instruction_id = -1
_output: deque[StandardOutput]
def __init__(self, request_id: int, ws: WebSocketSession):
self._request_id = request_id
self._ws = ws
self._output = deque()
def _recv(self) -> str | None:
msg = self._ws.receive_json()
if msg["type"] == "exec_received":
if msg["request_id"] != self._request_id:
warn(f"Expected request ID {self._request_id} with message {msg}")
return None
self._instruction_id = msg["seq"]
elif msg["type"] == "output":
if msg["instruction_id"] != self._instruction_id:
warn(
f"Expected instruction ID {self._instruction_id} with message {msg}"
)
return None
self._output.append(msg["chunk"])
elif msg["type"] == "result":
if msg["instruction_id"] != self._instruction_id:
warn(
f"Expected instruction ID {self._instruction_id} with message {msg}"
)
return None
self._result = msg["result"]
elif msg["type"] == "error":
raise ReplException(msg["code"])
return msg["type"]
@property
def output(self):
while self._result is None:
if self._recv() == "output":
yield self._output.popleft()
while self._output:
yield self._output.popleft()
_result: ExecResult | None = None
@property
def result(self):
while self._result is None:
self._recv()
return self._result
class Repl:
_request_id = 0
_instruction: ReplExecResult | None = None
_connection: Any
def __init__(
self,
token: str,
machine_name="new",
base_url=API_BASE_URL,
):
client = httpx.Client(
headers={"authorization": f"Bearer {token}", "x-forevervm-sdk": "python"}
)
base_url = re.sub(r"^http(s)?://", r"ws\1://", base_url)
self._connection = connect_ws(
f"{base_url}/v1/machine/{machine_name}/repl", client
)
self._ws = self._connection.__enter__()
def __del__(self):
self._connection.__exit__(None, None, None)
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self._connection.__exit__(type, value, traceback)
def exec(
self, code: str, timeout_seconds: int = DEFAULT_INSTRUCTION_TIMEOUT_SECONDS
) -> ReplExecResult:
if self._instruction is not None and self._instruction._result is None:
raise ReplException("Instruction already running")
request_id = self._request_id
self._request_id += 1
instruction = {"code": code, "timeout_seconds": timeout_seconds}
self._ws.send_json(
{"type": "exec", "instruction": instruction, "request_id": request_id}
)
self._instruction = ReplExecResult(request_id, self._ws)
return self._instruction
```
--------------------------------------------------------------------------------
/rust/forevervm/src/main.rs:
--------------------------------------------------------------------------------
```rust
#![deny(clippy::unwrap_used)]
use clap::{Args, Parser, Subcommand};
use forevervm::{
commands::{
auth::{login, logout, signup, whoami},
machine::{machine_list, machine_new},
repl::machine_repl,
},
DEFAULT_SERVER_URL,
};
use forevervm_sdk::api::id_types::MachineName;
use std::collections::HashMap;
use std::time::Duration;
use url::Url;
/// Parse a key-value pair in the format of `key=value`
fn parse_key_val(s: &str) -> Result<(String, String), String> {
let pos = s
.find('=')
.ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
Ok((s[..pos].to_string(), s[pos + 1..].to_string()))
}
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Args)]
pub struct ReplConfig {
machine_name: Option<MachineName>,
#[arg(long, default_value = "15")]
instruction_timeout_seconds: u64,
}
#[derive(Subcommand)]
enum Commands {
/// Signup to your account
Signup {
#[arg(long, default_value = DEFAULT_SERVER_URL)]
api_base_url: Url,
},
/// Login to your account
Login {
#[arg(long, default_value = DEFAULT_SERVER_URL)]
api_base_url: Url,
},
/// Logout from your account
Logout,
Whoami,
/// Machine management commands
Machine {
#[command(subcommand)]
command: MachineCommands,
},
/// Start a REPL session
Repl(ReplConfig),
}
#[derive(Subcommand)]
enum MachineCommands {
/// Create a new machine
New {
/// Add tags to the machine in the format key=value
#[arg(long = "tag", value_parser = parse_key_val, action = clap::ArgAction::Append)]
tags: Option<Vec<(String, String)>>,
},
/// List all machines
List {
/// Filter machines by tags in the format key=value
#[arg(long = "tag", value_parser = parse_key_val, action = clap::ArgAction::Append)]
tags: Option<Vec<(String, String)>>,
},
/// Start a REPL session for a specific machine
Repl(ReplConfig),
}
async fn main_inner() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Signup { api_base_url } => {
signup(api_base_url).await?;
}
Commands::Login { api_base_url } => {
login(api_base_url).await?;
}
Commands::Logout => {
logout().await?;
}
Commands::Whoami => {
whoami().await?;
}
Commands::Machine { command } => match command {
MachineCommands::New { tags } => {
let tags_map = tags
.map(|tags| tags.into_iter().collect::<HashMap<String, String>>())
.unwrap_or_default();
machine_new(tags_map).await?;
}
MachineCommands::List { tags } => {
let tags_map = tags
.map(|tags| tags.into_iter().collect::<HashMap<String, String>>())
.unwrap_or_default();
machine_list(tags_map).await?;
}
MachineCommands::Repl(config) => {
run_repl(config).await?;
}
},
Commands::Repl(config) => {
run_repl(config).await?;
}
}
Ok(())
}
pub async fn run_repl(config: ReplConfig) -> anyhow::Result<()> {
let instruction_timeout = Duration::from_secs(config.instruction_timeout_seconds);
machine_repl(config.machine_name, instruction_timeout).await?;
Ok(())
}
#[tokio::main]
async fn main() {
if let Err(e) = main_inner().await {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
```
--------------------------------------------------------------------------------
/scripts/publish.ts:
--------------------------------------------------------------------------------
```typescript
import * as cp from 'node:child_process'
import path from 'node:path'
import readline from 'node:readline/promises'
import * as fs from 'node:fs'
import { fileURLToPath } from 'url'
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
let abort = new AbortController()
function question(query: string) {
abort = new AbortController()
return rl.question(query, { signal: abort.signal })
}
rl.on('SIGINT', () => {
abort.abort()
process.exit()
})
function exec(command: string, options?: cp.ExecOptions & { log?: boolean }) {
return new Promise<string>((resolve, reject) => {
cp.exec(command, options, (err, stdout, stderr) => {
if (options?.log !== false && stderr) console.log(stderr)
if (err) reject(err)
else resolve(stdout.toString())
})
})
}
/** Get the version number being deployed. Arbitrarily uses the forevervm npm package;
* All packages should be in sync but this is not tested.
*/
function getVersion() {
const currentScriptPath = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'..',
'javascript',
'forevervm',
'package.json',
)
const json = JSON.parse(fs.readFileSync(currentScriptPath, 'utf-8'))
return json.version
}
/** Verify that all binary files exist for the given version. */
async function verifyBinariesExist(version: string) {
const files = [
'win-x64.exe.gz',
'linux-x64.gz',
'linux-arm64.gz',
'macos-x64.gz',
'macos-arm64.gz',
]
for (const file of files) {
const url = `https://github.com/jamsocket/forevervm/releases/download/v${version}/forevervm-${file}`
// send a HEAD request to check if the file exists
const res = await fetch(url, { method: 'HEAD' })
if (res.status !== 200) {
console.error(`Binary for ${file} does not exist! Got status ${res.status}`)
process.exit(1)
} else {
console.log(`Binary for ${file} exists!`)
}
}
}
async function main() {
const version = getVersion()
await verifyBinariesExist(version)
const branch = await exec('git branch --show-current')
if (branch.trim() !== 'main') {
console.error('Must publish from main branch!')
process.exit(1)
}
await exec('git fetch')
const commits = await exec('git rev-list HEAD...origin/main --count')
if (commits.trim() !== '0') {
console.error('main branch must be up to date!')
process.exit(1)
}
const changes = await exec('git diff --quiet')
.then(() => false)
.catch(() => true)
if (changes) {
console.error('Cannot publish with local changes!')
process.exit(1)
}
console.log(`Publishing to crates.io…`)
await publishToCrates()
console.log(`Published packages to crates.io!`)
console.log(`Publishing to npm…`)
await publishToNpm()
console.log(`Published packages to npm!`)
console.log(`Publishing to PyPI…`)
await publishToPyPI()
console.log(`Published packages to PyPI!`)
}
async function publishToCrates() {
const cwd = path.resolve('../rust')
await exec('cargo install cargo-workspaces', { cwd })
await exec('cargo workspaces publish --from-git', { cwd })
}
async function publishToNpm() {
let otp = ''
for (const pkg of ['forevervm', 'sdk', 'mcp-server']) {
const cwd = path.resolve('../javascript', pkg)
const json = await import(path.resolve(cwd, 'package.json'), { with: { type: 'json' } })
const version = await exec(`npm view ${json.name} version`)
if (json.version === version.trim()) {
console.log(`Already published ${json.name} ${json.version}`)
continue
}
let published = false
while (!published) {
try {
let cmd = 'npm publish'
if (otp) cmd += ' --otp=' + otp
await exec(cmd, { log: false, cwd })
published = true
} catch (e) {
if (!/npm error code EOTP/.test(e as string)) throw e
otp = await question('Enter your npm OTP: ')
if (!otp) throw e
}
}
}
}
async function publishToPyPI() {
const token = await question('Enter your PyPI token: ')
for (const pkg of ['forevervm', 'sdk']) {
const cwd = path.resolve('../python', pkg)
await exec('rm -rf dist', { cwd })
await exec('uv build', { cwd })
await exec('uv publish --token ' + token, { cwd })
}
}
try {
await main()
} finally {
rl.close()
}
```
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
```yaml
name: Build Rust binary and create release
on:
workflow_dispatch:
inputs:
version:
description: 'Release Version'
required: true
pull_request:
branches: [ "main" ]
paths:
- ".github/workflows/release.yml"
jobs:
build-macos-arm64:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install latest rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: beta
target: aarch64-apple-darwin
override: true
- name: Build for MacOS
run: cargo build --manifest-path=rust/forevervm/Cargo.toml --bin forevervm --target aarch64-apple-darwin --release
- name: strip binary
run: strip ./rust/target/aarch64-apple-darwin/release/forevervm
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: macos-binary-arm64
path: ./rust/target/aarch64-apple-darwin/release/forevervm
build-macos-x64:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install latest rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: beta
target: x86_64-apple-darwin
override: true
- name: Build for MacOS
run: cargo build --release --manifest-path=rust/forevervm/Cargo.toml --bin forevervm --target x86_64-apple-darwin
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: macos-binary-x64
path: ./rust/target/x86_64-apple-darwin/release/forevervm
build-linux:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
target: x86_64-unknown-linux-musl
override: true
- name: Install musl-tools
run: sudo apt-get install -y musl-tools
- name: Build for Linux
run: cargo build --release --target x86_64-unknown-linux-musl --manifest-path=rust/forevervm/Cargo.toml --bin forevervm
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: linux-binary
path: ./rust/target/x86_64-unknown-linux-musl/release/forevervm
build-linux-arm64:
runs-on: ubuntu-22.04-arm
steps:
- uses: actions/checkout@v2
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
target: aarch64-unknown-linux-musl
override: true
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y musl-tools gcc-aarch64-linux-gnu musl-dev
- name: Build for Linux
run: cargo build --release --target aarch64-unknown-linux-musl --manifest-path=rust/forevervm/Cargo.toml --bin forevervm
- uses: actions/upload-artifact@v4
with:
name: linux-binary-arm64
path: ./rust/target/aarch64-unknown-linux-musl/release/forevervm
build-windows:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
target: x86_64-pc-windows-gnu
override: true
- name: Install requirements
run: |
sudo apt-get update
sudo apt-get install -y gcc-mingw-w64 g++-mingw-w64-x86-64 nasm
- name: Set NASM path
run: |
echo "AWS_LC_SYS_PREBUILT_NASM=false" >> $GITHUB_ENV
which nasm
- name: Build for Windows
run: cargo build --release --target x86_64-pc-windows-gnu --manifest-path=rust/forevervm/Cargo.toml --bin forevervm
- name: ls
run: ls ./rust/target/x86_64-pc-windows-gnu/release
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: windows-binary
path: ./rust/target/x86_64-pc-windows-gnu/release/forevervm.exe
release:
needs: [build-linux, build-macos-arm64, build-macos-x64, build-windows, build-linux-arm64]
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
- name: Compress binary
run: >
gzip -9 --keep --force
windows-binary/forevervm.exe
linux-binary/forevervm
macos-binary-x64/forevervm
macos-binary-arm64/forevervm
linux-binary-arm64/forevervm
- name: ls
run: ls -la
- name: rename and move
run: >
mv windows-binary/forevervm.exe.gz forevervm-win-x64.exe.gz &&
mv linux-binary/forevervm.gz forevervm-linux-x64.gz &&
mv macos-binary-x64/forevervm.gz forevervm-macos-x64.gz &&
mv macos-binary-arm64/forevervm.gz forevervm-macos-arm64.gz &&
mv linux-binary-arm64/forevervm.gz forevervm-linux-arm64.gz
- name: Release
uses: softprops/action-gh-release@v2
with:
name: ${{ github.event.inputs.version }}
draft: true
generate_release_notes: true
tag_name: ${{ github.event.inputs.version }}
files: |
forevervm-win-x64.exe.gz
forevervm-linux-x64.gz
forevervm-macos-x64.gz
forevervm-macos-arm64.gz
forevervm-linux-arm64.gz
```
--------------------------------------------------------------------------------
/python/sdk/forevervm_sdk/__init__.py:
--------------------------------------------------------------------------------
```python
import httpx
from typing import Type, cast, TypeVar
from .config import API_BASE_URL
from .repl import Repl
from .types import (
CreateMachineRequest,
CreateMachineResponse,
ExecResponse,
ExecResultResponse,
ListMachinesResponse,
RequestOptions,
WhoamiResponse,
)
from forevervm_sdk.config import DEFAULT_INSTRUCTION_TIMEOUT_SECONDS
T = TypeVar("T")
class ForeverVM:
__client: httpx.Client | None = None
__client_async: httpx.AsyncClient | None = None
def __init__(self, token: str, base_url=API_BASE_URL):
self._token = token
self._base_url = base_url
def _url(self, path: str):
return f"{self._base_url}{path}"
def _headers(self):
return {"authorization": f"Bearer {self._token}", "x-forevervm-sdk": "python"}
@property
def _client(self):
if self.__client is None:
self.__client = httpx.Client(headers=self._headers())
return self.__client
@property
def _client_async(self):
if self.__client_async is None:
self.__client_async = httpx.AsyncClient()
return self.__client_async
def _get(self, path: str, type: Type[T], **kwargs: RequestOptions) -> T:
response = self._client.get(self._url(path), headers=self._headers(), **kwargs)
response.raise_for_status()
json = response.json()
return cast(T, json) if type else json
async def _get_async(self, path: str, type: Type[T], **kwargs: RequestOptions) -> T:
response = await self._client_async.get(
self._url(path), headers=self._headers(), **kwargs
)
response.raise_for_status()
json = response.json()
return cast(T, json) if type else json
def _post(self, path, type: Type[T], data=None, **kwargs: RequestOptions):
response = self._client.post(
self._url(path), headers=self._headers(), json=data, **kwargs
)
response.raise_for_status()
json = response.json()
return cast(T, json) if type else json
async def _post_async(
self, path, type: Type[T], data=None, **kwargs: RequestOptions
):
response = await self._client_async.post(
self._url(path), headers=self._headers(), json=data, **kwargs
)
response.raise_for_status()
json = response.json()
return cast(T, json) if type else json
def whoami(self):
return self._get("/v1/whoami", type=WhoamiResponse)
def whoami_async(self):
return self._get_async("/v1/whoami", type=WhoamiResponse)
def create_machine(self, tags: dict[str, str] = None, memory_mb: int = None):
request: CreateMachineRequest = {}
if tags:
request["tags"] = tags
if memory_mb is not None:
request["memory_mb"] = memory_mb
return self._post("/v1/machine/new", type=CreateMachineResponse, data=request)
def create_machine_async(self, tags: dict[str, str] = None, memory_mb: int = None):
request: CreateMachineRequest = {}
if tags:
request["tags"] = tags
if memory_mb is not None:
request["memory_mb"] = memory_mb
return self._post_async(
"/v1/machine/new", type=CreateMachineResponse, data=request
)
def list_machines(self):
return self._get("/v1/machine/list", type=ListMachinesResponse)
def list_machines_async(self):
return self._get_async("/v1/machine/list", type=ListMachinesResponse)
def exec(
self,
code: str,
machine_name: str | None = None,
interrupt: bool = False,
timeout_seconds: int = DEFAULT_INSTRUCTION_TIMEOUT_SECONDS,
):
if not machine_name:
new_machine = self.create_machine()
machine_name = new_machine["machine_name"]
return self._post(
f"/v1/machine/{machine_name}/exec",
type=ExecResponse,
data={
"instruction": {"code": code, "timeout_seconds": timeout_seconds},
"interrupt": interrupt,
},
)
async def exec_async(
self,
code: str,
machine_name: str | None = None,
interrupt: bool = False,
timeout_seconds: int = DEFAULT_INSTRUCTION_TIMEOUT_SECONDS,
):
if not machine_name:
new_machine = await self.create_machine_async()
machine_name = new_machine["machine_name"]
return await self._post_async(
f"/v1/machine/{machine_name}/exec",
type=ExecResponse,
data={
"instruction": {"code": code, "timeout_seconds": timeout_seconds},
"interrupt": interrupt,
},
)
def exec_result(self, machine_name: str, instruction_id: int):
return self._get(
f"/v1/machine/{machine_name}/exec/{instruction_id}/result",
type=ExecResultResponse,
timeout=1_200,
)
def exec_result_async(self, machine_name: str, instruction_id: int):
return self._get_async(
f"/v1/machine/{machine_name}/exec/{instruction_id}/result",
type=ExecResultResponse,
timeout=1_200,
)
def repl(self, machine_name="new") -> Repl:
return Repl(
token=self._token, machine_name=machine_name, base_url=self._base_url
)
```
--------------------------------------------------------------------------------
/python/sdk/tests/test_connect.py:
--------------------------------------------------------------------------------
```python
from os import environ
import pytest
from forevervm_sdk import ForeverVM
FOREVERVM_API_BASE = environ.get("FOREVERVM_API_BASE")
FOREVERVM_TOKEN = environ.get("FOREVERVM_TOKEN")
if not FOREVERVM_API_BASE:
raise Exception("FOREVERVM_API_BASE is not set")
if not FOREVERVM_TOKEN:
raise Exception("FOREVERVM_TOKEN is not set")
def test_whoami():
fvm = ForeverVM(FOREVERVM_TOKEN, base_url=FOREVERVM_API_BASE)
assert fvm.whoami()["account"]
def test_create_machine():
fvm = ForeverVM(FOREVERVM_TOKEN, base_url=FOREVERVM_API_BASE)
machine = fvm.create_machine()
assert machine["machine_name"]
machine_name = machine["machine_name"]
machines = fvm.list_machines()["machines"]
assert machine_name in [m["name"] for m in machines]
@pytest.mark.asyncio
async def test_exec():
fvm = ForeverVM(FOREVERVM_TOKEN, base_url=FOREVERVM_API_BASE)
machine_name = fvm.create_machine()["machine_name"]
# sync
code = "print(123) or 567"
result = fvm.exec(code, machine_name)
instruction_seq = result["instruction_seq"]
exec_result = fvm.exec_result(machine_name, instruction_seq)
assert exec_result["result"]["value"] == "567"
# async
result = await fvm.exec_async(code, machine_name)
instruction_seq = result["instruction_seq"]
exec_result = await fvm.exec_result_async(machine_name, instruction_seq)
assert exec_result["result"]["value"] == "567"
def test_repl():
fvm = ForeverVM(FOREVERVM_TOKEN, base_url=FOREVERVM_API_BASE)
machine_name = fvm.create_machine()["machine_name"]
repl = fvm.repl(machine_name)
assert repl
result = repl.exec("for i in range(5):\n print(i)")
output = list(result.output)
assert output == [
{"data": "0", "stream": "stdout", "seq": 0},
{"data": "1", "stream": "stdout", "seq": 1},
{"data": "2", "stream": "stdout", "seq": 2},
{"data": "3", "stream": "stdout", "seq": 3},
{"data": "4", "stream": "stdout", "seq": 4},
]
result = repl.exec("1 / 0")
assert "ZeroDivisionError" in result.result["error"]
def test_repl_timeout():
fvm = ForeverVM(FOREVERVM_TOKEN, base_url=FOREVERVM_API_BASE)
machine_name = fvm.create_machine()["machine_name"]
repl = fvm.repl(machine_name)
assert repl
result = repl.exec("from time import sleep")
result.result
result = repl.exec("sleep(10)", timeout_seconds=1)
assert "Timed out" in result.result["error"]
result = repl.exec("sleep(1); print('done')", timeout_seconds=5)
output = list(result.output)
assert output == [
{"data": "done", "stream": "stdout", "seq": 0},
]
result.result
@pytest.mark.asyncio
async def test_exec_timeout():
fvm = ForeverVM(FOREVERVM_TOKEN, base_url=FOREVERVM_API_BASE)
machine_name = fvm.create_machine()["machine_name"]
result = fvm.exec("from time import sleep", machine_name)
instruction_seq = result["instruction_seq"]
fvm.exec_result(machine_name, instruction_seq)
# sync
code = "sleep(10)"
result = fvm.exec(code, machine_name, timeout_seconds=1)
instruction_seq = result["instruction_seq"]
exec_result = fvm.exec_result(machine_name, instruction_seq)
assert "Timed out" in exec_result["result"]["error"]
# async
result = await fvm.exec_async(code, machine_name, timeout_seconds=1)
instruction_seq = result["instruction_seq"]
exec_result = await fvm.exec_result_async(machine_name, instruction_seq)
assert "Timed out" in exec_result["result"]["error"]
def test_machine_tags():
fvm = ForeverVM(FOREVERVM_TOKEN, base_url=FOREVERVM_API_BASE)
# Create machine with tags
tags = {"environment": "test", "purpose": "sdk-test"}
machine = fvm.create_machine(tags=tags)
assert machine["machine_name"]
machine_name = machine["machine_name"]
# Verify the tags are returned when listing machines
machines = fvm.list_machines()["machines"]
tagged_machine = next((m for m in machines if m["name"] == machine_name), None)
assert tagged_machine is not None
assert "tags" in tagged_machine
assert tagged_machine["tags"] == tags
# Create another machine with different tags
tags2 = {"environment": "test", "version": "1.0.0"}
machine2 = fvm.create_machine(tags=tags2)
assert machine2["machine_name"]
machine_name2 = machine2["machine_name"]
# Verify both machines with their respective tags
machines = fvm.list_machines()["machines"]
tagged_machine1 = next((m for m in machines if m["name"] == machine_name), None)
tagged_machine2 = next((m for m in machines if m["name"] == machine_name2), None)
assert tagged_machine1 is not None
assert tagged_machine2 is not None
assert tagged_machine1["tags"] == tags
assert tagged_machine2["tags"] == tags2
@pytest.mark.asyncio
async def test_machine_tags_async():
fvm = ForeverVM(FOREVERVM_TOKEN, base_url=FOREVERVM_API_BASE)
# Create machine with tags asynchronously
tags = {"environment": "test-async", "purpose": "async-test"}
machine = await fvm.create_machine_async(tags=tags)
assert machine["machine_name"]
machine_name = machine["machine_name"]
# Verify the tags are returned when listing machines asynchronously
machines = await fvm.list_machines_async()
machines_list = machines["machines"]
tagged_machine = next((m for m in machines_list if m["name"] == machine_name), None)
assert tagged_machine is not None
assert "tags" in tagged_machine
assert tagged_machine["tags"] == tags
```
--------------------------------------------------------------------------------
/rust/forevervm/src/commands/auth.rs:
--------------------------------------------------------------------------------
```rust
use crate::{config::ConfigManager, util::get_runner};
use colorize::AnsiColor;
use dialoguer::{theme::ColorfulTheme, Input, Password};
use forevervm_sdk::{
api::{api_types::ApiSignupRequest, token::ApiToken, ApiErrorResponse},
client::ForeverVMClient,
util::{validate_account_name, validate_email},
};
use reqwest::{Client, Url};
pub async fn whoami() -> anyhow::Result<()> {
let client = ConfigManager::new()?.client()?;
match client.whoami().await {
Ok(whoami) => {
println!(
"Logged in to {} as {}",
client.server_url().to_string().b_magenta(),
whoami.account.b_green(),
);
}
Err(err) => {
return Err(anyhow::anyhow!(err));
}
}
Ok(())
}
pub async fn signup(base_url: Url) -> anyhow::Result<()> {
let config_manager = ConfigManager::new()?;
let config = config_manager.load()?;
if config.token.is_some() {
println!("Already logged in");
return Ok(());
}
println!(
"Enter your email and an account name below, and we'll send you a ForeverVM API token!\n"
);
let email = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter your email")
.allow_empty(false)
.validate_with(|input: &String| -> Result<(), &str> {
if validate_email(input.trim()) {
Ok(())
} else {
Err("Please enter a valid email address (example: [email protected])")
}
})
.interact_text()?
.trim()
.to_string();
let account_name = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Give your account a name")
.allow_empty(false)
.validate_with(|input: &String| -> Result<(), &str> {
if validate_account_name(input.trim()) {
Ok(())
} else {
Err("Account names must be between 3 and 16 characters, and can only contain alphanumeric characters, underscores, and hyphens. (Note: account names are not case-sensitive.)")
}
})
.interact_text()?
.trim()
.to_string();
let client = Client::new();
// base_url is always suffixed with a /
let url = format!("{}internal/signup", base_url);
let runner = get_runner();
let response = client
.post(url)
.header("x-forevervm-runner", &runner)
.json(&ApiSignupRequest {
email: email.clone(),
account_name: account_name.clone(),
})
.send()
.await?;
if response.status().is_success() {
let mut command: String = "forevervm login".to_string();
// binaries installed with cargo are executed without typing `cargo` first
if runner != "cargo" {
command = format!("{runner} {command}");
}
println!(
"\nSuccess! Check your email for your API token! Then run {} to log in.\n",
command.b_green()
);
return Ok(());
}
let status_code = response.status();
let response_body = response.text().await?;
match serde_json::from_str::<ApiErrorResponse>(&response_body) {
Ok(body) => {
if body.code == "AccountNameAlreadyExists" {
Err(anyhow::anyhow!(
"Account already exists. Please sign up with a different account name."
))
} else if body.code == "EmailAlreadyExists" {
Err(anyhow::anyhow!("Email is already signed up. Check your email for your API token, or use a different email address."))
} else {
Err(anyhow::anyhow!(body))
}
}
Err(err) => Err(anyhow::anyhow!(format!(
"Unable to parse response as JSON. status code: {}, error: {}. response body: {}",
status_code, err, response_body
))),
}
}
pub async fn login(base_url: Url) -> anyhow::Result<()> {
let config_manager = ConfigManager::new()?;
let config = config_manager.load()?;
if config.server_url()? == base_url {
if let Some(token) = &config.token {
let client = ForeverVMClient::new(base_url.clone(), token.clone());
match client.whoami().await {
Ok(whoami) => {
println!("Already logged in as {}", whoami.account.b_green());
return Ok(());
}
Err(err) => {
println!("There is an existing token, but it gives an error: {}", err);
println!("The existing token will be replaced.")
}
}
}
} else if config.token.is_some() {
println!("There is an existing token for another server. It will be replaced.")
}
let token = Password::new().with_prompt("Enter your token").interact()?;
let token = ApiToken::new(token)?;
let client = ForeverVMClient::new(base_url.clone(), token.clone());
match client.whoami().await {
Ok(whoami) => {
println!("Logged in as {}", whoami.account.b_green());
}
Err(err) => {
println!("Error: {}", err);
return Err(err.into());
}
}
let mut config = config;
config.token = Some(token);
config.server_url = Some(base_url);
config_manager.save(&config)?;
Ok(())
}
pub async fn logout() -> anyhow::Result<()> {
let config_manager = ConfigManager::new()?;
let mut config = config_manager.load()?;
if config.token.is_none() {
println!("Not currently logged in");
return Ok(());
}
// Clear the token
config.token = None;
config_manager.save(&config)?;
println!("Successfully logged out");
Ok(())
}
```
--------------------------------------------------------------------------------
/rust/forevervm-sdk/src/client/mod.rs:
--------------------------------------------------------------------------------
```rust
use crate::{
api::{
api_types::{ApiExecRequest, ApiExecResponse, ApiExecResultResponse, Instruction},
http_api::{
CreateMachineRequest, CreateMachineResponse, ListMachinesRequest, ListMachinesResponse,
WhoamiResponse,
},
id_types::{InstructionSeq, MachineName},
protocol::MessageFromServer,
token::ApiToken,
},
util::get_runner,
};
use error::{ClientError, Result};
use futures_util::{Stream, StreamExt};
use repl::ReplConnection;
use reqwest::{
header::{HeaderMap, HeaderValue},
Client, Method, Response, Url,
};
use serde::{de::DeserializeOwned, Serialize};
use std::pin::Pin;
pub mod error;
pub mod repl;
pub mod typed_socket;
pub mod util;
pub struct ForeverVMClient {
api_base: Url,
client: Client,
token: ApiToken,
}
async fn parse_error(response: Response) -> Result<ClientError> {
let code = response.status().as_u16();
let message = response.text().await?;
if let Ok(err) = serde_json::from_str(&message) {
Err(ClientError::ApiError(err))
} else {
Err(ClientError::ServerResponseError { code, message })
}
}
impl ForeverVMClient {
pub fn new(api_base: Url, token: ApiToken) -> Self {
Self {
api_base,
token,
client: Client::new(),
}
}
pub fn server_url(&self) -> &Url {
&self.api_base
}
fn headers() -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert("x-forevervm-sdk", HeaderValue::from_static("rust"));
if let Some(val) = get_runner().and_then(|v| HeaderValue::from_str(&v).ok()) {
headers.insert("x-forevervm-runner", val);
}
headers
}
pub async fn repl(&self, machine_name: &MachineName) -> Result<ReplConnection> {
let mut base_url = self.api_base.clone();
match base_url.scheme() {
"http" => {
base_url
.set_scheme("ws")
.map_err(|_| ClientError::InvalidUrl)?;
}
"https" => {
base_url
.set_scheme("wss")
.map_err(|_| ClientError::InvalidUrl)?;
}
_ => return Err(ClientError::InvalidUrl),
}
let url = base_url.join(&format!("/v1/machine/{machine_name}/repl"))?;
ReplConnection::new(url, self.token.clone()).await
}
async fn post_request<Request: Serialize, Response: DeserializeOwned>(
&self,
path: &str,
request: Request,
) -> Result<Response> {
let url = self.api_base.join(&format!("/v1{}", path))?;
let response = self
.client
.request(Method::POST, url)
.headers(ForeverVMClient::headers())
.bearer_auth(self.token.to_string())
.json(&request)
.send()
.await?;
if !response.status().is_success() {
return Err(parse_error(response).await?);
}
Ok(response.json().await?)
}
async fn get_request<Response: DeserializeOwned>(&self, path: &str) -> Result<Response> {
let url = self.api_base.join(&format!("/v1{}", path))?;
let response = self
.client
.request(Method::GET, url)
.headers(ForeverVMClient::headers())
.bearer_auth(self.token.to_string())
.send()
.await?;
if !response.status().is_success() {
return Err(parse_error(response).await?);
}
Ok(response.json().await?)
}
pub async fn create_machine(
&self,
options: CreateMachineRequest,
) -> Result<CreateMachineResponse> {
self.post_request("/machine/new", options).await
}
pub async fn list_machines(
&self,
options: ListMachinesRequest,
) -> Result<ListMachinesResponse> {
self.post_request("/machine/list", options).await
}
pub async fn exec_instruction(
&self,
machine_name: &MachineName,
instruction: Instruction,
) -> Result<ApiExecResponse> {
let request = ApiExecRequest {
instruction,
interrupt: false,
};
self.post_request(&format!("/machine/{machine_name}/exec"), request)
.await
}
pub async fn exec_result(
&self,
machine_name: &MachineName,
instruction: InstructionSeq,
) -> Result<ApiExecResultResponse> {
self.get_request(&format!(
"/machine/{machine_name}/exec/{instruction}/result"
))
.await
}
pub async fn whoami(&self) -> Result<WhoamiResponse> {
self.get_request("/whoami").await
}
/// Returns a stream of `MessageFromServer` values from the execution result endpoint.
///
/// This method uses HTTP streaming to receive newline-delimited JSON responses
/// from the server. Each line is parsed into a `MessageFromServer` object.
pub async fn exec_result_stream(
&self,
machine_name: &MachineName,
instruction: InstructionSeq,
) -> Result<Pin<Box<dyn Stream<Item = Result<MessageFromServer>> + Send>>> {
let url = self.server_url().join(&format!(
"/v1/machine/{machine_name}/exec/{instruction}/stream-result"
))?;
let request = self
.client
.request(Method::GET, url)
.headers(ForeverVMClient::headers())
.bearer_auth(self.token.to_string())
.build()?;
let response = self.client.execute(request).await?;
if !response.status().is_success() {
return Err(parse_error(response).await?);
}
let stream = async_stream::stream! {
let mut bytes_stream = response.bytes_stream();
let mut buffer = String::new();
while let Some(bytes) = bytes_stream.next().await {
let mut value = String::from_utf8_lossy(&bytes?).to_string();
'chunk: loop {
if let Some((first, rest)) = value.split_once('\n') {
let json = &format!("{buffer}{first}");
yield match serde_json::from_str::<MessageFromServer>(json) {
Ok(message) => Ok(message),
Err(err) => Err(ClientError::from(err)),
};
value = String::from(rest);
buffer = String::new();
} else {
buffer += &value;
break 'chunk;
}
}
}
};
Ok(Box::pin(stream))
}
}
```
--------------------------------------------------------------------------------
/rust/forevervm-sdk/tests/basic_sdk_tests.rs:
--------------------------------------------------------------------------------
```rust
use forevervm_sdk::api::api_types::Instruction;
use forevervm_sdk::api::http_api::{CreateMachineRequest, ListMachinesRequest};
use forevervm_sdk::api::protocol::MessageFromServer;
use forevervm_sdk::{
api::{api_types::ExecResultType, protocol::StandardOutputStream, token::ApiToken},
client::ForeverVMClient,
};
use futures_util::StreamExt;
use std::env;
use url::Url;
fn get_test_credentials() -> (Url, ApiToken) {
let api_base = env::var("FOREVERVM_API_BASE")
.expect("FOREVERVM_API_BASE environment variable must be set");
let token =
env::var("FOREVERVM_TOKEN").expect("FOREVERVM_TOKEN environment variable must be set");
(
Url::parse(&api_base).unwrap(),
ApiToken::new(token).unwrap(),
)
}
#[tokio::test]
async fn test_whoami() {
let (api_base, token) = get_test_credentials();
let client = ForeverVMClient::new(api_base, token);
let whoami = client.whoami().await.expect("whoami call failed");
assert!(!whoami.account.is_empty());
}
#[tokio::test]
async fn test_create_machine() {
let (api_base, token) = get_test_credentials();
let client = ForeverVMClient::new(api_base, token);
// Create a new machine
let machine = client
.create_machine(CreateMachineRequest::default())
.await
.expect("failed to create machine");
let machine_name = machine.machine_name;
assert!(!machine_name.to_string().is_empty());
// Verify machine appears in list
let machines = client
.list_machines(ListMachinesRequest::default())
.await
.expect("failed to list machines");
assert!(machines.machines.iter().any(|m| m.name == machine_name));
}
#[tokio::test]
async fn test_exec() {
let (api_base, token) = get_test_credentials();
let client = ForeverVMClient::new(api_base, token);
// Create machine and execute code
let machine = client
.create_machine(CreateMachineRequest::default())
.await
.expect("failed to create machine");
let code = "print(123) or 567";
let result = client
.exec_instruction(
&machine.machine_name,
Instruction {
code: code.to_string(),
timeout_seconds: 10,
},
)
.await
.expect("exec failed");
let exec_result = client
.exec_result(
&machine.machine_name,
result.instruction_seq.expect("instruction seq missing"),
)
.await
.expect("failed to get exec result");
assert_eq!(
exec_result.result.result,
ExecResultType::Value {
value: Some("567".to_string()),
data: None
}
);
}
#[tokio::test]
async fn test_exec_stream() {
let (api_base, token) = get_test_credentials();
let client = ForeverVMClient::new(api_base, token);
// Create machine and execute code
let machine = client
.create_machine(CreateMachineRequest::default())
.await
.expect("failed to create machine");
let code = "for i in range(10): print(i)\n'done'";
let result = client
.exec_instruction(
&machine.machine_name,
Instruction {
code: code.to_string(),
timeout_seconds: 10,
},
)
.await
.expect("exec failed");
let mut stream = client
.exec_result_stream(
&machine.machine_name,
result.instruction_seq.expect("instruction seq missing"),
)
.await
.expect("failed to get exec result");
let mut i = 0;
while let Some(msg) = stream.next().await {
match msg {
Ok(MessageFromServer::Output { chunk, .. }) => {
assert_eq!(chunk.data, format!("{}", i));
i += 1;
}
Ok(MessageFromServer::Result(chunk)) => {
assert_eq!(
chunk.result.result,
ExecResultType::Value {
value: Some("'done'".to_string()),
data: None
}
);
}
_ => {
panic!("unexpected message");
}
}
}
}
#[tokio::test]
async fn test_exec_stream_image() {
let (api_base, token) = get_test_credentials();
let client = ForeverVMClient::new(api_base, token);
// Create machine and execute code
let machine = client
.create_machine(CreateMachineRequest::default())
.await
.expect("failed to create machine");
let code = "import matplotlib.pyplot as plt
plt.plot([0, 1, 2], [0, 1, 2])
plt.title('Simple Plot')
plt.show()";
let result = client
.exec_instruction(
&machine.machine_name,
Instruction {
code: code.to_string(),
timeout_seconds: 10,
},
)
.await
.expect("exec failed");
let mut stream = client
.exec_result_stream(
&machine.machine_name,
result.instruction_seq.expect("instruction seq missing"),
)
.await
.expect("failed to get exec result");
while let Some(chunk) = stream.next().await {
assert!(chunk.is_ok(), "chunk should parse as JSON");
}
}
#[tokio::test]
async fn test_repl() {
let (api_base, token) = get_test_credentials();
let client = ForeverVMClient::new(api_base, token);
// Create machine and get REPL
let machine = client
.create_machine(CreateMachineRequest::default())
.await
.expect("failed to create machine");
let mut repl = client
.repl(&machine.machine_name)
.await
.expect("failed to create REPL");
assert_eq!(repl.machine_name, machine.machine_name);
// Execute code that produces multiple outputs
let code = "for i in range(5):\n print(i)";
let mut result = repl.exec(code).await.expect("failed to execute code");
// Collect all output
let mut outputs = Vec::new();
while let Some(output) = result.next().await {
outputs.push(output);
}
// Verify outputs
assert_eq!(outputs.len(), 5);
for (i, output) in outputs.iter().enumerate() {
assert_eq!(output.stream, StandardOutputStream::Stdout);
assert_eq!(output.data, i.to_string());
assert_eq!(output.seq, (i as i64).into());
}
// Execute code that results in an error
let code = "1 / 0";
let exec_result = repl.exec(code).await.expect("failed to execute code");
let result = exec_result.result().await.unwrap();
let ExecResultType::Error { error } = result.result else {
panic!("Expected error");
};
assert!(error.contains("ZeroDivisionError"));
assert!(error.contains("division by zero"));
}
```
--------------------------------------------------------------------------------
/javascript/sdk/src/repl.ts:
--------------------------------------------------------------------------------
```typescript
import WebSocket from 'isomorphic-ws'
import { env } from './env'
import type { ExecResponse, MessageFromServer, StandardOutput } from './types'
export interface ExecOptions {
timeoutSeconds?: number
interrupt?: boolean
}
export interface Instruction {
code: string
timeout_seconds?: number
}
export type MessageToServer = {
type: 'exec'
instruction: Instruction
request_id: number
}
interface ReplOptions {
baseUrl?: string
token?: string
machine?: string
}
let createWebsocket = function (url: string, token: string) {
if (typeof window === 'undefined') {
return new WebSocket(url, {
headers: { 'Authorization': `Bearer ${token}`, 'x-forevervm-sdk': 'javascript' },
})
}
return new WebSocket(url + `?_forevervm_jwt=${token}`)
}
if (typeof CustomEvent !== 'function') {
class CustomEvent extends Event {
type: string
detail: any
bubbles: boolean
cancelable: boolean
constructor(type: string, params: any = {}) {
super(type, params)
this.type = type
this.detail = params.detail || null
this.bubbles = !!params.bubbles
this.cancelable = !!params.cancelable
}
}
// Make it globally available
;(global as any).CustomEvent = CustomEvent
}
export class Repl {
#baseUrl = 'wss://api.forevervm.com'
#token = env.FOREVERVM_TOKEN || ''
#machine: string | null = null
#ws: WebSocket
#listener = new EventTarget()
#queued: MessageToServer | undefined
#nextRequestId = 0
#retries = 0
constructor(options: ReplOptions = {}) {
if (options.token) this.#token = options.token
if (options.baseUrl) this.#baseUrl = options.baseUrl
if (options.machine) this.#machine = options.machine
if (!this.#token) {
throw new Error(
'foreverVM token must be supplied as either `options.token` or the environment variable `FOREVERVM_TOKEN`.',
)
}
this.#ws = this.#connect()
}
#connect() {
if (this.#ws && this.#ws.readyState !== WebSocket.CLOSED) return this.#ws
const machine = this.#machine || 'new'
const url = `${this.#baseUrl}/v1/machine/${machine}/repl`
this.#ws = createWebsocket(url, this.#token)
this.#ws.addEventListener('open', () => {
this.#retries = 0
const queued = this.#queued
this.#queued = undefined
if (queued) this.#send(queued)
})
this.#ws.addEventListener('close', () => this.#reconnect())
this.#ws.addEventListener('error', () => this.#reconnect())
this.#ws.addEventListener('message', ({ data }) => {
const msg = JSON.parse(data.toString()) as MessageFromServer
if (msg.type === 'connected') {
if (this.#machine && this.#machine !== msg.machine_name) {
console.warn(`Expected machine name ${this.#machine} but recevied ${msg.machine_name}`)
}
this.#machine = msg.machine_name
}
this.#listener.dispatchEvent(new CustomEvent('msg', { detail: msg }))
})
return this.#ws
}
async #reconnect() {
if (this.connecting) return
if (this.#retries > 0) {
const wait = 2 ** (this.#retries - 1)
await new Promise((resolve) => setTimeout(resolve, wait))
}
this.#retries += 1
this.#connect()
}
#send(message: MessageToServer) {
if (this.connected) this.#ws.send(JSON.stringify(message))
else this.#queued = message
}
get machineName() {
return this.#machine
}
get connected() {
return this.#ws?.readyState === WebSocket.OPEN
}
get connecting() {
return this.#ws?.readyState === WebSocket.CONNECTING
}
exec(code: string, options: ExecOptions = {}): ReplExecResult {
const request_id = this.#nextRequestId++
const instruction = { code, timeout_seconds: options.timeoutSeconds }
this.#send({ type: 'exec', instruction, request_id })
this.#listener = new EventTarget()
return new ReplExecResult(request_id, this.#listener)
}
}
export class ReplExecResult {
#requestId: number
#listener: EventTarget
// instruction state
#instructionId: number | undefined
// stdout/stderr state
#buffer: StandardOutput[] = []
#advance: (() => void) | undefined = undefined
// result state
#done = false
#resolve: (response: ExecResponse) => void = () => {}
#reject: (reason: any) => void = () => {}
result: Promise<ExecResponse>
constructor(requestId: number, listener: EventTarget) {
this.#requestId = requestId
this.#listener = listener
this.#listener.addEventListener('msg', this)
this.result = new Promise<ExecResponse>((resolve, reject) => {
this.#resolve = resolve
this.#reject = reject
})
}
get output(): { [Symbol.asyncIterator](): AsyncIterator<StandardOutput, void, unknown> } {
return {
[Symbol.asyncIterator]: () => ({
next: async () => {
while (true) {
const value = this.#buffer.shift()
if (value) return { value, done: false }
if (this.#done) return { value: undefined, done: true }
await new Promise<void>((resolve) => {
this.#advance = resolve
})
}
},
}),
}
}
#flush() {
while (this.#advance) {
this.#advance()
this.#advance = undefined
}
}
handleEvent(event: CustomEvent) {
const msg = event.detail as MessageFromServer
switch (msg.type) {
case 'exec_received':
if (msg.request_id !== this.#requestId) {
console.warn(`Expected request ID ${this.#requestId} with message`, msg)
break
}
this.#instructionId = msg.seq
break
case 'output':
if (msg.instruction_id !== this.#instructionId) {
console.warn(`Expected instruction ID ${this.#instructionId} with message`, msg)
break
}
this.#buffer.push(msg.chunk)
this.#flush()
break
case 'result':
if (msg.instruction_id !== this.#instructionId) {
console.warn(`Expected instruction ID ${this.#instructionId} with message`, msg)
break
}
this.#done = true
this.#flush()
this.#resolve(msg.result)
break
case 'error':
this.#reject(new Error(msg.code))
}
}
}
if (import.meta.vitest) {
const { test, expect, beforeAll } = import.meta.vitest
const FOREVERVM_TOKEN = process.env.FOREVERVM_TOKEN || ''
const FOREVERVM_API_BASE = process.env.FOREVERVM_API_BASE || ''
let ws: WebSocket
beforeAll(() => {
createWebsocket = (url: string, token: string) => {
ws = new WebSocket(url, { headers: { Authorization: `Bearer ${token}` } })
return ws
}
})
test.sequential('explicit token', async () => {
const repl = new Repl({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
const { value, error } = await repl.exec('1 + 1').result
expect(value).toBe('2')
expect(error).toBeUndefined()
})
test.sequential('return value', async () => {
const repl = new Repl({ baseUrl: FOREVERVM_API_BASE })
const { value, error } = await repl.exec('1 + 1').result
expect(value).toBe('2')
expect(error).toBeUndefined()
})
test.sequential('output', async () => {
const repl = new Repl({ baseUrl: FOREVERVM_API_BASE })
const output = repl.exec('for i in range(5):\n print(i)').output
let i = 0
for await (const { data, stream, seq } of output) {
expect(data).toBe(`${i}`)
expect(stream).toBe('stdout')
expect(seq).toBe(i)
i += 1
}
const { done } = await output[Symbol.asyncIterator]().next()
expect(done).toBe(true)
})
test.sequential('error', async () => {
const repl = new Repl({ baseUrl: FOREVERVM_API_BASE })
const { value, error } = await repl.exec('1 / 0').result
expect(value).toBeUndefined()
expect(error).toMatch('ZeroDivisionError')
})
test.sequential('reconnect', { timeout: 10000 }, async () => {
const repl = new Repl({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
await repl.exec('1 + 1').result
const machineName = repl.machineName
ws.close()
const { value, error } = await repl.exec('1 + 1').result
expect(value).toBe('2')
expect(error).toBeUndefined()
expect(repl.machineName).toBe(machineName)
})
}
```
--------------------------------------------------------------------------------
/javascript/mcp-server/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ListPromptsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod'
import { ForeverVM } from '@forevervm/sdk'
import fs from 'fs'
import path from 'path'
import os from 'os'
import { Command } from 'commander'
import { installForeverVM } from './install/index.js'
const DEFAULT_FOREVERVM_SERVER = 'https://api.forevervm.com'
interface ForeverVMOptions {
token?: string
baseUrl?: string
}
// Zod schema
const ExecMachineSchema = z.object({
pythonCode: z.string(),
replId: z.string(),
})
const RUN_REPL_TOOL_NAME = 'run-python-in-repl'
const CREATE_REPL_MACHINE_TOOL_NAME = 'create-python-repl'
export function getForeverVMOptions(): ForeverVMOptions {
if (process.env.FOREVERVM_TOKEN) {
return {
token: process.env.FOREVERVM_TOKEN,
baseUrl: process.env.FOREVERVM_BASE_URL || DEFAULT_FOREVERVM_SERVER,
}
}
const configFilePath = path.join(os.homedir(), '.config', 'forevervm', 'config.json')
if (!fs.existsSync(configFilePath)) {
console.error('ForeverVM config file not found at:', configFilePath)
process.exit(1)
}
try {
const fileContent = fs.readFileSync(configFilePath, 'utf8')
const config = JSON.parse(fileContent)
if (!config.token) {
console.error('ForeverVM config file does not contain a token')
process.exit(1)
}
let baseUrl = config.server_url || DEFAULT_FOREVERVM_SERVER
// remove trailing slash
baseUrl = baseUrl.replace(/\/$/, '')
return {
token: config.token,
baseUrl,
}
} catch (error) {
console.error('Failed to read ForeverVM config file:', error)
process.exit(1)
}
}
// ForeverVM integration
interface ExecReplResponse {
output: string
result: string
replId: string
error?: string
image?: string
}
async function makeExecReplRequest(
forevervmOptions: ForeverVMOptions,
pythonCode: string,
replId: string,
): Promise<ExecReplResponse> {
try {
const fvm = new ForeverVM(forevervmOptions)
const repl = await fvm.repl(replId)
const execResult = await repl.exec(pythonCode)
const output: string[] = []
for await (const nextOutput of execResult.output) {
output.push(nextOutput.data)
}
const result = await execResult.result
const imageResult = result.data?.['png'] as string | undefined
if (typeof result.value === 'string') {
return {
output: output.join('\n'),
result: result.value,
replId: replId,
image: imageResult,
}
} else if (result.value === null) {
return {
output: output.join('\n'),
result: 'The code did not return a value',
replId: replId,
image: imageResult,
}
} else if (result.error) {
return {
output: output.join('\n'),
result: '',
replId: replId,
error: `Error: ${result.error}`,
image: imageResult,
}
} else {
return {
output: output.join('\n'),
result: 'No result or error returned',
replId: replId,
image: imageResult,
}
}
} catch (error: any) {
return {
error: `Failed to execute Python code: ${error}`,
output: '',
result: '',
replId: replId,
}
}
}
async function makeCreateMachineRequest(forevervmOptions: ForeverVMOptions): Promise<string> {
try {
console.error('using options', forevervmOptions)
const fvm = new ForeverVM(forevervmOptions)
const machine = await fvm.createMachine()
return machine.machine_name
} catch (error: any) {
throw new Error(`Failed to create ForeverVM machine: ${error}`)
}
}
// Start server
async function runMCPServer() {
const forevervmOptions = getForeverVMOptions()
const server = new Server(
{ name: 'forevervm', version: '1.0.0' },
{ capabilities: { tools: {}, resources: {}, prompts: {} } },
)
// List resources
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [], // No resources currently available
}
})
// List prompts
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [], // No prompts currently available
}
})
// List tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: RUN_REPL_TOOL_NAME,
description:
'Run Python code in a given REPL. Common libraries including numpy, pandas, and requests are available to be imported. External API requests are allowed.',
inputSchema: {
type: 'object',
properties: {
pythonCode: {
type: 'string',
description: 'Python code to execute in the REPL.',
},
replId: {
type: 'string',
description:
'The ID corresponding with the REPL to run the Python code on. REPLs persist global state across runs. Create a REPL once per session with the create-python-repl tool.',
},
},
required: ['pythonCode', 'replId'],
},
},
{
name: CREATE_REPL_MACHINE_TOOL_NAME,
description:
'Create a Python REPL. Global variables, imports, and function definitions are preserved between runs.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
],
}
})
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params
try {
if (name === RUN_REPL_TOOL_NAME) {
const { pythonCode, replId } = ExecMachineSchema.parse(args)
const execResponse = await makeExecReplRequest(forevervmOptions, pythonCode, replId)
if (execResponse.error) {
return {
content: [
{
type: 'text',
text: JSON.stringify(execResponse),
isError: true,
},
],
}
}
if (execResponse.image) {
return {
content: [
{
type: 'image',
data: execResponse.image,
mimeType: 'image/png',
},
],
}
}
return {
content: [
{
type: 'text',
text: JSON.stringify(execResponse),
},
],
}
} else if (name === CREATE_REPL_MACHINE_TOOL_NAME) {
let replId
try {
replId = await makeCreateMachineRequest(forevervmOptions)
} catch (error) {
return {
content: [{ type: 'text', text: `Failed to create machine: ${error}` }],
isError: true,
}
}
return {
content: [
{
type: 'text',
text: replId,
},
],
}
} else {
return {
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
isError: true,
}
}
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(
`Invalid arguments: ${error.errors
.map((e) => `${e.path.join('.')}: ${e.message}`)
.join(', ')}`,
)
}
throw error
}
})
const transport = new StdioServerTransport()
await server.connect(transport)
}
function main() {
const program = new Command()
program.name('forevervm-mcp').version('1.0.0')
program
.command('install')
.description('Set up the ForeverVM MCP server')
.option('-c, --claude', 'Set up the MCP Server for Claude Desktop')
.option('-g, --goose', 'Set up the MCP Server for Codename Goose (CLI only)')
.option('-w, --windsurf', 'Set up the MCP Server for Windsurf')
.action(installForeverVM)
program.command('run').description('Run the ForeverVM MCP server').action(runMCPServer)
if (process.argv.length === 2) {
program.outputHelp()
return
}
program.parse(process.argv)
}
main()
```
--------------------------------------------------------------------------------
/rust/forevervm-sdk/src/client/repl.rs:
--------------------------------------------------------------------------------
```rust
use super::{
typed_socket::{websocket_connect, WebSocketRecv, WebSocketSend},
util::authorized_request,
ClientError,
};
use crate::api::{
api_types::{ExecResult, Instruction},
id_types::{InstructionSeq, MachineName, RequestSeq},
protocol::{MessageFromServer, MessageToServer, StandardOutput},
token::ApiToken,
};
use std::{
ops::{Deref, DerefMut},
sync::{atomic::AtomicU32, Arc, Mutex},
};
use tokio::{
sync::{broadcast, oneshot},
task::JoinHandle,
};
pub const DEFAULT_INSTRUCTION_TIMEOUT_SECONDS: i32 = 15;
#[derive(Default)]
pub struct RequestSeqGenerator {
next: AtomicU32,
}
impl RequestSeqGenerator {
pub fn next(&self) -> RequestSeq {
let r = self.next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
r.into()
}
}
#[derive(Debug)]
pub enum ReplConnectionState {
Idle,
WaitingForInstructionSeq {
request_id: RequestSeq,
send_result_handle: oneshot::Sender<ExecResultHandle>,
},
WaitingForResult {
instruction_id: InstructionSeq,
output_sender: broadcast::Sender<StandardOutput>,
result_sender: oneshot::Sender<ExecResult>,
},
}
impl Default for ReplConnectionState {
fn default() -> Self {
Self::Idle
}
}
pub struct ReplConnection {
pub machine_name: MachineName,
request_seq_generator: RequestSeqGenerator,
sender: WebSocketSend<MessageToServer>,
receiver_handle: Option<JoinHandle<()>>,
state: Arc<Mutex<ReplConnectionState>>,
}
fn handle_message(
message: MessageFromServer,
state: Arc<Mutex<ReplConnectionState>>,
) -> Result<(), ClientError> {
let msg = message;
match msg {
MessageFromServer::ExecReceived { seq, request_id } => {
let mut state = state.lock().expect("State lock poisoned");
let old_state = std::mem::take(state.deref_mut());
match old_state {
ReplConnectionState::WaitingForInstructionSeq {
request_id: expected_request_seq,
send_result_handle: receiver_sender,
} => {
if request_id != expected_request_seq {
tracing::warn!(
?request_id,
?expected_request_seq,
"Unexpected request seq"
);
return Ok(());
}
let (output_sender, output_receiver) = broadcast::channel::<StandardOutput>(50);
let (result_sender, result_receiver) = oneshot::channel();
*state = ReplConnectionState::WaitingForResult {
instruction_id: seq,
output_sender,
result_sender,
};
let _ = receiver_sender.send(ExecResultHandle {
result: result_receiver,
receiver: output_receiver,
});
}
state => {
tracing::error!(?state, "Unexpected ExecReceived while in state {state:?}");
}
}
}
MessageFromServer::Result(result) => {
let mut state = state.lock().expect("State lock poisoned");
let old_state = std::mem::take(state.deref_mut());
match old_state {
ReplConnectionState::WaitingForResult {
instruction_id: instruction_seq,
result_sender,
..
} => {
if result.instruction_id != instruction_seq {
tracing::warn!(
?instruction_seq,
?result.instruction_id,
"Unexpected instruction seq"
);
return Ok(());
}
let _ = result_sender.send(result.result);
}
state => {
tracing::error!(?state, "Unexpected Result while in state {state:?}");
}
}
}
MessageFromServer::Output {
chunk,
instruction_id: instruction,
} => {
let state = state.lock().expect("State lock poisoned");
match state.deref() {
ReplConnectionState::WaitingForResult {
instruction_id: instruction_seq,
output_sender,
..
} => {
if *instruction_seq != instruction {
tracing::warn!(
?instruction_seq,
?instruction,
"Unexpected instruction seq"
);
return Ok(());
}
let _ = output_sender.send(chunk);
}
state => {
tracing::error!(?state, "Unexpected Output while in state {state:?}");
}
}
}
MessageFromServer::Error(err) => {
return Err(ClientError::ApiError(err));
}
MessageFromServer::Connected { machine_name: _ } => {}
msg => tracing::warn!("message type not implmented: {msg:?}"),
}
Ok(())
}
async fn receive_loop(
mut receiver: WebSocketRecv<MessageFromServer>,
state: Arc<Mutex<ReplConnectionState>>,
) {
while let Ok(Some(msg)) = receiver.recv().await {
if let Err(err) = handle_message(msg, state.clone()) {
tracing::error!(?err, "Failed to handle message");
}
}
}
impl ReplConnection {
pub async fn new(url: reqwest::Url, token: ApiToken) -> Result<Self, ClientError> {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let req = authorized_request(url, token)?;
let (sender, mut receiver) =
websocket_connect::<MessageToServer, MessageFromServer>(req).await?;
let state: Arc<Mutex<ReplConnectionState>> = Arc::default();
let machine_name = match receiver.recv().await? {
Some(MessageFromServer::Connected { machine_name }) => machine_name,
_ => {
return Err(ClientError::Other(String::from(
"Expected `connected` message from REPL.",
)))
}
};
let receiver_handle = tokio::spawn(receive_loop(receiver, state.clone()));
Ok(Self {
machine_name,
request_seq_generator: Default::default(),
sender,
receiver_handle: Some(receiver_handle),
state,
})
}
pub async fn exec(&mut self, code: &str) -> Result<ExecResultHandle, ClientError> {
let instruction = Instruction {
code: code.to_string(),
timeout_seconds: DEFAULT_INSTRUCTION_TIMEOUT_SECONDS,
};
self.exec_instruction(instruction).await
}
pub async fn exec_instruction(
&mut self,
instruction: Instruction,
) -> Result<ExecResultHandle, ClientError> {
let request_id = self.request_seq_generator.next();
let message = MessageToServer::Exec {
instruction,
request_id,
};
self.sender.send(&message).await?;
let (send_result_handle, receive_result_handle) = oneshot::channel::<ExecResultHandle>();
{
let mut state = self.state.lock().expect("State lock poisoned");
*state.deref_mut() = ReplConnectionState::WaitingForInstructionSeq {
request_id,
send_result_handle,
};
}
receive_result_handle
.await
.map_err(|_| ClientError::InstructionInterrupted)
}
}
impl Drop for ReplConnection {
fn drop(&mut self) {
if let Some(handle) = self.receiver_handle.take() {
handle.abort();
}
}
}
#[derive(Debug)]
pub struct ExecResultHandle {
result: oneshot::Receiver<ExecResult>,
receiver: broadcast::Receiver<StandardOutput>,
}
impl ExecResultHandle {
pub async fn next(&mut self) -> Option<StandardOutput> {
self.receiver.recv().await.ok()
}
pub async fn result(self) -> Result<ExecResult, ClientError> {
self.result
.await
.map_err(|_| ClientError::InstructionInterrupted)
}
}
```
--------------------------------------------------------------------------------
/javascript/sdk/src/index.ts:
--------------------------------------------------------------------------------
```typescript
import { Repl } from './repl'
import { env } from './env'
import type {
ApiExecResponse,
ApiExecResultResponse,
ApiExecResultStreamResponse,
CreateMachineRequest,
CreateMachineResponse,
ListMachinesRequest,
ListMachinesResponse,
WhoamiResponse,
} from './types'
export * from './types'
export * from './repl'
interface ForeverVMOptions {
token?: string
baseUrl?: string
timeoutSec?: number
}
const DEFAULT_TIMEOUT_SEC = 15
export class ForeverVM {
#token = env.FOREVERVM_TOKEN || ''
#baseUrl = 'https://api.forevervm.com'
#timeoutSec = DEFAULT_TIMEOUT_SEC
constructor(options: ForeverVMOptions = {}) {
if (options.token) this.#token = options.token
if (options.baseUrl) this.#baseUrl = options.baseUrl
if (options.timeoutSec) this.#timeoutSec = options.timeoutSec
}
get #headers() {
return { 'authorization': `Bearer ${this.#token}`, 'x-forevervm-sdk': 'javascript' }
}
async #get(path: string) {
const response = await fetch(`${this.#baseUrl}${path}`, { headers: this.#headers })
if (!response.ok) {
const text = await response.text().catch(() => 'Unknown error')
throw new Error(`HTTP ${response.status}: ${text}`)
}
return await response.json()
}
async *#getStream(path: string) {
const response = await fetch(`${this.#baseUrl}${path}`, { headers: this.#headers })
if (!response.ok) {
const text = await response.text().catch(() => 'Unknown error')
throw new Error(`HTTP ${response.status}: ${text}`)
}
if (!response.body) return
const decoder = new TextDecoderStream()
const reader = response.body.pipeThrough(decoder).getReader()
// buffer JSON just in case an object is split across multiple stream chunks
let buffer = ''
while (true) {
let { done, value = '' } = await reader.read()
if (done) return
// loop until we've read all the data in this chunk
while (value) {
// find the next newline character
const newline = value.indexOf('\n')
// if there are no more newlines, add the remaining data to the buffer and break
if (newline === -1) {
buffer += value
break
}
// parse and yield the next line from the data
const line = value.slice(0, newline)
yield JSON.parse(buffer + line)
// remove the just-processed line from the value and reset the buffer
value = value.slice(newline + 1)
buffer = ''
}
}
}
async #post(path: string, body?: object) {
let response
try {
response = await fetch(`${this.#baseUrl}${path}`, {
method: 'POST',
headers: { ...this.#headers, 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
})
} catch (error) {
throw new Error(`Failed to fetch: ${error}`)
}
if (!response.ok) {
const text = await response.text().catch(() => 'Unknown error')
throw new Error(`HTTP ${response.status}: ${text}`)
}
return await response.json()
}
async whoami(): Promise<WhoamiResponse> {
return await this.#get('/v1/whoami')
}
async createMachine(request: CreateMachineRequest = {}): Promise<CreateMachineResponse> {
return await this.#post('/v1/machine/new', request)
}
async listMachines(request: ListMachinesRequest = {}): Promise<ListMachinesResponse> {
return await this.#post('/v1/machine/list', request)
}
async exec(
code: string,
machineName?: string,
interrupt: boolean = false,
timeoutSec: number = this.#timeoutSec,
): Promise<ApiExecResponse> {
if (!machineName) {
const createResponse = await this.createMachine()
machineName = createResponse.machine_name
}
return await this.#post(`/v1/machine/${machineName}/exec`, {
instruction: { code, timeout_seconds: timeoutSec },
interrupt,
})
}
async execResult(machineName: string, instructionSeq: number): Promise<ApiExecResultResponse> {
return await this.#get(`/v1/machine/${machineName}/exec/${instructionSeq}/result`)
}
async *execResultStream(
machineName: string,
instructionSeq: number,
): AsyncGenerator<ApiExecResultStreamResponse> {
yield* this.#getStream(`/v1/machine/${machineName}/exec/${instructionSeq}/stream-result`)
}
repl(machineName?: string): Repl {
return new Repl({
machine: machineName,
token: this.#token,
baseUrl: this.#baseUrl.replace(/^http/, 'ws'),
})
}
}
if (import.meta.vitest) {
const { test, expect } = import.meta.vitest
const FOREVERVM_API_BASE = process.env.FOREVERVM_API_BASE || ''
const FOREVERVM_TOKEN = process.env.FOREVERVM_TOKEN || ''
test('whoami', async () => {
const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
const whoami = await fvm.whoami()
expect(whoami.account).toBeDefined()
})
test('createMachine and listMachines', async () => {
const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
const machine = await fvm.createMachine()
expect(machine.machine_name).toBeDefined()
const machines = await fvm.listMachines()
const found = machines.machines.find(({ name }) => name === machine.machine_name)
expect(found).toBeDefined()
})
test('exec and execResult', async () => {
const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
const { machine_name } = await fvm.createMachine()
const { instruction_seq } = await fvm.exec('print(123) or 567', machine_name)
expect(instruction_seq).toBe(0)
const result = await fvm.execResult(machine_name, instruction_seq as number)
expect(result.result.value).toBe('567')
})
test('execResultStream', async () => {
const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
const { machine_name } = await fvm.createMachine()
const { instruction_seq } = await fvm.exec('for i in range(10): print(i)\n"done"', machine_name)
expect(instruction_seq).toBe(0)
let i = 0
for await (const item of fvm.execResultStream(machine_name, instruction_seq as number)) {
if (item.type === 'result') {
expect(item.result.value).toBe("'done'")
} else if (item.type === 'output') {
expect(item.chunk.stream).toBe('stdout')
expect(item.chunk.data).toBe(`${i}`)
expect(item.chunk.seq).toBe(i)
i += 1
}
}
})
test('execResultStream with image', { timeout: 10000 }, async () => {
const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
const { machine_name } = await fvm.createMachine()
const code = `import matplotlib.pyplot as plt
plt.plot([0, 1, 2], [0, 1, 2])
plt.title('Simple Plot')
plt.show()`
const { instruction_seq } = await fvm.exec(code, machine_name)
expect(instruction_seq).toBe(0)
for await (const _ of fvm.execResultStream(machine_name, instruction_seq as number)) {
}
// if we reach this point, it means all the stream chunks were valid JSON
})
test('createMachine with tags', async () => {
const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
// Create machine with tags
const taggedMachine = await fvm.createMachine({
tags: { env: 'test', purpose: 'sdk-test' },
})
expect(taggedMachine.machine_name).toBeDefined()
// List machines and verify tags
const machines = await fvm.listMachines()
const foundTagged = machines.machines.find(({ name }) => name === taggedMachine.machine_name)
expect(foundTagged).toBeDefined()
expect(foundTagged?.tags).toBeDefined()
expect(foundTagged?.tags?.env).toBe('test')
expect(foundTagged?.tags?.purpose).toBe('sdk-test')
})
test('listMachines with tag filter', async () => {
const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
// Create an untagged machine
const untaggedMachine = await fvm.createMachine()
expect(untaggedMachine.machine_name).toBeDefined()
// Create a uniquely tagged machine
const uniqueTag = `test-${Date.now()}`
const taggedMachine = await fvm.createMachine({
tags: { unique: uniqueTag },
})
expect(taggedMachine.machine_name).toBeDefined()
// List machines with the unique tag filter
const filteredMachines = await fvm.listMachines({
tags: { unique: uniqueTag },
})
// Verify only our machine with the unique tag is returned
expect(filteredMachines.machines.length).toBe(1)
expect(filteredMachines.machines[0].tags?.unique).toBe(uniqueTag)
})
test('createMachine with memory limit', async () => {
const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
// Create machine with memory limit
const machine = await fvm.createMachine({
memory_mb: 512,
})
expect(machine.machine_name).toBeDefined()
// Verify the machine was created (note: we can't directly verify the memory limit
// through the API as it doesn't return this value in machine details)
const machines = await fvm.listMachines()
const found = machines.machines.find(({ name }) => name === machine.machine_name)
expect(found).toBeDefined()
})
}
```
--------------------------------------------------------------------------------
/scripts/bump-versions.ts:
--------------------------------------------------------------------------------
```typescript
import * as fs from 'fs'
import * as path from 'path'
import * as TOML from '@iarna/toml'
import { Command } from 'commander'
import chalk from 'chalk'
import { execSync } from 'child_process'
class SemverVersion {
constructor(
public major: number,
public minor: number,
public patch: number,
) {}
static parse(version: string): SemverVersion {
const [major, minor, patch] = version.split('.')
return new SemverVersion(parseInt(major), parseInt(minor), parseInt(patch))
}
toString() {
return `${this.major}.${this.minor}.${this.patch}`
}
bump(type: 'major' | 'minor' | 'patch') {
switch (type) {
case 'major':
return new SemverVersion(this.major + 1, 0, 0)
case 'minor':
return new SemverVersion(this.major, this.minor + 1, 0)
case 'patch':
return new SemverVersion(this.major, this.minor, this.patch + 1)
default:
throw new Error(`Invalid version bump type: ${type}`)
}
}
equals(other?: SemverVersion | null) {
if (other === null || other === undefined) return false
return this.major === other.major && this.minor === other.minor && this.patch === other.patch
}
cmp(other: SemverVersion) {
if (this.major < other.major) return -1
if (this.major > other.major) return 1
if (this.minor < other.minor) return -1
if (this.minor > other.minor) return 1
if (this.patch < other.patch) return -1
if (this.patch > other.patch) return 1
return 0
}
}
interface Package {
repo: PackageRepo
path: string
name: string
currentVersion?: SemverVersion
publishedVersion?: SemverVersion | null
private: boolean
}
interface PackageRepo {
repoName: string
gatherPackages(): Package[]
getDeployedVersion(name: string): Promise<SemverVersion | null>
updateVersion(path: string, version: SemverVersion): void
}
class NpmPackageRepo implements PackageRepo {
repoName = 'npm'
gatherPackages(): Package[] {
const jsDir = path.join(import.meta.dirname, '..', 'javascript')
const packages: Package[] = []
const crawlDir = (dir: string) => {
const items = fs.readdirSync(dir)
for (const item of items) {
const fullPath = path.join(dir, item)
if (item === 'node_modules') {
continue
}
const stat = fs.statSync(fullPath)
if (stat.isDirectory()) {
crawlDir(fullPath)
} else if (item === 'package.json') {
const content = fs.readFileSync(fullPath, 'utf-8')
const pkg = JSON.parse(content)
packages.push({
repo: this,
path: fullPath,
name: pkg.name,
currentVersion: pkg.version && SemverVersion.parse(pkg.version),
private: pkg.private || false,
})
}
}
}
crawlDir(jsDir)
return packages
}
async getDeployedVersion(name: string): Promise<SemverVersion | null> {
try {
const response = await fetch(`https://registry.npmjs.org/${name}`)
if (!response.ok) {
if (response.status === 404) {
return null
}
throw new Error(`Failed to fetch package info: ${response.statusText}`)
}
const data = (await response.json()) as any
const version = data['dist-tags']?.latest
return version ? SemverVersion.parse(version) : null
} catch (error) {
console.error(`Error fetching version for ${name}:`, error)
return null
}
}
updateVersion(packageFilePath: string, version: SemverVersion): void {
const content = fs.readFileSync(packageFilePath, 'utf-8')
const pkg = JSON.parse(content)
pkg.version = version.toString()
fs.writeFileSync(packageFilePath, JSON.stringify(pkg, null, 2) + '\n')
// update lockfile by running `npm install`
execSync('npm install', { cwd: path.dirname(packageFilePath) })
}
}
class PythonPackageRepo implements PackageRepo {
repoName = 'python'
gatherPackages(): Package[] {
const pythonDir = path.join(import.meta.dirname, '..', 'python')
const packages: Package[] = []
const crawlDir = (dir: string) => {
const items = fs.readdirSync(dir)
for (const item of items) {
const fullPath = path.join(dir, item)
const stat = fs.statSync(fullPath)
if (stat.isDirectory()) {
crawlDir(fullPath)
} else if (item === 'pyproject.toml') {
const content = fs.readFileSync(fullPath, 'utf-8')
const parsed = TOML.parse(content)
const projectData = parsed.project as any
if (projectData?.name && projectData?.version) {
packages.push({
repo: this,
path: fullPath,
name: projectData.name,
currentVersion: projectData.version && SemverVersion.parse(projectData.version),
private: false, // Note: implement this if we need private packages
})
}
}
}
}
crawlDir(pythonDir)
return packages
}
async getDeployedVersion(name: string): Promise<SemverVersion | null> {
try {
const response = await fetch(`https://pypi.org/pypi/${name}/json`)
if (!response.ok) {
if (response.status === 404) {
return null
}
throw new Error(`Failed to fetch package info: ${response.statusText}`)
}
const data = (await response.json()) as any
const version = data.info?.version
return version ? SemverVersion.parse(version) : null
} catch (error) {
console.error(`Error fetching version for ${name}:`, error)
return null
}
}
updateVersion(path: string, version: SemverVersion): void {
const content = fs.readFileSync(path, 'utf-8')
const newContent = content.replace(/version\s*=\s*"[^"]*"/, `version = "${version}"`)
if (content === newContent) {
throw new Error(`Failed to update version in ${path}`)
}
fs.writeFileSync(path, newContent)
}
}
class CargoPackageRepo implements PackageRepo {
repoName = 'cargo'
gatherPackages(): Package[] {
const cargoDir = path.join(import.meta.dirname, '..', 'rust')
const packages: Package[] = []
const crawlDir = (dir: string) => {
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
if (entry.name === 'target') {
continue
}
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
crawlDir(fullPath)
} else if (entry.name === 'Cargo.toml') {
const content = fs.readFileSync(fullPath, 'utf-8')
const data = TOML.parse(content) as any
if (data.package && data.package.name && data.package.version) {
packages.push({
repo: this,
path: fullPath,
name: data.package.name,
currentVersion: data.package.version && SemverVersion.parse(data.package.version),
private: false, // Cargo packages are typically public
})
}
}
}
}
crawlDir(cargoDir)
return packages
}
async getDeployedVersion(name: string): Promise<SemverVersion | null> {
try {
const response = await fetch(`https://crates.io/api/v1/crates/${name}`, {
headers: {
// crates.io requires a non-default user agent.
'User-Agent': 'forevervm bump-versions script',
},
})
if (!response.ok) {
if (response.status === 404) {
return null
}
throw new Error(`Failed to fetch package info: ${response.statusText}`)
}
const data = (await response.json()) as any
const version = data.crate?.max_version
return version ? SemverVersion.parse(version) : null
} catch (error) {
console.error(`Error fetching version for ${name}:`, error)
return null
}
}
updateVersion(path: string, version: SemverVersion): void {
const content = fs.readFileSync(path, 'utf-8')
const newContent = content.replace(/version\s*=\s*"[^"]*"/, `version = "${version}"`)
if (content === newContent) {
throw new Error(`Failed to update version in ${path}`)
}
fs.writeFileSync(path, newContent)
}
}
function collectPackages(): Package[] {
const packages: Package[] = []
packages.push(...new NpmPackageRepo().gatherPackages())
packages.push(...new PythonPackageRepo().gatherPackages())
packages.push(...new CargoPackageRepo().gatherPackages())
return packages
}
async function getCurrentVersions(packages: Package[]): Promise<Package[]> {
// filter out private packages
const filteredPackages = packages.filter((pkg) => !pkg.private)
const publishedVersions = await Promise.all(
filteredPackages.map(async (pkg) => {
const publishedVersion = await pkg.repo.getDeployedVersion(pkg.name)
return { ...pkg, publishedVersion }
}),
)
return publishedVersions
}
async function main() {
const program = new Command()
program.name('bump-versions').description('CLI to manage package versions')
program
.command('info')
.description('List all packages and their versions')
.action(async () => {
const packages = await getCurrentVersions(collectPackages())
for (const pkg of packages) {
const same = pkg.currentVersion?.equals(pkg.publishedVersion)
if (same) {
console.log(
`${chalk.cyan(pkg.repo.repoName)} ${chalk.yellow(pkg.name)}: local ${chalk.green(pkg.currentVersion)} == published ${chalk.green(pkg.publishedVersion)}`,
)
} else {
console.log(
`${chalk.cyan(pkg.repo.repoName)} ${chalk.yellow(pkg.name)}: local ${chalk.green(pkg.currentVersion)} != published ${chalk.red(pkg.publishedVersion || 'N/A')}`,
)
}
}
})
program
.command('bump [type]')
.description('Bump all packages versions')
.argument('[type]', 'Type of version bump', 'patch')
.option('-d, --dry-run', 'Perform a dry run without making changes')
.action(async (type: 'major' | 'minor' | 'patch', _, options: { dryRun: boolean }) => {
if (type !== 'major' && type !== 'minor' && type !== 'patch') {
console.error('Invalid version type; must be one of "major", "minor", or "patch"')
process.exit(1)
}
const dryRun = options.dryRun
const packages = await getCurrentVersions(collectPackages())
const validVersions = packages
.map((pkg) => pkg.currentVersion)
.filter((v) => v !== null && v !== undefined)
const maxLocalVersion = validVersions.reduce((a, b) => (a.cmp(b) > 0 ? a : b))
const maxPublishedVersion = packages
.map((pkg) => pkg.publishedVersion)
.filter((v) => v !== null && v !== undefined)
.reduce((a, b) => (a!.cmp(b!) > 0 ? a : b))
const maxVersion =
maxLocalVersion!.cmp(maxPublishedVersion!) > 0 ? maxLocalVersion : maxPublishedVersion
const bumpedVersion = maxVersion!.bump(type)
const plan = []
for (const pkg of packages) {
if (pkg.currentVersion !== bumpedVersion) {
plan.push({
package: pkg,
from: pkg.currentVersion,
to: bumpedVersion,
})
}
}
console.log('Plan:')
for (const item of plan) {
console.log(
`${chalk.blue(item.package.repo.repoName)} ${chalk.cyan(item.package.name)}: ${chalk.red(item.from)} ${chalk.gray('->')} ${chalk.green(item.to)}`,
)
}
if (dryRun) {
console.log('Dry run; no changes will be made.')
} else {
for (const item of plan) {
console.log(
`Updating ${chalk.blue(item.package.repo.repoName)} ${chalk.cyan(item.package.name)}: ${chalk.red(item.from)} ${chalk.gray('->')} ${chalk.green(item.to)}`,
)
item.package.repo.updateVersion(item.package.path, item.to)
}
}
})
program.parse()
}
main()
```