WebSocket API & CLI
NeuroSkill™ exposes a local JSON WebSocket server on every launch. All 60+ EEG metrics, device events, and control commands are available over a single persistent connection. No authentication required — the server binds to 0.0.0.0 (all interfaces) so LAN clients can connect.
A companion CLI (cli.ts) provides instant command-line access with auto-discovery, session-aware defaults, colorized output, and --json mode for piping to jq.
Connection
mDNS Discovery
NeuroSkill™ registers a Bonjour / Avahi service on startup. The service type is _skill._tcp.local. with TXT records version=1 and format=json. Discover the port automatically:
dns-sd -B _skill._tcp macOSavahi-browse _skill._tcp LinuxDirect Connection
The port is OS-assigned (random). If mDNS is unavailable, use lsof to find it:
pgrep -if skill | xargs -I{} lsof -iTCP -sTCP:LISTEN -nP -p {}Then connect:
ws://<host>:<port>Notes
- Multiple clients can connect simultaneously
- Broadcast capacity: 512 messages — slow consumers will lag
- The server binds to
0.0.0.0— accessible from LAN, not just localhost
Wire Format
Outbound (server → client)
Broadcast events are UTF-8 JSON with an "event" field:
{ "event": "eeg-bands", "payload": { ... } }Inbound (client → server)
Send commands as JSON with a "command" field:
{ "command": "status" }Responses
Command responses echo the command name and include an "ok" boolean:
Success
{ "command": "status", "ok": true, ... }Error
{ "command": "label", "ok": false,
"error": "missing required field: \"text\"" }Not Broadcast
Raw EEG samples (256 Hz), PPG (64 Hz), IMU (50 Hz), and spectrogram slices are not broadcast over the WebSocket — their high frequency would overwhelm the connection. Those streams are only available via the internal Tauri IPC event bus for the native UI.
Events server → client
eeg-bands ~4 HzFired every epoch (~250 ms). The main data stream — contains 60+ computed metrics: per-channel absolute and relative band powers (δ θ α β γ γ+), cross-band ratios (TBR, TAR, BAR, DTR, FAA), spectral shape (PSE, APF, BPS, SNR, SEF95, spectral centroid), complexity (Hjorth activity/mobility/complexity, permutation entropy, Higuchi FD, DFA exponent, sample entropy, PAC θ–γ), composite scores (focus, relaxation, engagement, mood, meditation, cognitive load, drowsiness), laterality index, coherence, mu suppression, headache & migraine research correlates, consciousness metrics (LZC, wakefulness, integration), PPG (HR, RMSSD, SDNN, pNN50, LF/HF, respiratory rate, SpO₂, perfusion index, Baevsky stress index), IMU (head pitch, roll, stillness, nod/shake counts), and artifact events (blink count/rate, jaw clench count/rate).
{
"event": "eeg-bands",
"payload": {
"timestamp": 1708800000.25,
"channels": [
{
"channel": "TP9",
"delta": 12.3, "theta": 8.1, "alpha": 15.2,
"beta": 6.7, "gamma": 2.1, "high_gamma": 0.4,
"rel_delta": 0.274, "rel_theta": 0.180, "rel_alpha": 0.338,
"rel_beta": 0.149, "rel_gamma": 0.047, "rel_high_gamma": 0.009,
"dominant": "alpha", "dominant_symbol": "α", "dominant_color": "#22c55e"
},
{ "channel": "AF7", "..." : "..." },
{ "channel": "AF8", "..." : "..." },
{ "channel": "TP10", "..." : "..." }
],
"faa": -0.08,
"tar": 0.53, "bar": 0.82, "dtr": 1.52, "tbr": 1.21,
"pse": 0.72, "apf": 10.2, "bps": -1.45, "snr": 18.3,
"sef95": 28.4, "spectral_centroid": 12.1,
"coherence": 0.65, "mu_suppression": 0.89, "mood": 62.0,
"laterality_index": -0.03, "pac_theta_gamma": 0.12,
"hjorth_activity": 245.1, "hjorth_mobility": 0.31, "hjorth_complexity": 1.42,
"permutation_entropy": 0.91, "higuchi_fd": 1.42,
"dfa_exponent": 0.85, "sample_entropy": 1.63,
"focus_score": 55.0, "relaxation_score": 42.0, "engagement_score": 68.0,
"hr": 72.1, "rmssd": 42.1, "sdnn": 58.3, "pnn50": 18.2,
"lf_hf_ratio": 1.8, "respiratory_rate": 14.5,
"spo2_estimate": 97.8, "perfusion_index": 2.1, "stress_index": 120.5,
"blink_count": 34, "blink_rate": 12.3,
"jaw_clench_count": 2, "jaw_clench_rate": 0.7,
"head_pitch": -5.2, "head_roll": 1.3, "stillness": 95.0,
"nod_count": 0, "shake_count": 0,
"meditation": 52.0, "cognitive_load": 38.0, "drowsiness": 12.0,
"headache_index": 18.3, "migraine_index": 11.7,
"consciousness_lzc": 62.4, "consciousness_wakefulness": 71.0, "consciousness_integration": 45.8
}
}muse-status ~1 HzDevice heartbeat. Connection state, battery %, sample counts, per-channel signal quality, firmware/hardware version, serial number, MAC address, headset preset, retry state, and raw sensor readings (accelerometer, gyroscope, fuel gauge, temperature).
{
"event": "muse-status",
"payload": {
"state": "connected",
"connected": true,
"streaming": true,
"name": "Muse-A1B2",
"id": "00:55:DA:B7:...",
"serial_number": "MUSE2-A1B2C3D4",
"mac_address": "00:55:DA:B7:...",
"firmware_version": "1.4.2",
"hardware_version": "6",
"bootloader_version": "2.1",
"preset": "p21",
"battery": 85,
"sample_count": 128000,
"ppg_sample_count": 32000,
"channel_quality": { "TP9": 0.95, "AF7": 0.88, "AF8": 0.91, "TP10": 0.97 },
"retry_attempt": 0,
"retry_countdown_secs": null,
"accel": [0.01, -9.81, 0.03],
"gyro": [0.1, -0.2, 0.05],
"fuel_gauge_mv": 3850,
"temperature_raw": 2950
}
}label-created on eventEmitted when any client creates a label via the Label window or the label command. Broadcast to all connected clients so dashboards can show markers on the EEG chart.
{
"event": "label-created",
"payload": {
"text": "eyes closed meditation",
"label_id": 42
}
}Commands client → server
statusReturns a comprehensive snapshot: device state (connection, battery, firmware, serial, MAC, preset, sample counts, retry state, raw sensors), current session (start time, duration), embedding counts (today, total, recording days, encoder loaded, overlap config), label count, last calibration timestamp, per-channel signal quality, 48-hour sleep staging summary, latest epoch scores (all 60+ metrics), and recording history (total sessions/days, streak, today vs 7-day averages).
{ "command": "status" }{
"command": "status", "ok": true,
"device": {
"state": "connected", "connected": true, "streaming": true,
"name": "Muse-A1B2", "battery": 85, "sample_count": 128000,
"firmware_version": "1.4.2", "serial_number": "MUSE2-...",
"mac_address": "00:55:DA:B7:...", "preset": "p21"
},
"session": { "start_utc": 1708790000, "duration_secs": 2847 },
"embeddings": {
"today": 342, "total": 14820, "recording_days": 31,
"encoder_loaded": true, "overlap_secs": 2.5
},
"labels": { "total": 12 },
"calibration": { "last_calibration_utc": 1708750000 },
"signal_quality": { "TP9": 0.95, "AF7": 0.88, "AF8": 0.91, "TP10": 0.97 },
"sleep": {
"window_hours": 48, "total_epochs": 420, "epoch_secs": 5,
"wake_epochs": 38, "n1_epochs": 35, "n2_epochs": 210,
"n3_epochs": 95, "rem_epochs": 42
},
"scores": {
"focus": 55.0, "relaxation": 42.0, "engagement": 68.0,
"faa": -0.080, "tar": 0.530, "bar": 0.820, "tbr": 1.210,
"hr": 72.1, "meditation": 52.0, "drowsiness": 12.0,
"bands": { "rel_delta": 0.274, "rel_theta": 0.180, "rel_alpha": 0.338, "..." : "..." }
},
"history": {
"total_sessions": 45, "recording_days": 31,
"current_streak_days": 5, "total_recording_hours": 82.3,
"longest_session_min": 95, "avg_session_min": 42,
"today_vs_avg": {
"focus": { "today": 0.62, "avg_7d": 0.55, "delta_pct": 12.7, "direction": "up" }
}
}
}labelInserts a timestamped annotation. Returns the created label_id. Also broadcasts a label-created event to all connected clients (including the desktop app's dashboard chart).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| text | string | required | Label text (free-form, must not be empty) |
| label_start_utc | u64 | optional | Override start timestamp in unix seconds (default: now) |
{ "command": "label", "text": "eyes closed resting state" }{ "command": "label", "ok": true, "label_id": 42 }searchQueries the HNSW vector index for the k nearest embeddings within a time range. Searches across all recording days. Returns results ranked by cosine distance, with timestamps, distances, day folders, labels, and an analysis summary (distance statistics, top days, temporal distribution, neighbor metric averages).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| start_utc | u64 | required | Search window start (unix seconds) |
| end_utc | u64 | required | Search window end (must be ≥ start_utc) |
| k | u64 | optional | Nearest neighbours per query (default: 10, max: 100) |
| ef | u64 | optional | HNSW search beam width (default: max(k, 50), higher = more accurate) |
{ "command": "search", "start_utc": 1708790000, "end_utc": 1708800000, "k": 5 }{
"command": "search", "ok": true,
"result": {
"query_count": 541,
"searched_days": ["20260223", "20260224"],
"results": [{
"timestamp_unix": 1708795032,
"neighbors": [{
"distance": 0.023, "timestamp_unix": 1708791200,
"date": "20260223", "device_name": "Muse-A1B2",
"labels": [{ "text": "focused reading" }]
}]
}],
"analysis": {
"total_neighbors": 25, "time_span_hours": 6.2,
"distance_stats": { "min": 0.018, "mean": 0.042, "max": 0.089, "stddev": 0.015 },
"top_days": [["20260224", 15], ["20260223", 10]],
"temporal_distribution": { "8": 12, "9": 8, "14": 5 },
"neighbor_metrics": { "focus": 0.62, "relaxation": 0.45 }
}
}
}sessionsLists all recording sessions across all daily folders, newest first. Sessions are contiguous ranges of embedding epochs (gap threshold: 120 seconds). Each session includes start/end timestamps, epoch count, and the day folder name.
{ "command": "sessions" }{
"command": "sessions", "ok": true,
"sessions": [
{ "start_utc": 1708790000, "end_utc": 1708792847, "n_epochs": 541, "day": "20260224" },
{ "start_utc": 1708700000, "end_utc": 1708702565, "n_epochs": 513, "day": "20260223" },
{ "start_utc": 1708681500, "end_utc": 1708684353, "n_epochs": 570, "day": "20260223" }
]
}compareComputes aggregate metrics for two time ranges and returns them side-by-side. Includes per-range session metrics (all bands + derived scores + PPG), sleep staging for both ranges, compare insights (timeseries stats, per-metric deltas with direction and percentage change, lists of improved/declined metrics), and auto-enqueues a Brain Nebula™ projection job for visual comparison.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| a_start_utc | u64 | required | Session A start (unix seconds) |
| a_end_utc | u64 | required | Session A end |
| b_start_utc | u64 | required | Session B start |
| b_end_utc | u64 | required | Session B end |
{ "command": "compare", "a_start_utc": 1708700000, "a_end_utc": 1708702565, "b_start_utc": 1708790000, "b_end_utc": 1708792847 }{
"command": "compare", "ok": true,
"a": { "focus": 0.62, "relaxation": 0.45, "hr": 72.1, "..." : "..." },
"b": { "focus": 0.71, "relaxation": 0.38, "hr": 68.4, "..." : "..." },
"sleep_a": { "summary": { "total_epochs": 513, "..." : "..." }, "epochs": [...] },
"sleep_b": { "summary": { "total_epochs": 541, "..." : "..." }, "epochs": [...] },
"insights": {
"n_epochs_a": 513, "n_epochs_b": 541,
"deltas": {
"focus": { "a": 0.62, "b": 0.71, "abs": 0.09, "pct": 14.5, "direction": "up" },
"hr": { "a": 72.1, "b": 68.4, "abs": -3.7, "pct": -5.1, "direction": "down" }
},
"improved": ["focus", "engagement", "snr"],
"declined": ["relaxation", "hr"]
},
"umap": {
"queued": true, "job_id": 3,
"estimated_ready_utc": 1708800012,
"queue_position": 0, "estimated_secs": 12,
"n_a": 513, "n_b": 541
}
}sleepClassifies EEG epochs into sleep stages (Wake/N1/N2/N3/REM) using relative band-power ratios with simplified AASM heuristics. Returns a per-epoch hypnogram and a summary, plus a sleep analysis with efficiency, onset latency, REM latency, transition count, awakenings, stage durations, and bout analysis.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| start_utc | u64 | required | Analysis window start (unix seconds) |
| end_utc | u64 | required | Analysis window end (must be ≥ start_utc) |
{ "command": "sleep", "start_utc": 1708750000, "end_utc": 1708780000 }{
"command": "sleep", "ok": true,
"epochs": [
{ "utc": 1708750000, "stage": "Wake", "rel_delta": 0.22, "rel_theta": 0.25, "rel_alpha": 0.30, "rel_beta": 0.18 },
{ "utc": 1708750005, "stage": "N1", "..." : "..." }
],
"summary": {
"total_epochs": 1054, "epoch_secs": 5,
"wake_epochs": 134, "n1_epochs": 89, "n2_epochs": 421,
"n3_epochs": 298, "rem_epochs": 112
},
"analysis": {
"efficiency_pct": 87.3, "onset_latency_min": 8.5,
"rem_latency_min": 62.0, "transitions": 34, "awakenings": 6,
"stage_minutes": { "wake": 11.2, "n1": 7.4, "n2": 35.1, "n3": 24.8, "rem": 9.3 },
"bouts": {
"N2": { "count": 8, "mean_min": 4.4, "max_min": 12.1 },
"N3": { "count": 5, "mean_min": 5.0, "max_min": 9.2 },
"REM": { "count": 3, "mean_min": 3.1, "max_min": 5.8 }
}
}
}calibrateOpens the calibration window in the app. Requires a connected Muse device. The calibration protocol runs guided alternating actions (default: Eyes Open / Eyes Closed, 10s each, 5s breaks, 3 loops) to establish baseline metrics.
{ "command": "calibrate" }{ "command": "calibrate", "ok": true }umapEnqueues a Brain Nebula™ projection job (GPU-accelerated UMAP via fast-umap + wgpu/CubeCL) on the selected embeddings. Returns a job_id for polling. Results are cached in ~/.skill/umap_cache/ — repeated queries for the same session pair return instantly. User labels are attached to points when available.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| a_start_utc | u64 | required | Session A start (unix seconds) |
| a_end_utc | u64 | required | Session A end |
| b_start_utc | u64 | required | Session B start |
| b_end_utc | u64 | required | Session B end |
{ "command": "umap", "a_start_utc": 1708700000, "a_end_utc": 1708702565, "b_start_utc": 1708790000, "b_end_utc": 1708792847 }{
"command": "umap", "ok": true,
"queued": true, "job_id": 5,
"estimated_ready_utc": 1708800014,
"queue_position": 0, "estimated_secs": 14,
"n_a": 513, "n_b": 541
}umap_pollPolls an async Brain Nebula™ projection job. Returns pending with epoch progress (epoch, total_epochs, loss, best_loss, elapsed_secs, epoch_ms), or complete with the 3D point cloud and cluster analysis (separation score, inter-cluster distance, intra-spread per session, centroids, outlier counts).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| job_id | u64 | required | Job ID from the umap command |
{ "command": "umap_poll", "job_id": 5 }{
"command": "umap_poll", "ok": true,
"status": "complete",
"result": {
"points": [
{ "x": 1.23, "y": -0.45, "z": 2.01, "session": 0, "utc": 1708700005 },
{ "x": 1.31, "y": -0.52, "z": 1.98, "session": 0, "utc": 1708700010, "label": "focused" },
{ "x": -2.10, "y": 1.82, "z": 0.33, "session": 1, "utc": 1708790005 }
],
"n_a": 513, "n_b": 541, "dim": 32, "elapsed_ms": 8432,
"analysis": {
"separation_score": 3.42,
"inter_cluster_distance": 4.15,
"intra_spread_a": 1.21, "intra_spread_b": 1.08,
"centroid_a": [0.82, -0.31, 1.55],
"centroid_b": [-1.95, 1.42, 0.22],
"n_outliers_a": 3, "n_outliers_b": 2
}
}
}Command-Line Interface
A TypeScript CLI wraps the WebSocket API with mDNS auto-discovery, session-aware defaults, colorized output, and --json mode for piping to jq. 9 commands including search, compare, sleep staging, and UMAP with live progress bars.
npx neuroskill status --json | jq '.scores.focus'WebSocket Examples
Python Stream metrics + submit labels
import asyncio, json, websockets, time
async def main():
async with websockets.connect("ws://localhost:PORT") as ws:
# Open calibration window
await ws.send(json.dumps({"command": "calibrate"}))
print(await ws.recv())
# Submit a label
await ws.send(json.dumps({"command": "label",
"text": "eyes closed resting state"}))
print(await ws.recv())
# Search embeddings from the last 5 minutes
now = int(time.time())
await ws.send(json.dumps({"command": "search",
"start_utc": now - 300, "end_utc": now, "k": 5}))
result = json.loads(await ws.recv())
print(json.dumps(result, indent=2))
# Listen for live events
async for msg in ws:
data = json.loads(msg)
if data.get("event") == "eeg-bands":
p = data["payload"]
print(f"α={p['channels'][1]['rel_alpha']:.3f} focus={p['focus_score']:.0f} hr={p.get('hr','--')}")
asyncio.run(main())Node.js Search + compare
const WebSocket = require("ws");
const ws = new WebSocket("ws://localhost:PORT");
ws.on("open", () => {
// Search last 5 minutes
const now = Math.floor(Date.now() / 1000);
ws.send(JSON.stringify({
command: "search",
start_utc: now - 300, end_utc: now, k: 10
}));
});
ws.on("message", (data) => {
const msg = JSON.parse(data);
if (msg.command) {
// Response to a command
console.log("command response:", msg.command, msg.ok);
} else if (msg.event) {
// Broadcast event
console.log("event:", msg.event);
}
});websocat Quick terminal test
# Install: brew install websocat (or: cargo install websocat)
# Open calibration window
echo '{ "command":"calibrate" } ' | websocat ws://localhost:PORT
# Submit a label
echo '{ "command":"label","text":"meditation session" } ' | websocat ws://localhost:PORT
# Search last 10 minutes
echo '{ "command":"search","start_utc":'$(($(date +%s)-600))',"end_utc":'$(date +%s)' } ' \
| websocat ws://localhost:PORT
# Stream all events (Ctrl+C to stop)
websocat ws://localhost:PORTNotes
Embedding Model
Embeddings are 32-dimensional vectors produced by the ZUNA EEG foundation model. Each epoch is 5 seconds of 4-channel EEG (1,280 samples @ 256 Hz), z-scored and divided by 10. The HNSW index uses M=16, ef_construction=200 for fast approximate nearest-neighbor search.
UMAP Backend
Brain Nebula™ uses fast-umap with a wgpu/CubeCL GPU backend (parametric UMAP). Configurable via ~/.skill/umap_config.json: n_neighbors, n_epochs (50–2000), repulsion_strength, neg_sample_rate, timeout_secs. Results cached in ~/.skill/umap_cache/.
Data Storage
Each recording day gets a folder (~/.skill/YYYYMMDD/) containing eeg.sqlite (embeddings table + timestamps) and eeg_embeddings.hnsw (HNSW index). Labels are stored separately in ~/.skill/labels.sqlite. Settings live in ~/.skill/settings.json.
Overlap & Epoch Rate
Default overlap is 2.5 s (50%), meaning a new embedding epoch every 2.5 s. Configurable between 0 s (no overlap) and 4.5 s (90% overlap). The band analysis hop is 64 samples (250 ms), producing ~4 snapshots/second.
Sleep Staging
Sleep classification uses relative band-power ratios (delta/theta/alpha/beta) with simplified AASM heuristics applied to each 5-second embedding epoch. The analysis includes efficiency, onset/REM latency, transition count, awakenings, per-stage durations, and bout statistics.