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