| name | yeet-scripts |
| description | Use when writing, editing, or debugging yeet scripts — JavaScript files run by the `yeet` daemon as real-time observability tools. Triggers include filenames under an `examples/<name>/{data,render,dump}.js` layout, any `.js` that calls `yeet.graph.query` / `yeet.graph.subscribe`, imports from `./data.js` with a `watch()` export, or invocations like `yeet run path/to/script.js`. Skip for ordinary Node/Deno/browser JS — the runtime is none of those. |
Writing yeet scripts
Yeet scripts are single JavaScript files run by the yeet daemon inside a V8 isolate. They subscribe to a GraphQL system graph (CPU, memory, processes, network, GPU, docker, hwmon) and render or act on it. The runtime is not Node, not Deno, not a browser — assume nothing standard works until you've checked.
Always read the full API reference at CLAUDE.md in the repo root before writing non-trivial code. This skill is the mental model and the gotchas; CLAUDE.md is the authoritative API surface.
Mental model
What the runtime does NOT have
If you reach for one of these, stop and use the alternative:
| Missing | Use instead |
|---|
process, process.env, process.argv | yeet.args (parsed minimist-style) |
require, CommonJS | import |
fetch, XMLHttpRequest, WebSocket | yeet.graph.query / yeet.graph.subscribe |
fs, path, os, child_process | nothing — scripts have no FS or subprocess access |
Buffer | Uint8Array / ArrayBuffer |
crypto, TextEncoder, TextDecoder, URL | not present |
performance.now() | Date.now() |
setImmediate | queueMicrotask(fn) or setTimeout(fn, 0) |
Intl.*, String.prototype.localeCompare | plain < / > comparisons; manual formatting (V8 built without ICU) |
requestAnimationFrame | setInterval |
High-frequency gotchas
These bite repeatedly. Internalize them.
The convention every example follows
Each example in examples/<name>/ is split into three files. Follow this layout when adding a new one — it's what makes the scripts easy to read, retarget, and modify.
examples/<name>/
data.js # pure data layer: queries the graph, normalizes, emits events
render.js # presentation: takes events from data.js and draws to the TTY
dump.js # one-line debugger: pipes data.js events as JSON to stdout
data.js — the data layer
Exports a single watch(opts, emit) function (and often a one-shot snapshot()). It owns all yeet.graph.* calls and emits a stream of small, tagged event objects. It must not touch tty or style — that's render.js's job. Keeping it pure means the same data layer works for render.js, dump.js, alerting scripts, and tests.
Event shape is { kind, t, ...payload }. Document the kinds in the file header. Standard kinds you'll see across the repo:
config — emitted once at startup with parameters the renderer needs (intervals, units, etc.)
snapshot / tick — the recurring data event
progress — for finite-duration scripts (e.g. leak-hunt)
report — terminal output of a finite run
error — caught errors, with error: String(...)
Skeleton:
export function watch(opts, emit) {
const intervalMs = opts.intervalMs ?? 2000;
let stopped = false;
let ticket = null;
return {
stop() {
stopped = true;
if (ticket) yeet.graph.unsubscribe(ticket);
},
};
}
render.js — the presentation layer
Imports watch from ./data.js. Owns tty.*, style.*, and any layout / ANSI logic. Two modes are conventional:
--once — one snapshot to stdout, exit. Pipe-safe (no tty.alt(), no cursor games, must work without a PTY).
- live (default) —
tty.alt() + tty.hideCursor(), redraw on each event, restore on exit.
Read CLI args with yeet.args. Coerce explicitly (Number(...) || default). Honor --interval, --width, etc. consistently with sibling scripts.
dump.js — the debugger
Six lines. Imports watch, runs it, console.log(JSON.stringify(ev)) per event. Lets you yeet run examples/x/dump.js | jq . to verify the data layer in isolation. Always include one — it's the fastest way to debug.
import { watch } from "./data.js";
const intervalMs = Number(yeet.args.interval) || 2000;
watch({ intervalMs }, (ev) => console.log(JSON.stringify(ev)));
Patterns worth copying
- Header docblock listing event kinds and their payload shapes. Every
data.js in examples/ does this. It's the contract between data and render.
unwrap = (resp) => resp.data ?? resp as a small helper when bouncing between query (envelope) and subscribe callbacks (often already-unwrapped data).
- Per-key prev-state
Map for rate calculation (bytes/sec, CPU%): keep last sample's totals + timestamp, compute delta on each tick, prune entries for keys that disappeared.
- Inline GraphQL variables only after sanitizing. The runtime supports
$var interpolation in .graphql modules; for ad-hoc inline queries, validate / strip the input (String(name).replace(/[^a-zA-Z0-9_.-]/g, "")) before string-concatenating it.
- Shared rendering primitives go in
examples/lib/. See examples/lib/vice.js for double-buffered Screen, Braille Canvas, palette constants — copy from there rather than reinventing flicker-free redraws.
Reference implementations
When in doubt, read these in order:
examples/docker-net/ — cleanest demonstration of the data/render/dump split with a polling data source. Start here.
examples/process-galaxy/ — same layout but with a yeet.graph.subscribe streaming source instead of polling.
examples/leak-hunt/ — finite-duration script (windowed sample → report event → exit).
examples/lib/vice.js + examples/vice-city-*.js — flicker-free double-buffered rendering, Braille sub-cell graphics, gauges. Crib from here for anything visually ambitious.
CLAUDE.md — full API reference for every yeet.*, tty.*, style.*, and timer global. Consult before writing any non-trivial usage.
Testing a script
yeet run path/to/script.js
yeet run path/to/script.js --once
yeet run path/to/dump.js | jq .
yeet graph query '<gql>'
yeet graph dump
If tty.* throws "No such device or address (os error 6)", the script lost its PTY — either run it interactively or wrap with script -q -c 'yeet run x.js' /dev/null.