| 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. |
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.
- One
.jsfile is the tool. No bundler, nopackage.json, nonode_modules. - ES modules only.
import/exportwork;require()does not. - Importing a
.gql/.graphqlfile gives you a(vars?) => { query, subscribe, unsubscribe }factory. - The host injects three globals:
yeet,console,tty,style, plus standard timers (setTimeout,setInterval,queueMicrotask). - Discover the graph from the shell, not from inside the script:
yeet graph dump # full SDL yeet graph query '{ host { uptime { uptime } } }'
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 |
These bite repeatedly. Internalize them.
yeet.graph.queryreturns the full GraphQL envelope. Destructure:const { data, errors } = await yeet.graph.query(...). Readingresult.foodirectly givesundefinedand looks like a schema break.yeet.graph.subscribereturns a ticket string, not an unsubscribe function. Capture it; pass it toyeet.graph.unsubscribe(ticket)to stop.tty.*throws without a PTY (everything excepttty.size()). Piping output (yeet run x.js | tee log) detaches the PTY. If a script needs to support both interactive and piped modes, probe at startup:let hasTty = true; try { tty.write(""); } catch { hasTty = false; }
style.fg(text, r, g, b)is quantized to the 16-colour ANSI palette despite the RGB-shaped signature. Smooth gradients collapse. Pick RGB values that land on the bright codes you want:(255, 50, 220)→ bright magenta(0, 240, 240)→ bright cyan(255, 220, 100)→ bright yellow(255, 40, 60)→ bright red(90, 90, 90)→ bright black (visible dark gray)
yeet.argsvalues are strings or booleans, never numbers. Coerce withNumber(yeet.args.interval) || 2000.- Kebab-case flag names become snake_case keys:
--multi-word-flag→yeet.args.multi_word_flag. - The isolate exits when the top-level module finishes and no timers / pending promises remain. Long-running scripts must keep an interval or subscription alive (or call
yeet.exit()on cleanup). - No
printf-style substitution inconsole.log— args are space-joined. Format yourself.
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
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 eventprogress— for finite-duration scripts (e.g.leak-hunt)report— terminal output of a finite runerror— caught errors, witherror: String(...)
Skeleton:
export function watch(opts, emit) {
const intervalMs = opts.intervalMs ?? 2000;
let stopped = false;
let ticket = null;
// For polling: setInterval + an immediate first tick.
// For streaming: yeet.graph.subscribe(...) and store the ticket.
return {
stop() {
stopped = true;
if (ticket) yeet.graph.unsubscribe(ticket);
},
};
}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 (notty.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.
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)));- Header docblock listing event kinds and their payload shapes. Every
data.jsinexamples/does this. It's the contract between data and render. unwrap = (resp) => resp.data ?? respas a small helper when bouncing betweenquery(envelope) andsubscribecallbacks (often already-unwrapped data).- Per-key prev-state
Mapfor 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
$varinterpolation in.graphqlmodules; 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/. Seeexamples/lib/vice.jsfor double-buffered Screen, Braille Canvas, palette constants — copy from there rather than reinventing flicker-free redraws.
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 ayeet.graph.subscribestreaming source instead of polling.examples/leak-hunt/— finite-duration script (windowed sample →reportevent → 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 everyyeet.*,tty.*,style.*, and timer global. Consult before writing any non-trivial usage.
yeet run path/to/script.js # interactive
yeet run path/to/script.js --once # one-shot, pipe-safe
yeet run path/to/dump.js | jq . # verify the data layer
yeet graph query '<gql>' # probe the graph from the shell
yeet graph dump # full SDLIf 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.