@baublet/mote
v0.0.4
Published
Instrument, automate, track, and checkpoint personal dev environment VMs on GCP
Readme
mote
Instrument, automate, track, and checkpoint personal dev environments on GCP.
Size slugs. GPU shorthands. Layered config. Minimal dependencies.
Vision
VMs are a better development experience than containers: full OS, real systemd, easier virtualization, no layered filesystem quirks. Mote gives you a simple interface for steering those VMs in GCP, with an API tailored for LLM use that's also easy for humans.
The end goal: point an AI agent at a new repo's LLM.txt, and it uses Mote to create, configure, snapshot, branch, clone, and tear down dev environments on your behalf. You get reproducible, GPU-capable workspaces you can spin up, fork, and destroy without thinking about infrastructure.
Install
npm install -g mote # or: git clone … && npm linkRequires: Node ≥ 20, gcloud CLI authed.
Setup
# point at your project (visible everywhere, persisted)
mote config set project my-gcp-project
# optionally change zone
mote config set zone us-west1-b
# see full config (shows source of each value)
mote config getConfiguration
──────────────────────────────────────────────────
project my-gcp-project config
zone us-west1-b config
machine medium default
disk 100 default
diskType pd-balanced default
image ubuntu-2404-lts-amd64 default
imageProject ubuntu-os-cloud default
tag mote-vm default
exposeMethod tunnel default
autoIdleTimeout 180 default
showTips true defaultConfig layers: code defaults → ~/.mote/config.json → CLI flags.
Every default is overridable at every level.
Quick start
mote create my-app # medium (4 vCPU, 16 GB)
mote run my-app echo hello # run a command
mote ssh my-app # interactive shell
mote save my-app "clean install" # checkpoint
mote restore my-app "clean" # fuzzy match
mote stop my-app # storage-only billing
mote start my-app # resume
mote rm my-app # destroyAccessing your mote
SSH
mote ssh my-app # interactive shell
mote ssh my-app # aliases: mote console, mote cRunning commands
mote run my-app uname -a # run a single command
mote run my-app -- ls -la /etc # use -- for flags
mote run my-app --sudo apt update # run as rootFile transfer
mote push my-app ./local-file.txt # upload to ~ on the mote
mote push my-app ./src ~/project/src # upload to specific path
mote pull my-app ~/data/results.csv # download to current directory
mote pull my-app ~/data ./local-data # download to specific pathScripts
mote script my-app ./setup.sh # upload and run a local script
mote script my-app ./setup.sh --sudo # run as root
mote install my-app docker # run a bundled install script
mote install --list # see available scriptsGit + SSH keys
Copy your host's ~/.gitconfig and SSH keys to the mote so you can git clone private repos immediately:
mote install my-app gitThis copies ~/.gitconfig and ~/.ssh/id_* from your machine to the mote, fixes permissions, and adds github.com/gitlab.com/bitbucket.org to known_hosts.
Note: This copies your private SSH keys to the VM. For personal dev environments this is fine. If you'd prefer not to copy keys, use SSH agent forwarding instead:
gcloud compute ssh mote-my-app --zone us-central1-a -- -AAgent forwarding lets the VM use your local keys without copying them, but only while you're connected.
Port forwarding
To access a service running on your mote (e.g. a web server on port 8080), use mote expose:
mote expose my-app 8080 # → public HTTPS URL via Cloudflare tunnel
mote expose my-app 3000,8080 # multiple ports
mote info my-app # see tunnel URLs
mote unexpose my-app # close allThis creates a Cloudflare Quick Tunnel — automatic HTTPS, no firewall rules, no raw IP exposure. cloudflared is installed on the mote automatically on first use. Tunnel URLs are ephemeral and change on restart.
For a stable IP-based endpoint (opens GCP firewall rules with 0.0.0.0/0 ingress):
mote expose my-app 8080 --method firewall
mote unexpose my-app --method firewallSet the default method permanently with mote config set exposeMethod firewall.
You can also use standard gcloud SSH port forwarding for local-only access without exposing anything publicly:
gcloud compute ssh mote-my-app --zone us-central1-a -- -L 8080:localhost:8080
# now localhost:8080 on your machine → port 8080 on the moteSize slugs
Instead of remembering e2-standard-4, use a slug:
mote create tiny --machine xs # e2-small (2 vCPU, 2 GB)
mote create app --machine small # e2-standard-2 (2 vCPU, 8 GB)
mote create app --machine medium # e2-standard-4 (4 vCPU, 16 GB) ← default
mote create app --machine large # e2-standard-8 (8 vCPU, 32 GB)
mote create app --machine xl # e2-standard-16 (16 vCPU, 64 GB)
mote create app --machine 2xl # e2-standard-32 (32 vCPU, 128 GB)Or pass any GCP machine type directly:
mote create app --machine n2-standard-8 # validated against zone catalogSet your preferred default:
mote config set machine large # all future motes are 8 vCPUGPU slugs
mote create ml --gpu t4 # Tesla T4 → auto n1-standard-8
mote create ml --gpu l4 # L4 → auto g2-standard-8
mote create ml --gpu a100 # A100 → auto a2-highgpu-1g
mote create ml --gpu t4 --gpu-count 4 # 4× T4
mote create ml --gpu t4 --machine large # override machine for GPUSee all slugs: mote catalog sizes
Cloning
Clone a mote to get an identical copy — same disk contents, machine type, GPU config, and schedule settings:
mote clone my-app my-app-v2 # full clone (disk + settings + schedule)
mote clone my-app experiment --stopped # clone but don't start it
mote clone my-app my-app-v2 --from "baseline" # clone from a specific save
mote clone my-app my-app-v2 --no-copy-schedule # clone without schedule/auto-idle settingsBy default, clone copies the source mote's scheduled start/stop times and auto-idle timeout. Use --no-copy-schedule to skip that.
Catalog
Machine types and GPUs are fetched from GCP and cached (24h):
mote catalog machines # what's in your zone
mote catalog gpus # GPUs in your zone
mote catalog sizes # all slug → type mappings
mote catalog refresh # force refresh cache
mote catalog machines --zone europe-west1-b # different zoneIf offline or no project configured, falls back to hardcoded catalog.
Config
mote config get # show all (with source labels)
mote config get project # show one key
mote config set project my-proj # set
mote config set zone us-east1-b # change zone
mote config set machine large # change default size
mote config set disk 100 # bigger default disk
mote config unset machine # revert to code default
mote config path # ~/.mote/config.jsonOverride anything per-command:
mote create my-app --project other-proj --zone asia-east1-a --machine xlPriority: CLI flag > config.json > code default > gcloud default.
Workspace isolation
By default mote stores state in ~/.mote/. Use --state to isolate per-project:
mote --state ./my-project/.mote/state.json create dev
mote --state ./my-project/.mote/state.json lsEach workspace gets its own state, config, and scheduler cron entry.
Scheduling
Motes cost money while running. The scheduler saves you money by automatically stopping idle VMs and optionally starting/stopping them on a time-based schedule.
How it works
The scheduler is a lightweight cron job that runs on your host machine (laptop, desktop, whatever you run mote from). When you run mote scheduler install, it adds a single line to your crontab that calls mote scheduler tick once per minute. Each tick:
- Loops through your running motes
- SSHes in and checks for active users (via the
wcommand) - Stops any mote that's been idle longer than the timeout
- Starts/stops motes based on their time schedules (if configured)
The tick command is fast and quiet — it reads ~/.mote/state.json, makes a few quick SSH calls, and exits. Output goes to ~/.mote/scheduler.log.
Important: Your host machine must stay on for this to work. If your laptop sleeps or shuts down, the cron job won't run and motes won't be auto-managed.
Installing the scheduler
mote scheduler installThis does one thing: adds a crontab entry like:
* * * * * /path/to/mote scheduler tick >> ~/.mote/scheduler.log 2>&1No daemons, no background processes, no system services — just a standard cron job. You can verify it with crontab -l and remove it anytime with mote scheduler uninstall.
mote scheduler status # check if installed, when it last ran, mote counts
mote scheduler uninstall # remove the cron jobAuto-idle (default: ON, 3 hours)
Once the scheduler is installed, it checks each running mote for activity every minute. "Activity" means someone is logged in via SSH — the scheduler runs w -hs | wc -l via SSH and only considers a mote active when more than one session is present (the check's own SSH session counts as one).
- If users are connected: records the mote as active
- If nobody is connected and the mote has been idle longer than the timeout: warm-stops it
The default timeout is 3 hours. Change it globally:
mote config set autoIdleTimeout 180 # 3 hours (default)
mote config set autoIdleTimeout 60 # 1 hour
mote config set autoIdleTimeout 0 # disable auto-idle globallyOr override per-mote:
mote schedule my-app --auto-idle 120 # 2h timeout for this mote only
mote schedule my-app --no-auto-idle # never auto-stop this moteScheduled stop/start (opt-in)
You can also set time-based schedules to stop and start motes automatically:
mote schedule my-app --stop-at 20:00 --start-at 08:00 # daily
mote schedule my-app --stop-at 20:00 --start-at 08:00 --days mon,tue,wed,thu,fri # weekdays only
mote schedule my-app --stop-at 20:00 --start-at 08:00 --tz America/New_York # specific timezone
mote schedule my-app --clear # remove schedule
mote schedule my-app # view current settingsTesting the scheduler
You can run a tick manually and override the idle timeout for quick testing:
mote scheduler tick # run one cycle now
mote scheduler tick --auto-idle-timeout 1 # use 1-minute timeout (for testing)All commands
CONFIG
mote config get [key] Show config
mote config set <key> <val> Set a value
mote config unset <key> Revert to default
mote config path Config file location
CATALOG
mote catalog machines Machine types in zone
mote catalog gpus GPU types in zone
mote catalog sizes All slug mappings
mote catalog refresh Force refresh cache
MOTES
mote create <name> Create (--machine, --gpu, --expose, etc.)
mote ls List (grouped by project)
mote run <name> <cmd...> Run a command
mote ssh <name> Interactive shell (aliases: console, c)
mote save <name> [label] Checkpoint
mote saves <name> List checkpoints
mote restore <name> [save] Restore (id, label, or latest)
mote stop <name> Stop (storage-only)
mote start <name> Start
mote expose <name> <ports> Expose ports (HTTPS tunnel by default)
mote unexpose <name> [ports] Close exposed ports
mote push <name> <src> [dst] Upload files
mote pull <name> <src> [dst] Download files
mote clone <src> <name> Clone a mote (--no-copy-schedule to skip)
mote script <name> <path> Upload + run script
mote install [name] [software] Install bundled script (docker, node, vscode, git)
mote install --list List available install scripts
mote info <name> Details
mote resize <name> Change size (--machine, --gpu, --gpu-count, --disk)
mote cost [name] Cost summary (all) or detailed breakdown (one)
mote rm <name> Destroy
mote nuke Destroy everything
MAINTENANCE
mote gc Reconcile state against GCP
mote cron Cold-store warm-stopped motes
mote skill Install Claude Code skill into cwd
SCHEDULER
mote scheduler install Install cron job
mote scheduler uninstall Remove cron job
mote scheduler status Show scheduler status
mote scheduler tick Run one scheduler cycle
SCHEDULE
mote schedule <name> View/set per-mote scheduleCost
| State | Pay for |
|---|---|
| Running | vCPUs + RAM + GPU + disk + IP |
| Warm-stopped (mote stop) | Disk only (~$10/mo for 100 GB pd-balanced) |
| Cold-stored (mote stop --freeze) | Snapshot only (~$2.60/mo for 100 GB) |
| Saves | Snapshots (~$0.026/GB/mo, incremental) |
| Removed | $0 |
Cost command
mote cost # summary table of all motes
mote cost my-app # detailed breakdown for one motemote cost (no name) shows a table with current cost and projected monthly for every mote.
mote cost <name> shows a per-component rate card (compute, GPU, disk, IP), warm-stopped/cold-stored costs, and — if a schedule is set — a projected blended monthly cost with savings vs 24/7 running.
Disk resize
mote resize my-app --disk 200 # grow disk to 200 GB (GCP only allows growing)
mote resize my-app --disk 200 --machine large # resize disk and machine togetherDisk resize works in any state. For running/warm-stopped motes, the disk is resized live via gcloud compute disks resize. For cold-stored motes, the new size is recorded and applied on next mote start.
Testing
Prerequisites
- Node.js ≥ 20
- gcloud CLI installed and authenticated (mote checks this on every command):
# install: https://cloud.google.com/sdk/docs/install
gcloud auth login
gcloud config set project <YOUR_PROJECT_ID>- Install dependencies:
npm installTypecheck
npm run typecheck # runs tsc over all JS with JSDoc typesManual test sequence
Run these in order. Each step builds on the last.
# 1. Preflight — verify gcloud detection works
node bin/mote.js --help # should print help (no gcloud check)
node bin/mote.js ls # should pass preflight, print "No motes yet"
# 2. Config
node bin/mote.js config get # show resolved config with source labels
node bin/mote.js config path # print ~/.mote/config.json path
# 3. Catalog (hits GCP API, verifies auth + project)
node bin/mote.js catalog sizes # list slug mappings (no API call)
node bin/mote.js catalog machines # fetch machine types for default zone
node bin/mote.js catalog gpus # fetch GPU types for default zone
# 4. Create a mote
node bin/mote.js create test-mote # default medium (e2-standard-4, 100 GB)
node bin/mote.js ls # should show test-mote RUNNING with IP
node bin/mote.js info test-mote # full details
# 5. Run commands on it
node bin/mote.js run test-mote uname -a
node bin/mote.js run test-mote whoami
# 5b. Install scripts
node bin/mote.js install --list # should list docker, git, node, vscode
node bin/mote.js install test-mote git # copies SSH keys + git config, sets up known_hosts
# 6. Save + restore
node bin/mote.js save test-mote "baseline"
node bin/mote.js saves test-mote # should show the save
node bin/mote.js restore test-mote "baseline"
# 7. Stop / start cycle
node bin/mote.js stop test-mote
node bin/mote.js ls # status should be WARM_STOPPED
node bin/mote.js start test-mote
node bin/mote.js ls # status should be RUNNING
# 8. Cleanup
node bin/mote.js rm test-mote -y # destroy VM + disk + saves + firewall rules
node bin/mote.js ls # should be empty againOptional: GPU test
Requires GPU quota in your project/zone:
node bin/mote.js create gpu-test --gpu t4
node bin/mote.js run gpu-test nvidia-smi
node bin/mote.js rm gpu-test -yOptional: Clone + expose test
node bin/mote.js create src-mote
node bin/mote.js clone src-mote dst-mote
node bin/mote.js expose dst-mote 8080 # tunnel (default)
node bin/mote.js expose dst-mote 443 --method firewall # firewall
node bin/mote.js info dst-mote # shows both
node bin/mote.js unexpose dst-mote
node bin/mote.js rm src-mote -y
node bin/mote.js rm dst-mote -yArchitecture
mote/
bin/mote.js CLI entry + command routing (commander)
src/
commands.js command handlers
config.js layered config (code → user → CLI)
db.js JSON state store (atomic writes)
gcloud.js thin gcloud CLI wrapper (sync, async, interactive)
catalog.js GCP machine/GPU catalog + 24h cache
slugs.js size/GPU slug resolution
pricing.js cost estimation (hourly + monthly)
scheduler.js cron-based auto-idle + scheduled start/stop
reconcile.js drift detection (state ↔ GCP)
tips.js contextual tips
warnLongOp.js duration warnings for long operations
scripts/
<name>/vm.sh VM-side install script (runs with sudo on the mote)
<name>/host.sh host-side script (runs locally before vm.sh)Install scripts live in scripts/<name>/. Each script can have a vm.sh (uploaded and run on the mote with sudo), a host.sh (run locally on your machine first), or both. Host scripts receive MOTE_INSTANCE, MOTE_ZONE, and MOTE_PROJECT as environment variables for gcloud calls.
Three bundled dependencies (commander, cli-table3, write-file-atomic). No native modules. Source runs directly (node bin/mote.js); esbuild bundles to dist/mote.js for distribution.
State: ~/.mote/state.json
Config: ~/.mote/config.json
Cache: ~/.mote/cache/<zone>-machines.json, <zone>-gpus.json
