spacesheep
Streams

Stream live data from a remote server into a space

A server somewhere (a lab box, a cloud VM, a build machine) sends a line of data twice a second. Every open copy of your space draws it about a tenth of a second later, and buttons in the space can send presses back to the server. The space is never republished. Scripts, sensors and webhooks can stream the same way. Try it below; the server is pretend and runs in this tab.

Try it: set an intensity and press Run testSimulated in your browser. Nothing here talks to a server.
1 · The remote servermy-box
starting…
$ npx spacesheep stream lab/my-box --system --load-test
  streaming to lab/my-box
2 · The streamlab/my-box
Keeps the last 10 min · carried 0 lines
New versions published: 0
3 · Your spaceopen in 3 tabs
load · 1 min–
cpu busy–
70%
Nothing running.

A real one: a Linux server on Google Cloud streaming into lab-1, live (sign in to open it).

What just happened

1
Something sends a line

Anything that can print text or make a request can stream: a server reading its own CPU, a script reading a sensor, a job reporting progress. Here the source is a pretend server that prints one line of JSON twice a second.

2
The stream carries it

Each line goes to a stream, a name like lab/my-box. The stream hands it to every open tab and keeps the last ten minutes for tabs that open later. Nothing is published.

3
The space draws it

The space names the stream once. Each new line reaches it about a tenth of a second after it was sent, and the space redraws at most once per screen refresh.

Buttons go the other way. The space sends a press, here run at your intensity, back through the stream. The source acts on it only if it knows that button, and it clamps the numbers itself, because anyone who can open the space can press. To keep a button for the people who can edit the space, add data-press="editors" to the page's ss-streams declaration: everyone else's press is refused, and the button can say why.

Start streaming

From a server or a computer

Any Linux or Mac with Node 20+. Two commands: one gives it a key, one starts the stream.

# on a laptop signed in to spacesheep: a key that can only send numbers, never shown
npx -y spacesheep@latest keys create --scope stream --name my-box | ssh my-box 'npx -y spacesheep@latest keys save'

# on the box: its own numbers, Run test buttons, kept running in the background
npx -y spacesheep@latest stream lab/my-box --system --load-test --service

--system reads the computer itself (per-core load, memory, temperatures). --load-test adds the buttons you just pressed, with nothing to install. --service keeps it running after you log out. A cloud server has no thermometer, so its temperatures are a model of its load, and the line says so.

Or ask your agentClaude Code, or any agent with the spacesheep plugin
Stream my server MY-BOX into a spacesheep space and make me a space that shows it live.
From this computer (signed in to spacesheep), run:
  npx -y spacesheep@latest keys create --scope stream --name MY-BOX | ssh MY-BOX 'npx -y spacesheep@latest keys save'
  ssh MY-BOX 'npx -y spacesheep@latest stream lab/MY-BOX --system --load-test --service'
Then publish a space that declares the stream lab/MY-BOX and draws its per-core load,
load average and temperatures, with an intensity slider and a Run test button.

From your own server code

No CLI needed: a server can talk to the stream over HTTP. Send a reading with a POST, hear the space's buttons with a long poll, and answer each press. Here is a whole server, in two languages:

// server.mjs: stream this server's load into a space, and answer its Run test button
import os from "node:os";

const BASE = "https://spacesheep.dev/api/streams/lab/my-box";
const HEAD = { Authorization: "Bearer " + process.env.SPACESHEEP_KEY, "Content-Type": "application/json" };

// 1. Send a reading twice a second. Each POST is one line on the stream.
setInterval(() => {
  const value = { load: os.loadavg(), mem_free_mb: Math.round(os.freemem() / 1048576) };
  fetch(BASE, { method: "POST", headers: HEAD, body: JSON.stringify(value) }).catch(() => {});
}, 500);

// 2. Hear the space's buttons (a long poll), and answer every press.
const get = (url) => fetch(url, { headers: HEAD }).then((r) => r.json());
let { cursor } = await get(BASE + "/-/events?wait=0");   // start now: ignore old presses
while (true) {
  const r = await get(BASE + "/-/events?after=" + cursor + "&wait=25000");
  for (const ev of r.events) {
    const known = ev.name === "run";                          // only buttons you handle
    if (known) startLoad(Math.min(100, Math.max(5, Number(ev.data?.intensity) || 50)));  // clamp: anyone can press
    await fetch(BASE + "/-/ack", { method: "POST", headers: HEAD,
      body: JSON.stringify({ seq: ev.seq, status: known ? "started" : "refused" }) });
  }
  cursor = r.cursor;
}

startLoad is your own code: whatever a press should do on your server. Run it with a streams-only key: SPACESHEEP_KEY=ss_… node server.mjs. The space shows each reading about a tenth of a second after the POST, and a press answered with started comes back to the button that sent it.

From your own program

Drop --system and give the CLI any program that prints one JSON line per reading:

#!/usr/bin/env python3
# read-sensors.py: each line it prints becomes one value on the stream
import json, time
while True:
    temps = [read_chip_temp(i) for i in range(4)]   # your sensor code here
    print(json.dumps({"host": "chip-1", "temp_c": temps, "temp_source": "sensor"}), flush=True)
    time.sleep(0.5)
npx -y spacesheep@latest stream lab/chip-1 --run "python3 read-sensors.py"

From anywhere that makes a request

A webhook, a cloud function, a phone shortcut: one POST is one value. Use a streams-only key (npx spacesheep keys create --scope stream).

curl -X POST https://spacesheep.dev/api/streams/lab/orders \
  -H "Authorization: Bearer $SPACESHEEP_KEY" -H "Content-Type: application/json" \
  -d '{"orders_per_min": 42}'

Show a stream in a space

Declare the stream in the head of the space's page. The declaration is also the access rule: a space can read only the streams it names, and only the people who can open the space see them.

<meta name="ss-streams" content="lab/my-box">

<script>
  const s = ss.stream("lab/my-box");
  s.draw((last, history, status) => {
    // once per screen refresh, only when a new line arrived:
    // last = the newest line, history = the last 600, status.lag_ms = source → screen
  });
  s.send("run", { intensity: 60, seconds: 30 }).then((r) => console.log(r.status)); // "started"
</script>

Buttons only editors can press

Anyone who can open the space can press its buttons. For a button that costs something, like a load test or a restart, keep it for the people who can edit the space: the owner, and whoever the Share panel lists as Can edit. Add data-press="editors" to the declaration. Everyone else's press is refused before it reaches the source, and status.can_press tells the page, so the button can be greyed out with a reason instead of failing on the click.

<meta name="ss-streams" content="lab/my-box" data-press="editors">

<script>
  s.draw((last, history, status) => {
    run.disabled = status.can_press === false;   // null until the server answers
    run.title = run.disabled ? "View only. Ask the owner for edit access." : "";
  });
</script>

A data-ss-send button is greyed out for readers with no script at all. People who can edit the space can also change this rule, so give Can edit only to people you'd let press.

Agents already know this: the spacesheep skill teaches it, and the MCP tool streams shows them a stream's latest lines so they can see its exact shape before drawing it.

When it isn't streaming

Your streams page shows where it stops: whether each source is sending (its last hour, minute by minute, with any gap and any value that was refused and why), who has a space open that reads it, and whether their copy drew what it got. A gap is the source; a page that got values and drew none is the page's own code, and the error is shown.

What people stream

A load test you can watchA lab box's cores, memory and heat, with a Run test button and an intensity slider.
A chip's heat cameraA thermal grid per reading, painted on the die as compute moves around the chip.
A build or a training runProgress, loss and throughput from wherever the work runs, in the space the team already reads.
Sensors in a roomTemperature, power, CO₂: anything a script can read and print.
A service, liveOrders, signups or errors per minute, posted by a webhook or a cron job.

Good to know

PlanSending into a stream is part of Max. Anyone who can open the space can watch, on any plan or none.
Creating a streamThere's no create step. The first value makes it.
Who can see itOnly spaces that declare it, and only people who can open those spaces.
The sender's keyStreams-only: it can send to your streams and hear their buttons, nothing else. No deploys, no reads.
ButtonsThe CLI runs only the buttons it was given, never a press from before it started; a source clamps every number it's sent.
Who can pressAnyone who can open the space, or with data-press="editors" on the declaration, only the people who can edit it.
Limits16 KB per value, 20 values a second per stream, the last 600 values or 10 minutes kept, 200 streams per account.
SpeedSource to screen measured at 74 ms on 2026-10-02, a Google Cloud server in Iowa to a browser in California.
Health/me/streams: every stream's last hour, the tabs watching, and whether their pages drew it.