A CLI for race data analysis — lap times, GPS telemetry, sector splits, track tooling — designed to be driven by AI agents as well as humans. Aimed at hobby karting and amateur motorsport.
Status: v0.1.0.
laps,session, andtrack(create+edit) work end-to-end against RaceBox / Racelogic VBOX files andkart-track/v1GeoJSON. Outputs are JSON-first so an agent can compose them.
Once published to npm:
npm install -g lapvisor
# or run on demand:
npx lapvisor --helpRequires Node.js 22+. End users do not need Bun.
Bun (>=1.2) is used for dev and tests; tsup produces the Node-runnable bundle that ships to npm.
bun install
bun run dev <subcommand> ... # run from source under Bun
bun run dev laps --help # subcommand help
bun test # run tests
bun run lint # biome
bun run build # bundle -> dist/index.js (Node ESM)
node dist/index.js --help # run the built artifact
bun link # expose `lapvisor` on PATH for local consumers| Command | Description |
|---|---|
laps <file> |
Parse a session, detect laps from gate crossings, emit a summary (count, best, mean, venue). |
session <file> |
Emit a complete render bundle (samples + laps + sectors + gates) as JSON for downstream UIs. |
lap <file> <index> |
Emit one lap as a lapvisor-lap/v1 bundle (rich telemetry + cumulative distance + sectors + per-lap aggregates) for distance-aligned cross-driver comparison. |
track create |
Build a kart-track/v1 GeoJSON file from a structured gate description (stdin or --input). |
track edit <file> |
Open a local browser editor for a kart-track/v1 file (drag gates, edit name/kind/bearing/width, atomic save). |
The samples/ directory ships a real RaceBox session at Plytinės Kartodromas (Vilnius) and the matching kart-track/v1 track file, so every command can be exercised end-to-end without other inputs.
SESSION="samples/RaceBox Track Sessionon 05-05-2026 19-35.vbo"
TRACK="samples/plytines.track.json"
# 1. Lap summary (works without a track file — uses the gates inside the VBO).
lapvisor laps "$SESSION"
# 2. Full render bundle with sector splits — pair with the track file for named sectors.
lapvisor session "$SESSION" --track "$TRACK" | jq '{laps: (.laps|length), gates: (.gates|length), sectors: .sectorSplits[0].splits|length}'
# expect: 9 laps, 12 gates, 10 sectors
# 3. Visual track editor — opens http://localhost:5174/ in your browser.
cp "$TRACK" /tmp/edit-me.track.json # work on a copy
lapvisor track edit /tmp/edit-me.track.json
# 4. Build a track from scratch — pipe an intent JSON through `track create`.
echo '{
"name": "Toy track",
"features": [
{"id":"sf","kind":"start_finish","name":"S/F","order":0,
"center":[25.349,54.725],"bearing_deg":90,"width_m":8,"unidirectional":false}
]
}' | lapvisor track createlapvisor laps session.vbo # human-readable summary
lapvisor laps session.vbo --json # JSON (also emitted when stdout is not a TTY){
"source": "session.vbo",
"format": "vbo",
"lapCount": 9,
"meta": { "venue": "Plytines", "startedAt": "2026-05-05T16:35:00.000Z" },
"bestMs": 44058,
"meanMs": 44517
}Emits a lapvisor-session/v1 bundle: GPS samples, detected laps, sector splits, and gate geometry. Always JSON. Designed to be consumed by external UIs (e.g. the karting repo's kart view shells out to this and renders Leaflet on top).
lapvisor session session.vbo --track gates.geojson | jq '.laps | length'Reads a structured gate description (stdin or --input), validates with zod, computes LineString endpoints from each gate's (center, bearing_deg, width_m) via Haversine destination, emits a complete kart-track/v1 FeatureCollection.
echo '{
"name": "My track",
"features": [
{"id":"sf","kind":"start_finish","name":"S/F","order":0,
"center":[25.349,54.725],"bearing_deg":90,"width_m":8,"unidirectional":false}
]
}' | lapvisor track create > my-track.jsonComposable: upstream tools that parse vendor track formats (e.g. RaceChrono .rcz) decode the source and pipe an intent here — no source-format parsing lives in lapvisor.
Opens a .track.json in a local browser editor — drag gate centres on a Leaflet map, edit name/kind/bearing/width/unidirectional in a side panel, add or delete traps, save back to the same file.
lapvisor track edit my-track.json # http://localhost:5174/
lapvisor track edit my-track.json --port 6000 --no-open
lapvisor track edit my-track.json --readOnly # disable the POST handlerSave: click Save or press Cmd/Ctrl-S. The server validates the schema tag, recomputes gate geometry from (center, bearing, width) (defence in depth), and atomically overwrites the file (tmp + rename).
| Format | Status | Notes |
|---|---|---|
.vbo (Racelogic VBOX / RaceBox) |
working | Parser: src/adapters/vbo.ts · Reference: docs/formats/vbo.md. |
.gpx, .fit, .tcx, lap-time CSV |
planned | — |
| Capability | Status | Notes |
|---|---|---|
| Lap detection from gate crossings | working | Sub-sample timestamp interpolation + direction lock + sats/velocity/min-lap filters. See docs/analysis/laps.md. |
| Sector splits | working | Per-lap offsets at each sector gate. Used by session. |
| Per-lap stats (top speed, peak G, …) | planned | — |
kart-track/v1 GeoJSON is owned by lapvisor — schema, builder (track create), and visual editor (track edit). Spec: docs/formats/kart-track-v1.md. Design notes (ADRs) live in the companion private karting repo under docs/decisions/.
The lower-level building blocks are available directly:
import { readFileSync } from "node:fs";
import { parseVbo } from "./src/adapters/vbo.js";
import { detectLaps } from "./src/analysis/laps.js";
import { buildKartTrack } from "./src/track/geojson.js";
const file = parseVbo(readFileSync("session.vbo", "utf8"), "session.vbo");
const { laps } = detectLaps(file.samples, file.gates);
for (const l of laps) {
console.log(`L${l.index}: ${(l.durationMs / 1000).toFixed(3)} s`);
}Concise reference notes that complement the code live under docs/:
MIT — see LICENSE.