#
tokens: 47290/50000 84/86 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
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 | [![GitHub Repo stars](https://img.shields.io/github/stars/jamsocket/forevervm?style=social)](https://github.com/jamsocket/forevervm)
 5 | [![Chat on Discord](https://img.shields.io/discord/939641163265232947?color=404eed&label=discord)](https://discord.gg/N5sEpsuhh9)
 6 | 
 7 | | repo                                                | version                     |
 8 | |-----------------------------------------------------|-----------------------------|
 9 | | [cli](https://github.com/jamsocket/forevervm) | [![pypi](https://img.shields.io/pypi/v/forevervm)](https://pypi.org/project/forevervm/) |
10 | | [sdk](https://github.com/jamsocket/forevervm) | [![pypi](https://img.shields.io/pypi/v/forevervm-sdk)](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 | [![GitHub Repo stars](https://img.shields.io/github/stars/jamsocket/forevervm?style=social)](https://github.com/jamsocket/forevervm)
  5 | [![Chat on Discord](https://img.shields.io/discord/939641163265232947?color=404eed&label=discord)](https://discord.gg/N5sEpsuhh9)
  6 | 
  7 | | repo                                                | version                     |
  8 | |-----------------------------------------------------|------------------------------|
  9 | | [cli](https://github.com/jamsocket/forevervm) | [![npm](https://img.shields.io/npm/v/forevervm)](https://www.npmjs.com/package/forevervm) |
 10 | | [sdk](https://github.com/jamsocket/forevervm) | [![npm](https://img.shields.io/npm/v/@forevervm/sdk)](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 | 
```
Page 1/2FirstPrevNextLast