Imported from NeuroSkill-com/skill (
SKILL.md). Install upstream withnpx skills add NeuroSkill-com/skill. Copyright stays with the author.
NeuroSkill™ CLI — Complete Reference
NeuroSkill™ exposes a real-time EEG analysis API through a local WebSocket server and an HTTP tunnel.
The cli.ts script is the fastest way to query it from a terminal, shell script, or any
automation pipeline.
Contents
- Supported Devices
- Transport — WebSocket & HTTP
- Quick Start
- Output Modes
- Global Options
- Polling with
status - Commands
- Screenshots
- Data Reference
- Use-Case Recipes
- Focus & Productivity
- Stress & Anxiety
- Sleep Quality
- Cognitive Load
- Meditation & Relaxation
- Comparing Two Sessions
- Time-Range Queries
- Automation & Scripting
Supported Devices
NeuroSkill™ supports multiple EEG headsets. The CLI and API work identically regardless of which device is connected — all commands, scores, and metrics are device-agnostic.
| Device | Channels | Sample Rate | Connection | Notes |
|---|---|---|---|---|
| Muse (S, 2, 2016) | 4 (TP9, AF7, AF8, TP10) | 256 Hz | BLE | Default device. PPG + IMU included. |
| OpenBCI Ganglion | 4 | 200 Hz | BLE | Open-source research-grade board. |
| OpenBCI Cyton / Cyton+Daisy | 8 / 16 | 250 Hz | Serial / WiFi | Full 10-20 montage capable. |
| Neurable MW75 Neuro | 12 (FT7/T7/TP7/CP5/P7/C5, FT8/T8/TP8/CP6/P8/C6) | 500 Hz | BLE + RFCOMM | Noise-cancelling headphones with EEG. Behind mw75-rfcomm feature flag. |
| RE-AK Nucleus Hermes | 8 (Fp1, Fp2, AF3, AF4, F3, F4, FC1, FC2) | 250 Hz | BLE GATT | ADS1299 + 9-DOF IMU. |
| Emotiv (EPOC/Insight/Flex/MN8) | 14 / 5 / 32 | 128 Hz | Cortex WebSocket | IMU included. Connected via Emotiv Cortex API. |
| IDUN Guardian | 1 (bipolar in-ear) | 250 Hz | BLE | Single-channel earbud EEG + 6-DOF IMU. |
| Mendi | — (fNIRS optical) | — | BLE | Frontal fNIRS headband with IMU + battery telemetry. Not traditional EEG. |
| Cognionics / CGX (Quick-20/32r/8r, AIM-2, Patch) | 2–30 | up to 500 Hz | USB Serial (FTDI) | Research-grade; full 10-20 montage on Quick-20+. IMU on r/m variants. |
| AttentivU | 4 ExG | 250 Hz | BLE | EEG glasses with 9-axis IMU. |
| BrainBit (Original, 2, Pro, Flex 4/8) | 4 (O1, O2, T3, T4) | 250 Hz | BLE (NeuroSDK2) | Consumer EEG headband. |
| g.tec Unicorn Hybrid Black | 8 | 250 Hz | BLE (Unicorn API) | Research-grade headset with IMU. |
| NeuroField Q21 | 20 | 256 Hz | PCAN-USB (CAN bus) | Full 10-20 montage. |
| BrainMaster (Atlantis/Discovery/Freedom) | 2–24 | 256 Hz | USB Serial | Neurofeedback amplifiers. Discovery/Freedom support up to 24 channels. |
| NeuroSky MindWave | 1 (Fp1) | 512 Hz | Serial (ThinkGear) | Single-channel consumer headset. |
| Neurosity (Crown / Notion) | 8 (CP3, C3, F5, PO3, PO4, F6, C4, CP4) | 256 Hz | Cloud-streamed | IMU included. |
| Brain Products BrainVision | 16 | 500 Hz | TCP/IP (RDA) | Research-grade; full 10-20 montage. |
The DSP pipeline dynamically scales to the active channel count (1–32). Inactive channels have zero overhead.
Transport — WebSocket & HTTP
NeuroSkill™ runs a local server (auto-discovered via mDNS or lsof). All commands work over
both transports; the CLI picks the best one automatically.
WebSocket (ws://127.0.0.1:<port>)
- Full-duplex, low-latency. Best for live data, event streaming, and polling loops.
- Commands are JSON messages sent over the socket; responses arrive as JSON messages.
- Supports real-time broadcast events (EEG packets, scores, label-created, …).
- Used by default when the server is reachable.
# Force WebSocket:
node cli.ts status --ws
# Direct WebSocket from any language (wscat example):
wscat -c ws://127.0.0.1:8375
> {"command":"status"}
< {"command":"status","ok":true,"device":{...},"scores":{...},...}
HTTP (http://127.0.0.1:<port>/)
- Request / response only. No live streaming, no broadcast events.
- All commands are
POST /with a JSON body; the response is JSON. - Useful from
curl, Pythonrequests, Nodefetch, or any HTTP client. - Automatic fallback when the WebSocket is unreachable.
# Force HTTP:
node cli.ts status --http
# curl equivalent of every CLI command:
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"status"}'
# Extract a single field with jq:
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"status"}' | jq '.scores.relaxation'
Auto-transport (default)
When neither --ws nor --http is given, the CLI probes WebSocket first and silently
falls back to HTTP. Informational lines are written to stderr (not stdout), so JSON
piping is never polluted.
Port Discovery
The CLI finds the port automatically via:
--port <n>flag (explicit, skips all discovery)- mDNS (
_skill._tcpservice advertisement, 5 s timeout) lsof/pgrepfallback (probes each TCP LISTEN port)
node cli.ts status --port 62853 # skip discovery entirely
Quick Start
# Prerequisites (run once):
npm install
# Snapshot of everything:
node cli.ts status
# Pipe-friendly JSON output:
node cli.ts status --json | jq '.scores'
# Print full help with examples:
node cli.ts --help
Aliases:
node cli.ts,npx tsx cli.ts, and./cli.ts(afterchmod +x) all work.
Output Modes
Every command has three output modes. Choose the one that suits your use case.
Default (no flag) — human-readable summary only
The CLI prints a structured, colored, human-readable summary to stdout. The underlying raw JSON response is not printed — it is discarded after the summary is rendered.
node cli.ts status # colored summary, no JSON
node cli.ts session 0 # trend table, no JSON
node cli.ts sleep # stage counts, no JSON
This is the fastest way to read results at a glance, but data that the summary
doesn't surface — such as per-epoch arrays, full reference objects, or every
metric field — is silently dropped. See What --full reveals
for a command-by-command breakdown.
--json — raw JSON only, pipe-safe
print() calls are suppressed entirely; only printResult() fires.
Output is plain JSON with no ANSI codes, written to stdout.
Informational lines (transport, connection, mDNS discovery) go to stderr and
never pollute the JSON stream.
node cli.ts status --json # full JSON, no summary
node cli.ts status --json | jq '.scores.relaxation' # pipe to jq
node cli.ts sleep --json | jq '.summary'
node cli.ts search --json | jq '.result.results[0].neighbors'
node cli.ts umap --json | jq '.result.points | length'
Use --json whenever you need to pipe output to another tool, parse it in a
script, or feed it into an API.
--full — human-readable summary and colorized JSON
Both print() and printResult() fire. The human-readable summary prints first,
then the complete colorized JSON response is appended below it.
node cli.ts status --full # summary + full colorized JSON
node cli.ts session 0 --full # trend table + raw first/second/trends objects
node cli.ts sleep --full # stage counts + full epochs[] array
node cli.ts umap --full # cluster analysis + full points[] array
node cli.ts search --full # match summary + all neighbors for every query epoch
--full is the inspection mode: use it when you want to see which exact fields the
server returned, discover keys the summary omits, or verify data before writing a
--json pipeline.
Colors:
--fulluses ANSI-colored JSON (keys in blue, strings in green, numbers in cyan). If you need plain text from--full, pipe throughsed 's/\x1b\[[0-9;]*m//g'or just switch to--json.
What --full reveals
The following data exists in the server response but is omitted by the default
summary. It is only visible with --full (colorized) or --json (plain).
status
| Hidden field | Type | Contents |
|---|---|---|
scores.faa, scores.tar, scores.bar … |
numbers | EEG ratios and spectral indices not surfaced in the summary's Scores section |
calibration.actions[] |
array | Full ordered list of calibration step objects (name, duration, …) |
labels.recent[] |
array | Full label objects; summary only prints text + timestamp |
hooks.latest_trigger |
object | Most recent hook trigger across all hooks: { hook, triggered_at_utc, distance, label_id, label_text }. The summary shows hook name, timestamp, and distance; the JSON has the full object. |
history.today_vs_avg |
object | Per-metric today-vs-7-day-avg comparison table (metric, today, avg_7d, delta_pct, direction) |
apps |
object | Most-used apps by window-switch count. Contains top_all_time, top_24h, top_7d arrays. Each entry: { app_name, switches, last_seen }. |
labels.top_all_time |
array | Most frequent label texts (all time). Each: { text, count }. |
labels.top_24h / labels.top_7d |
array | Most frequent label texts in the last 24 h / 7 d. |
labels.embedded |
number | Count of labels with text embeddings computed. |
screenshots |
object | Screenshot/OCR summary counts: total, with_embedding, with_ocr, with_ocr_embedding. Also top_apps_all_time and top_apps_24h arrays (each: { app_name, count }). |
node cli.ts status --json | jq '.history.today_vs_avg'
node cli.ts status --json | jq '.calibration.actions'
node cli.ts status --json | jq '.labels.recent'
node cli.ts status --json | jq '.hooks.latest_trigger'
node cli.ts status --json | jq '.apps.top_all_time[:5]'
node cli.ts status --json | jq '.apps.top_24h'
node cli.ts status --json | jq '.labels.top_all_time'
node cli.ts status --json | jq '.screenshots'
node cli.ts status --json | jq '.screenshots.top_apps_24h'
session
| Hidden field | Type | Contents |
|---|---|---|
first |
object | Every metric averaged over the first half of the session |
second |
object | Every metric averaged over the second half |
trends |
object | Direction string ("up" / "down" / "flat") for every metric key |
metrics |
object | All ~50 metric fields — the summary only prints a curated subset |
node cli.ts session 0 --json | jq '.metrics' # all metric averages
node cli.ts session 0 --json | jq '.trends' # all trend directions
node cli.ts session 0 --json | jq '{r1: .first.relaxation, r2: .second.relaxation}'
node cli.ts session 0 --json | jq '[.trends | to_entries[] | select(.value == "up") | .key]'
sessions
| Hidden field | Type | Contents |
|---|---|---|
sessions[] |
array | Raw session objects — the summary formats them into a table but the JSON contains the same data |
node cli.ts sessions --json | jq '.sessions[0]'
# → { "day": "20260224", "start_utc": 1740412800, "end_utc": 1740415510, "n_epochs": 541 }
search
| Hidden field | Type | Contents |
|---|---|---|
result.results[] |
array | Full list of query epochs, each with its complete neighbors[] array. The summary only shows the 5 closest overall — --full shows every neighbor for every query epoch. |
result.analysis.temporal_distribution |
object | Hour-of-day match counts (the bar chart in the summary, but as raw numbers) |
result.analysis.top_days |
array | [["YYYYMMDD", count], …] |
node cli.ts search --json | jq '.result.results | length' # query epoch count
node cli.ts search --json | jq '.result.results[0].neighbors' # all neighbors for epoch 0
node cli.ts search --json | jq '[.result.results[].neighbors[]] | sort_by(.distance) | .[0]'
node cli.ts search --json | jq '.result.analysis.temporal_distribution'
compare
| Hidden field | Type | Contents |
|---|---|---|
a |
object | All ~50 averaged metrics for session A |
b |
object | All ~50 averaged metrics for session B |
sleep_a / sleep_b |
objects | Full sleep staging summary for each range |
insights.deltas |
object | Full delta table for every metric (not just the key ones shown in the summary) |
umap |
object | Enqueued job info: job_id, estimated_secs, n_a, n_b |
node cli.ts compare --json | jq '.a' # all metrics for A
node cli.ts compare --json | jq '.b' # all metrics for B
node cli.ts compare --json | jq '.insights.deltas' # every metric delta
node cli.ts compare --json | jq '.insights.deltas | to_entries | sort_by(.value.pct) | reverse'
node cli.ts compare --json | jq '.umap.job_id' # use with umap --json
sleep
| Hidden field | Type | Contents |
|---|---|---|
epochs[] |
array | Per-epoch classification: { utc, stage, rel_delta, rel_theta, rel_alpha, rel_beta } for every 5-second window. Can be thousands of entries for a full night. |
node cli.ts sleep --json | jq '.epochs | length' # total epochs
node cli.ts sleep --json | jq '.epochs[0]' # first epoch
node cli.ts sleep --json | jq '[.epochs[] | select(.stage == 3)] | length' # N3 epochs
node cli.ts sleep --json | jq '[.epochs[] | {utc: .utc, stage: .stage}]' # hypnogram data
umap
| Hidden field | Type | Contents |
|---|---|---|
result.points[] |
array | 3D coordinates for every embedding epoch: { x, y, z, session, utc, label? }. Typically 500–2000+ entries. |
node cli.ts umap --json | jq '.result.points | length'
node cli.ts umap --json | jq '.result.points[0]'
# → { "x": 1.23, "y": -0.45, "z": 2.01, "session": "A", "utc": 1740380105 }
node cli.ts umap --json | jq '[.result.points[] | select(.session == "B")]'
node cli.ts umap --json | jq '[.result.points[] | select(.label != null)]' # labeled points only
search-labels
| Hidden field | Type | Contents |
|---|---|---|
results[].eeg_metrics |
object | Full EEG metrics object for the label window — the summary shows only 5 fields; the JSON has all available metrics |
results[].context |
string | Long-context string (if set) — only a truncated preview is shown in the summary |
node cli.ts search-labels "deep focus" --json | jq '.results[0].eeg_metrics'
node cli.ts search-labels "stress" --json | jq '[.results[].eeg_metrics.tbr]'
node cli.ts search-labels "meditation" --json | jq '.results[0].context'
interactive
| Hidden field | Type | Contents |
|---|---|---|
nodes[] |
array | All graph nodes — the summary prints each layer; --json gives the raw array with all fields |
edges[] |
array | All graph edges with from_id, to_id, distance, kind |
dot |
string | Complete Graphviz DOT source — only accessible via --dot or --json (never printed in default or --full) |
svg |
string | Pre-rendered SVG — PCA-scatter layout for found_labels |
svg_col |
string | Pre-rendered SVG — column-per-EEG-parent layout |
svg_3d |
string | Pre-rendered SVG — 3-D perspective-projected view of all nodes including screenshots (dark theme with depth cues) |
nodes[].eeg_metrics |
object | Full EEG metrics for text_label nodes — the summary shows 5 fields; JSON has all |
nodes[].proj_x/y/z |
numbers | 3-D PCA coordinates (normalised [-1, 1]) for all nodes with text embeddings |
nodes[].filename |
string | Screenshot image path (screenshot nodes only) |
nodes[].app_name |
string | Application name at capture time (screenshot nodes only) |
nodes[].window_title |
string | Window title at capture time (screenshot nodes only) |
nodes[].ocr_text |
string | OCR-extracted text (screenshot nodes only) |
nodes[].ocr_similarity |
number | Cosine similarity between query and OCR text (screenshot nodes only) |
node cli.ts interactive "deep focus" --json | jq '.nodes | length'
node cli.ts interactive "meditation" --json | jq '.edges | map(.kind) | unique'
node cli.ts interactive "anxiety" --json | jq '[.nodes[] | select(.kind == "text_label") | .text]'
node cli.ts interactive "coding" --json | jq '[.nodes[] | select(.kind == "screenshot") | {file: .filename, app: .app_name}]'
node cli.ts interactive "focus" --dot | dot -Tsvg > graph.svg # visualize with graphviz
node cli.ts interactive "stress" --dot | dot -Tpng > graph.png
node cli.ts interactive "relaxed" --json | jq '.dot' -r | dot -Tsvg > graph.svg # same via --json
node cli.ts interactive "work" --json | jq '.svg_3d' -r > graph_3d.svg # 3D perspective SVG
say
Like notify, say produces only a one-line confirmation in default mode.
| Hidden field | Type | Contents |
|---|---|---|
ok |
boolean | Confirmation that the utterance was enqueued |
spoken |
string | Echo of the text that was sent to TTS |
voice |
string | Voice name used (only present when --voice was specified) |
node cli.ts say "Eyes open" --json
# → { "command": "say", "ok": true, "spoken": "Eyes open" }
calibrations
The calibrations summary formats profiles into a table but the JSON contains
the full profile objects with all actions, durations, and settings.
| Hidden field | Type | Contents |
|---|---|---|
profiles[].actions[] |
array | Full ordered list of action objects (name, duration_secs) |
profiles[].break_duration_secs |
number | Break between action loops |
profiles[].auto_start |
boolean | Whether the profile auto-starts on window open |
node cli.ts calibrations --json | jq '.profiles[0].actions'
node cli.ts calibrations get 3 --json | jq '.profile'
dnd
The dnd summary prints a rich visual status (bars, scores, tips) but several
raw fields are only visible via --json.
| Hidden field | Type | Contents |
|---|---|---|
avg_score |
number | Current rolling average focus score (0–100) |
sample_count |
number | How many focus score samples have been collected |
window_size |
number | Target number of samples for the rolling window |
mode_identifier |
string | DND automation mode identifier |
dnd_active |
boolean | Whether this app activated DND |
os_active |
boolean | Whether the OS reports DND as active |
node cli.ts dnd --json | jq '{avg: .avg_score, threshold: .threshold, active: .dnd_active}'
notify
notify has almost no human-readable summary — only a one-line echo of the title and body.
The entire JSON confirmation from the server is suppressed in default mode.
| Hidden field | Type | Contents |
|---|---|---|
ok |
boolean | Confirmation that the OS notification was dispatched |
command |
string | Echo of the command name ("notify") |
node cli.ts notify "done" --full
# default output: ⚡ notify "done"
# --full appends: { "command": "notify", "ok": true }
node cli.ts notify "done" --json
# → { "command": "notify", "ok": true }
If ok is false the CLI exits with an error even in default mode, so --full
or --json is only useful when you need the confirmation in a script:
# Verify notification was delivered before continuing a script:
node cli.ts notify "build finished" --json | jq -e '.ok' > /dev/null \
&& echo "notification sent" || echo "notification failed"
calibrate
calibrate has two hidden layers:
Layer 1 — the list_calibrations intermediate call.
When --profile is given, the CLI internally calls list_calibrations to resolve the
profile name to a UUID. That response — which contains the full list of all calibration
profiles with their actions, durations, and settings — is consumed internally and
never printed, not even with --full. To inspect it, use raw or HTTP directly:
# See all profiles and their full action sequences:
node cli.ts raw '{"command":"list_calibrations"}' --json
# or:
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"list_calibrations"}' | jq '.'
The list_calibrations response shape:
{
"command": "list_calibrations",
"ok": true,
"profiles": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Eyes Open/Closed",
"loop_count": 3,
"break_duration_secs": 5,
"auto_start": true,
"actions": [
{ "name": "Eyes Open", "duration_secs": 20 },
{ "name": "Eyes Closed", "duration_secs": 20 }
]
},
// ... more profiles
]
}
Layer 2 — the run_calibration confirmation.
The actual calibration trigger response is suppressed in default mode, just like notify.
| Hidden field | Type | Contents |
|---|---|---|
ok |
boolean | Confirmation that the calibration window opened and started |
command |
string | Echo of the command name ("run_calibration") |
node cli.ts calibrate --full
# default output:
# ⚡ calibrate
# profile: Eyes Open/Closed (a1b2c3d4-…)
# --full appends:
# { "command": "run_calibration", "ok": true }
node cli.ts calibrate --json
# → { "command": "run_calibration", "ok": true }
# Check all available profile names without starting calibration:
node cli.ts raw '{"command":"list_calibrations"}' --json | jq '[.profiles[].name]'
# Verify calibration started in a script:
node cli.ts calibrate --profile "Eyes Open" --json | jq -e '.ok' > /dev/null \
&& echo "calibration started" || echo "failed — is a Muse connected?"
timer
Like notify, timer produces only a one-line header in default mode.
The server confirmation is suppressed.
| Hidden field | Type | Contents |
|---|---|---|
ok |
boolean | Confirmation that the focus-timer window opened and started |
command |
string | Echo of the command name ("timer") |
node cli.ts timer --full
# default output: ⚡ timer
# --full appends: { "command": "timer", "ok": true }
node cli.ts timer --json
# → { "command": "timer", "ok": true }
# Use in a script after a calibration block:
node cli.ts calibrate --json | jq -e '.ok' > /dev/null \
&& node cli.ts timer --json | jq -e '.ok' > /dev/null \
&& echo "calibration + timer both started"
listen
| Hidden field | Type | Contents |
|---|---|---|
| events array | array | Full array of every raw broadcast event received. The summary only prints event_type × count (plus a dedicated 🪝 section for hook triggers); --full appends every packet. |
| hook events | objects | Full { event: "hook", payload: { hook, scenario, distance, label_id, label_text, triggered_at_utc, command, text, context } } for each Proactive Hook trigger — the summary shows hook name, distance, and matched label; --json gives the complete payload for scripting. |
node cli.ts listen --seconds 10 --json | jq '[.[] | select(.event == "scores")]'
node cli.ts listen --seconds 5 --json | jq '.[0]' # first event in full
node cli.ts listen --seconds 60 --json | jq '[.[] | select(.event == "hook") | .payload]'
Global Options
| Flag | Description |
|---|---|
--port <n> |
Connect to an explicit port (skips mDNS) |
--ws |
Force WebSocket; error if unreachable |
--http |
Force HTTP REST; no live-event commands |
--json |
Raw JSON only — no summary, no colors, pipe-safe |
--full |
Human-readable summary and colorized full JSON appended below |
--no-color |
Disable ANSI color output (also honoured via NO_COLOR env var or non-TTY stdout) |
--version, -v |
Print CLI version and exit |
--help, -h |
Show full help and exit |
--poll <n> |
(status only) Re-poll every N seconds; keeps the socket open |
--dot |
(interactive only) Graphviz DOT to stdout — pipe to dot -Tsvg |
--trends |
(sessions only) Show first-half → second-half deltas |
--mode <m> |
(search-labels) text | context | both; (search-images) semantic | substring |
--k <n> |
Number of nearest neighbors (search, search-labels, search-images, eeg-for-screenshots) |
--by-image <path> |
(search-images) Search by visual similarity using CLIP embeddings instead of OCR text |
--window <n> |
(screenshots-for-eeg, eeg-for-screenshots) Temporal window in seconds (default 30 / 60) |
--k-text <n> |
(interactive) k for text-label HNSW search (default 5) |
--k-eeg <n> |
(interactive) k for EEG-similarity HNSW search (default 5) |
--k-labels <n> |
(interactive) k for label-proximity step (default 3) |
--reach <n> |
(interactive) temporal window in minutes around each EEG point (default 10) |
--ef <n> |
HNSW ef parameter (search-labels; default max(k×4, 64)) |
--seconds <n> |
Duration for listen (default 5) |
--profile <p> |
Profile name or UUID for calibrate |
--context "..." |
(label) Long-form annotation body; used by search-labels --mode context |
--at <utc> |
(label) Backdate to a specific unix second (default: now) |
--voice <name> |
(say) Voice name to use (e.g. Jasper); omit for server default |
--keywords <csv> |
(hooks add/update) Comma-separated keywords |
--scenario <s> |
(hooks add/update) any | cognitive | emotional | physical |
--command <cmd> |
(hooks add/update) Command to run on trigger |
--hook-text <txt> |
(hooks add/update) Payload text |
--threshold <f> |
(hooks add/update) Distance threshold (0.01–1.0) |
--recent <n> |
(hooks add/update) Recent-refs limit (10–20) |
--limit <n> |
(hooks log) Page size (default: 20) |
--offset <n> |
(hooks log) Row offset (default: 0) |
--actions "L1:20,L2:20" |
(calibrations create/update) Actions as label:secs pairs |
--loops <n> |
(calibrations create/update) Loop count (default: 3) |
--break <n> |
(calibrations create/update) Break duration in seconds (default: 5) |
--auto-start |
(calibrations create/update) Auto-start when opened |
--name "…" |
(calibrations update) Rename the profile |
--system "..." |
(llm chat) Prepend a system prompt |
--temperature <f> |
(llm chat) Sampling temperature 0–2 (default 0.8) |
--max-tokens <n> |
(llm chat) Maximum tokens to generate per turn (default 2048) |
--image <path> |
(llm chat) Attach image file (can repeat: --image a.jpg --image b.png) |
--mmproj <file> |
(llm add) Also download a vision projector from the same repo |
--bedtime <HH:MM> |
(sleep-schedule set) Bedtime in 24-h format (e.g. 23:00) |
--wake <HH:MM> |
(sleep-schedule set) Wake-up time in 24-h format (e.g. 07:00) |
--preset <id> |
(sleep-schedule set) Apply a named preset: default, early_bird, night_owl, short_sleeper, long_sleeper |
--metric-type <t> |
(health metrics) Metric type to query: restingHeartRate, hrv, vo2Max, bodyMass, etc. |
Polling with status
status is the single fastest call to get a complete system snapshot.
Poll it periodically from any script or external tool to react to EEG state changes.
The --poll <n> flag keeps the WebSocket connection open and re-polls every N seconds
with a live timestamp header — no need for a shell loop:
# Built-in polling (single connection, live updates):
node cli.ts status --poll 5 # refresh every 5 s
node cli.ts status --poll 10 --json # JSON snapshot every 10 s (Ctrl+C to stop)
# One-shot snapshot:
node cli.ts status --json
# Manual shell loop (opens a new connection each time):
while true; do
node cli.ts status --json | jq '.scores.relaxation'
sleep 5
done
# Alert when focus drops below 0.4:
while true; do
RELAX=$(node cli.ts status --json | jq '.scores.relaxation')
if (( $(echo "$FOCUS < 0.4" | bc -l) )); then
node cli.ts notify "Relaxation dropped" "Current: $FOCUS"
fi
sleep 10
done
What status returns
{
"command": "status",
"ok": true,
"device": {
"state": "connected", // "connected" | "connecting" | "disconnected"
"name": "Muse-A1B2", // or "MW75-Neuro-XXXX", "Hermes-XXXX", "Ganglion-XXXX"
"battery": 73, // percent
"firmware": "1.3.4",
"eeg_samples": 195840, // cumulative samples this run
"ppg_samples": 30600, // Muse only (PPG sensor)
"imu_samples": 122400,
"eeg_channels": 4 // 4 (Muse/Ganglion), 8 (Hermes), or 12 (MW75)
},
"session": {
"start_utc": 1740412800, // Unix seconds (UTC)
"duration_secs": 1847,
"n_epochs": 369 // 5-second embedding epochs computed so far
},
"signal_quality": {
// Keys vary by device — Muse: tp9/af7/af8/tp10; Hermes: fp1/fp2/af3/af4/f3/f4/fc1/fc2
// MW75: ft7/t7/tp7/cp5/p7/c5/ft8/t8/tp8/cp6/p8/c6
"tp9": 0.95, // 0–1; ≥0.9 = good, ≥0.7 = acceptable
"af7": 0.88,
"af8": 0.91,
"tp10": 0.97
},
"scores": {
// Core scores (0–1 unless noted):
"relaxation": 0.38,
"relaxation": 0.40,
"engagement": 0.60,
"meditation": 0.52,
"mood": 0.55,
"cognitive_load": 0.33,
"drowsiness": 0.10,
"hr": 68.2, // bpm (from PPG)
"snr": 14.3, // signal-to-noise ratio in dB
"stillness": 0.88, // 0–1; 1 = perfectly still
// Band powers (relative, sum ≈ 1):
"bands": {
"rel_delta": 0.28,
"rel_theta": 0.18,
"rel_alpha": 0.32,
"rel_beta": 0.17,
"rel_gamma": 0.05
},
// EEG ratios & spectral indices:
"faa": 0.042, // Frontal Alpha Asymmetry (positive = approach)
"tar": 0.56, // Theta/Alpha Ratio
"bar": 0.53, // Beta/Alpha Ratio
"tbr": 1.06, // Theta/Beta Ratio
"apf": 10.1, // Alpha Peak Frequency (Hz)
"coherence": 0.614,
"mu_suppression": 0.031
},
"embeddings": {
"today": 342,
"total": 14820,
"recording_days": 31
},
"labels": {
"total": 58,
"embedded": 42,
"recent": [
{ "id": 42, "text": "meditation start", "created_at": 1740413100 }
],
"top_all_time": [
{ "text": "deep focus", "count": 12 },
{ "text": "meditation", "count": 8 }
],
"top_24h": [ { "text": "deep focus", "count": 3 } ],
"top_7d": [ { "text": "deep focus", "count": 7 } ]
},
"apps": {
"top_all_time": [
{ "app_name": "VS Code", "switches": 342, "last_seen": 1740413000 },
{ "app_name": "Firefox", "switches": 198, "last_seen": 1740412900 }
],
"top_24h": [ { "app_name": "VS Code", "switches": 28, "last_seen": 1740413000 } ],
"top_7d": [ { "app_name": "VS Code", "switches": 142, "last_seen": 1740413000 } ]
},
"screenshots": {
"total": 1842,
"with_embedding": 1840,
"with_ocr": 1780,
"with_ocr_embedding": 1756,
"top_apps_all_time": [
{ "app_name": "VS Code", "count": 620 },
{ "app_name": "Firefox", "count": 310 }
],
"top_apps_24h": [ { "app_name": "VS Code", "count": 48 } ]
},
"sleep": {
// Last 48 h sleep staging summary:
"total_epochs": 1054,
"wake_epochs": 134,
"n1_epochs": 89,
"n2_epochs": 421,
"n3_epochs": 298,
"rem_epochs": 112,
"epoch_secs": 5
},
"hooks": {
"total": 3, // total configured hooks
"enabled": 2, // how many are enabled
"latest_trigger": { // most recent trigger across all hooks (null if never)
"hook": "Deep Work Guard", // hook name that fired
"triggered_at_utc": 1740413100,
"distance": 0.0892, // cosine distance to matched reference
"label_id": 7,
"label_text": "focused reading session"
}
},
"history": {
"total_sessions": 63,
"recording_days": 31,
"current_streak_days": 7,
"total_recording_hours": 94.2,
"longest_session_min": 187,
"avg_session_min": 89
}
}
Commands
status
Full snapshot: device state, session, signal quality, scores, bands, embeddings, labels (with most-frequent texts), hooks (with latest trigger), sleep summary, recording history, app usage analytics, and screenshot/OCR statistics.
Use --poll <n> to re-poll every N seconds over the same open connection
(keeps the socket open; press Ctrl+C to stop).
node cli.ts status
node cli.ts status --json
node cli.ts status --json | jq '.scores.relaxation'
node cli.ts status --json | jq '.scores.bands'
node cli.ts status --json | jq '.device.battery'
node cli.ts status --json | jq '.signal_quality'
node cli.ts status --json | jq '.sleep'
node cli.ts status --json | jq '.history.current_streak_days'
node cli.ts status --json | jq '.hooks' # hook summary + latest trigger
node cli.ts status --json | jq '.hooks.latest_trigger' # most recent hook trigger
node cli.ts status --json | jq '.hooks.latest_trigger.hook' # which hook fired last
node cli.ts status --json | jq '.apps.top_24h' # most-used apps in last 24h
node cli.ts status --json | jq '.apps.top_all_time[:5]' # top 5 apps all-time
node cli.ts status --json | jq '.labels.top_all_time' # most frequent label texts
node cli.ts status --json | jq '.screenshots' # screenshot/OCR counts
node cli.ts status --json | jq '.screenshots.top_apps_24h' # top screenshot apps (24h)
node cli.ts status --poll 5 # refresh every 5 seconds
node cli.ts status --poll 10 --json # JSON snapshot every 10 seconds
HTTP:
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"status"}'
session
Full metric breakdown for a single recording session, with first-half → second-half trend arrows.
Index 0 = most recent, 1 = previous, and so on.
node cli.ts session # most recent session (default: 0)
node cli.ts session 0 # same
node cli.ts session 1 # previous session
node cli.ts session 2 # two sessions ago
node cli.ts session --json
node cli.ts session 1 --json | jq '.metrics.relaxation'
node cli.ts session 0 --json | jq '{relaxation: .metrics.relaxation, hr: .metrics.hr, trend: .trends.relaxation}'
HTTP (two requests):
# Step 1 — get session list to find timestamps:
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"sessions"}' | jq '.sessions[0]'
# Step 2 — fetch full metrics for that session:
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"session_metrics","start_utc":1740412800,"end_utc":1740415510}'
Example output:
⚡ session [0]
20260224 2/24/2026, 8:00:00 AM → 8:45:10 AM 45m 10s 541 epochs
Core Scores
focus 0.70 ↑ (0.64 → 0.76)
relaxation 0.40 ↓ (0.44 → 0.36)
engagement 0.60 → (0.60 → 0.61)
meditation 0.52 ↑ (0.47 → 0.57)
mood 0.55 → (0.54 → 0.56)
cognitive load 0.33 ↓ (0.38 → 0.28)
drowsiness 0.10 → (0.11 → 0.09)
PPG / Heart
heart rate (bpm) 68.2 ↓ (70.1 → 66.3)
rmssd (ms) 42.1 ↑ (38.4 → 45.8)
...
EEG Bands
δ delta 28% ↓ (31% → 25%)
θ theta 18% ↓ (21% → 15%)
α alpha 32% ↑ (28% → 36%)
β beta 17% → (17% → 17%)
γ gamma 5% → (5% → 5%)
JSON response structure:
{
"ok": true,
"metrics": {
"relaxation": 0.38,
"relaxation": 0.40,
"n_epochs": 541,
// ... all metrics (see Data Reference)
},
"first": {
"relaxation": 0.38, // first-half average
// ...
},
"second": {
"relaxation": 0.41, // second-half average
// ...
},
"trends": {
"relaxation": "up", // "up" | "down" | "flat"
"relaxation": "down",
// ...
}
}
sessions
List every recorded session across all days. Sessions are contiguous embedding ranges (gap threshold: 120 seconds between epochs).
node cli.ts sessions
node cli.ts sessions --json
node cli.ts sessions --json | jq '.sessions | length'
node cli.ts sessions --json | jq '.sessions[0]'
node cli.ts sessions --json | jq '[.sessions[] | {day, dur: (.end_utc - .start_utc)}]'
node cli.ts sessions --trends # show per-session metric trends
HTTP:
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"sessions"}'
Example output:
⚡ sessions
3 session(s)
20260223 2/23/2026, 9:15:00 AM → 10:02:33 AM 47m 33s 570 epochs
20260223 2/23/2026, 2:30:00 PM → 3:12:45 PM 42m 45s 513 epochs
20260224 2/24/2026, 8:00:00 AM → 8:45:10 AM 45m 10s 541 epochs
JSON response:
{
"command": "sessions",
"ok": true,
"sessions": [
{
"day": "20260224",
"start_utc": 1740412800, // Unix seconds
"end_utc": 1740415510,
"n_epochs": 541 // 5-second embedding windows
},
{
"day": "20260223",
"start_utc": 1740380100,
"end_utc": 1740382665,
"n_epochs": 513
}
// ...newest first
]
}
Getting Unix timestamps for other commands:
# Get start/end of the most recent session: node cli.ts sessions --json | jq '{start: .sessions[0].start_utc, end: .sessions[0].end_utc}'
say
Speak text aloud via the on-device KittenTTS engine (fire-and-forget). The server enqueues the utterance on a dedicated TTS thread and returns immediately — the response arrives before audio playback begins.
Requires espeak-ng on PATH. First run downloads a ~30 MB TTS model from HuggingFace Hub.
node cli.ts say "Eyes open. Starting calibration."
node cli.ts say "Break time. Next: Eyes Closed." --voice Jasper
node cli.ts say "Calibration complete." --http
node cli.ts say "Hello!" --json
HTTP:
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"say","text":"Eyes open. Starting calibration."}'
# With a specific voice:
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"say","text":"Break time.","voice":"Jasper"}'
Response:
{ "command": "say", "ok": true, "spoken": "Eyes open. Starting calibration." }
// With --voice Jasper:
{ "command": "say", "ok": true, "spoken": "Break time.", "voice": "Jasper" }
Note:
--voiceis optional; omitting it uses the voice last selected in Settings → Voice.
label
Create a timestamped text annotation on the current EEG moment.
Labels are stored in the database, shown in the dashboard, and searchable via search-labels.
Optional flags:
--context "..."— long-form body stored alongside the short text; used bysearch-labels --mode contextand--mode both.--at <utc>— backdate the label to a specific unix second instead of using the current time (useful for retrospective annotation).
node cli.ts label "meditation start"
node cli.ts label "eyes closed"
node cli.ts label "feeling anxious"
node cli.ts label "coffee just finished"
node cli.ts label "task switch: coding → email"
node cli.ts label "phone notification distracted me"
node cli.ts label --json "focus block start" # just print the label_id
node cli.ts label "breathwork" --context "box breathing 4-4-4-4, 10 min"
node cli.ts label "retrospective note" --at 1740412800
HTTP:
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"label","text":"meditation start"}'
# With long-form context:
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"label","text":"eyes closed","context":"4-7-8 breathing exercise"}'
# Backdated to a specific timestamp:
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"label","text":"retrospective note","label_start_utc":1740412800}'
# Save the label_id for later reference:
LABEL_ID=$(curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"label","text":"focus block start"}' | jq '.label_id')
echo "Created label #$LABEL_ID"
Response:
{ "command": "label", "ok": true, "label_id": 42 }
hooks
List configured Proactive Hooks with scenario + last-trigger metadata.
Supports the following subcommands:
| Subcommand | Description |
|---|---|
hooks (or hooks status) |
List hooks with scenario + last-trigger metadata |
hooks list |
List raw hook rules (name, keywords, threshold, enabled, …) |
hooks add <name> [opts] |
Add a new hook rule |
hooks remove <name> |
Delete a hook by name |
hooks enable <name> |
Enable a hook |
hooks disable <name> |
Disable a hook |
hooks update <name> [opts] |
Update fields on an existing hook |
hooks suggest "kw1,kw2" |
Suggest threshold from matching labels + recent EEG embeddings |
hooks log [--limit N --offset M] |
View paginated hook trigger audit log rows |
Hook mutation flags (for hooks add and hooks update):
| Flag | Description |
|---|---|
--keywords <csv> |
Comma-separated keywords (e.g. "focus,deep work,flow") |
--scenario <s> |
any | cognitive | emotional | physical |
--command <cmd> |
Command to run on trigger |
--hook-text <txt> |
Payload text |
--threshold <f> |
Distance threshold (0.01–1.0) |
--recent <n> |
Recent-refs limit (10–20) |
# Status (default) — hooks with scenario + last trigger
node cli.ts hooks
node cli.ts hooks --json
node cli.ts hooks --json | jq '.hooks[] | {name: .hook.name, scenario: .hook.scenario, last: .last_trigger.triggered_at_utc}'
# List raw hook rules
node cli.ts hooks list
node cli.ts hooks list --json
# Add a new hook
node cli.ts hooks add "Deep Work Guard" --keywords "focus,deep work,flow" --scenario cognitive --threshold 0.14
node cli.ts hooks add "Stress Alert" --keywords "stress,anxious,overwhelmed" --scenario emotional --threshold 0.12
# Update an existing hook
node cli.ts hooks update "Deep Work Guard" --keywords "focus,flow" --threshold 0.12
# Enable / disable
node cli.ts hooks enable "Deep Work Guard"
node cli.ts hooks disable "Deep Work Guard"
# Remove
node cli.ts hooks remove "Deep Work Guard"
# Suggest threshold from real EEG/label data
node cli.ts hooks suggest "focus,deep work"
node cli.ts hooks suggest "focus" --json | jq '.suggestion.suggested'
# Scenario-focused quick examples:
# cognitive: deep work overload guard
node cli.ts hooks suggest "focus,deep work"
# emotional: stress-recovery style labels
node cli.ts hooks suggest "stress,anxious,overwhelmed"
# physical: fatigue/body-state style labels
node cli.ts hooks suggest "fatigue,tired,slump"
# View hook trigger audit log
node cli.ts hooks log --limit 20 --offset 0
node cli.ts hooks log --json | jq '.rows[] | {ts: .triggered_at_utc, hook: (.hook_json|fromjson).name, scenario: (.hook_json|fromjson).scenario}'
HTTP:
# Status
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"hooks_status"}'
# List raw rules
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"hooks_get"}'
# Set hooks (add/remove/update — sends the full array)
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"hooks_set","hooks":[...]}'
# Suggest threshold
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"hooks_suggest","keywords":["focus","deep work"]}'
# Audit log
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"hooks_log","limit":20,"offset":0}'
How Proactive Hooks Work (Broadcast → listen)
Proactive Hooks are a real-time pattern matching system that runs inside the EEG embedding pipeline. Every 5 seconds, when a new EEG embedding epoch is computed, the server checks all enabled hooks against the live brain state:
EEG stream → 5 s epoch → embedding vector → cosine distance to hook references
↓
distance ≤ threshold?
↓
scenario gate passes?
↓
broadcast { event: "hook", payload: {...} }
+ audit log in hooks.sqlite
+ OS toast notification
Step by step:
-
Labels as reference patterns — When you create labels (
node cli.ts label "deep focus"), the server embeds both the text and the surrounding EEG window. Each hook'skeywordsare matched against label texts to build a set of reference EEG embeddings (up torecent_limitmost recent matches). -
Live comparison — Every new 5-second EEG epoch is compared (cosine distance) against every enabled hook's reference embeddings. If the closest match is within the hook's
distance_threshold, the hook fires. -
Scenario gating — Before firing, the hook checks the current epoch's metrics against the scenario filter:
any— always passescognitive— requires elevated theta/beta ratio or cognitive load ≥ 55emotional— requires stress index ≥ 55, mood ≤ 45, or relaxation ≤ 35physical— requires drowsiness ≥ 55, headache/migraine index ≥ 45, or extreme HR
-
Cooldown — A hook cannot fire more than once every 10 seconds (prevents rapid-fire spam when the brain state is sustained).
-
Broadcast — When a hook fires, the server pushes a
{ "event": "hook" }message over WebSocket to all connected clients. This is the same broadcast mechanism used for EEG, scores, and label events — any WebSocket listener receives it. -
Audit log — Every trigger is persisted to
hooks.sqlitefor later review viahooks log.
Why this matters for the CLI:
Because hook triggers are broadcast events, listen captures them automatically.
This lets you build automation pipelines that react to your brain state in real time:
# ── React to hook triggers in a shell script ──────────────────────────────────
# Listen for 5 minutes and act on any hook triggers:
node cli.ts listen --seconds 300 --json | jq -c '.[] | select(.event == "hook") | .payload' | while read -r payload; do
HOOK=$(echo "$payload" | jq -r '.hook')
DIST=$(echo "$payload" | jq -r '.distance')
LABEL=$(echo "$payload" | jq -r '.label_text')
echo "Hook triggered: $HOOK (dist=$DIST, label=$LABEL)"
# Run custom actions based on hook name:
case "$HOOK" in
"Deep Work Guard")
node cli.ts notify "Deep Focus Detected" "Distance: $DIST to '$LABEL'"
;;
"Stress Alert")
osascript -e 'display notification "Take a break" with title "Stress Detected"'
;;
esac
done
# ── Python real-time hook listener ────────────────────────────────────────────
python3 - <<'EOF'
import asyncio, json
import websockets
async def listen_for_hooks(port: int):
async with websockets.connect(f"ws://127.0.0.1:{port}") as ws:
print("Listening for hook triggers…")
async for raw in ws:
msg = json.loads(raw)
if msg.get("event") == "hook":
p = msg["payload"]
print(f"🪝 {p['hook']} fired! distance={p['distance']:.4f} label=\"{p['label_text']}\"")
# Do something: send Slack message, log to CSV, trigger HomeKit, etc.
asyncio.run(listen_for_hooks(8375))
EOF
# ── End-to-end workflow: create labels, add hook, listen for triggers ─────────
# 1. Record some EEG while in a specific state and label it:
node cli.ts label "deep focus coding"
node cli.ts label "deep concentration"
# 2. Create a hook that fires when your brain returns to that state:
node cli.ts hooks add "Focus Mode" \
--keywords "focus,concentration,deep" \
--scenario cognitive \
--threshold 0.15
# 3. Verify the threshold makes sense:
node cli.ts hooks suggest "focus,concentration,deep"
# 4. Listen and watch for triggers:
node cli.ts listen --seconds 300
# 5. Check the audit log after the session:
node cli.ts hooks log --limit 10
Hook trigger event shape (WebSocket):
{
"event": "hook",
"payload": {
"hook": "Deep Work Guard", // hook rule name
"scenario": "cognitive", // scenario filter
"context": "labels", // match source
"distance": 0.0892, // cosine distance to closest reference
"label_id": 7, // which label's EEG pattern matched
"label_text": "focused reading session",
"triggered_at_utc": 1740412830, // unix seconds
"command": "notify", // configured action
"text": "You're in deep focus!" // configured payload
}
}
Note: Hook broadcast events are only available over WebSocket. HTTP transport (
--http) has no push streaming, so you cannot receive hook triggers via HTTP. Uselisten(which requires WebSocket) or connect directly viaws://.
search-labels
Semantic (vector) search across all your EEG annotations. The query is embedded and compared against the label HNSW index.
node cli.ts search-labels "deep focus"
node cli.ts search-labels "relaxed meditation" --k 10
node cli.ts search-labels "anxiety" --mode context
node cli.ts search-labels "flow state" --mode both --k 5
node cli.ts search-labels "creative work" --json | jq '.results[].text'
node cli.ts search-labels "morning routine" --json | jq '.results[] | {text, sim: .similarity}'
Modes:
text(default) — searches the label short-text HNSW indexcontext— searches the long-context HNSW (requires context fields to be set)both— runs both indexes, deduplicates by best cosine distance
HTTP:
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{"command":"search_labels","query":"deep focus","k":10,"mode":"text"}'
Example output:
⚡ search-labels "deep focus" (mode: text, k: 10)
model: Xenova/bge-small-en-v1.5
k: 10 results: 3
#7 "focused reading session"
similarity: 88% distance: 0.1204 model: bge-small-en-v1.5
recorded: 2/24/2026, 8:05:00 AM (300s window)
eeg: focus=0.74 relaxation=0.38 engagement=0.62 hr=66.10 mood=0.58
#12 "deep work block"
similarity: 84% distance: 0.1601
recorded: 2/23/2026, 9:20:00 AM (300s window)
eeg: focus=0.71 relaxation=0.42 engagement=0.65 hr=68.30 mood=0.55
JSON response:
{
"command": "search_labels",
"ok": true,
"query": "deep focus",
"mode": "text",
"model": "Xenova/bge-small-en-v1.5",
"k": 10,
"count": 3,
"results": [
{
"label_id": 7,
"text": "focused reading session",
"context": "",
"distance": 0.1204,
"similarity": 0.8796, // 1 − distance
"eeg_start": 1740412800,
"eeg_end": 1740413100,
"created_at": 1740412810,
"embedding_model": "bge-small-en-v1.5",
"eeg_metrics": {
"relaxation": 0.38,
"relaxation": 0.38,
"engagement": 0.62,
"hr": 66.1,
"mood": 0.58,
"rel_alpha": 0.35,
"rel_beta": 0.19
}
}
]
}
interactive
Cross-modal 5-layer graph search. Combines semantic text search, EEG similarity search, temporal label proximity, and screenshot discovery into a single directed graph:
"deep focus" → text_label nodes (semantically similar annotations)
↓
eeg_point nodes (raw EEG moments from label time windows)
↓
found_label nodes (labels near those EEG moments in time)
↓
screenshot nodes (screenshots near EEG timestamps, ranked by
window-title / OCR-text proximity to query)
Four output formats — choose exactly one:
| Flag | Output |
|---|---|
| (none) | Colored human-readable summary of all four layers |
--full |
Summary + colorized JSON appended below |
--json |
Raw JSON: { query, nodes, edges, dot } — pipe-safe |
--dot |
Graphviz DOT source only — pipe directly to dot -Tsvg or dot -Tpng |
# Default summary:
node cli.ts interactive "deep focus"
# Tune the pipeline:
node cli.ts interactive "meditation" --k-text 8 --k-eeg 8 --k-labels 5 --reach 15
# Raw JSON — count nodes:
node cli.ts interactive "flow state" --json | jq '.nodes | length'
# Extract text_label texts:
node cli.ts interactive "focus" --json | jq '[.nodes[] | select(.kind == "text_label") | .text]'
# Extract EEG moment timestamps:
node cli.ts interactive "anxiety" --json | jq '[.nodes[] | select(.kind == "eeg_point") | .timestamp_unix]'
# Extract discovered nearby labels:
node cli.ts interactive "stress" --json | jq '[.nodes[] | select(.kind == "found_label") | .text]'
# Extract discovered screenshots:
node cli.ts interactive "coding" --json | jq '[.nodes[] | select(.kind == "screenshot") | {file: .filename, app: .app_name, ocr_sim: .ocr_similarity}]'
# Render graph as SVG (requires graphviz):
node cli.ts interactive "deep focus" --dot | dot -Tsvg > graph.svg
# Render graph as PNG:
node cli.ts interactive "meditation" --dot | dot -Tpng > graph.png
# Pull DOT from JSON output instead:
node cli.ts interactive "focus" --json | jq -r '.dot' | dot -Tsvg > graph.svg
# Full inspection (summary + full JSON):
node cli.ts interactive "anxiety" --full
Pipeline parameters:
| Flag | Default | Range | Description |
|---|---|---|---|
--k-text <n> |
5 | 1–20 | k for text-label HNSW search |
--k-eeg <n> |
5 | 1–20 | k for EEG-similarity HNSW per text label |
--k-labels <n> |
3 | 1–10 | k for label-proximity per EEG point |
--reach <n> |
10 | 1–60 | Temporal window (minutes) around each EEG point |
All parameters are server-clamped to their stated range. Out-of-range values are silently adjusted.
HTTP:
curl -s -X POST http://127.0.0.1:8375/ \
-H "Content-Type: application/json" \
-d '{
"command": "interactive_search",
"query": "deep focus",
"k_text": 5,
"k_eeg": 5,
"k_labels": 3,
"reach_minutes": 10
}'
Example default output:
⚡ interactive "deep focus" (k-text:5, k-eeg:5, k-labels:3, reach:10m)
Graph 7 nodes · 9 edges
edges: text_sim ×2 eeg_bridge ×3 label_prox ×4
──────────────────────────────────────────────────────
● query "deep focus"
◆ Text Labels (2 semantically similar labels)
#0 "focused reading session" 2/24/2026, 8:00:00 AM PST
similarity: 88% dist: 0.1204
focus 0.74 relaxation 0.38 engagement 0.62 hr 66.10 meditation 0.44
#1 "concentration phase" 2/23/2026, 2:30:00 PM PST
similarity: 82% dist: 0.1805
◈ EEG Moments (3 neural moments found)
#0 2/24/2026, 8:12:45 AM PST dist: 0.0231 ← tl_0
#1 2/22/2026, 10:14:00 AM PST dist: 0.0319 ← tl_0
#2 2/21/2026, 3:02:00 PM PST dist: 0.0487 ← tl_1
◉ Nearby Labels (2 labels found near EEG moments)
#0 "eyes closed" 2/24/2026, 8:13:00 AM PST 0.8m from EEG point
#1 "task complete" 2/22/2026, 10:18:00 AM PST 4.0m from EEG point
tip: rerun with --dot | dot -Tsvg > graph.svg to visualize
JSON response structure:
{
"command": "interactive_search",
"ok": true,
"query": "deep focus",
"k_text": 5,
"k_eeg": 5,
"k_labels": 3,
"reach_minutes": 10,
"nodes": [
{
"id": "query",
"kind": "query",
"text": "deep focus",
"timestamp_unix": null,
"distance": 0.0,
"eeg_metrics": null,
"parent_id": null
},
{
"id": "tl_0",
"kind": "text_label",
"text": "focused reading session",
"timestamp_unix": 1740412800,
"distance": 0.1204, // cosine distance from query
"eeg_metrics": {
"relaxation": 0.38, "engagement": 0.62,
"hr": 66.1, "meditation": 0.44, "rel_alpha": 0.35
},
"parent_id": "query"
},
{
"id": "ep_1740413565",
"kind": "eeg_point",
"text": null,
"timestamp_unix": 1740413565,
"distance": 0.0231, // cosine distance in EEG embedding space
"eeg_metrics": null,
"parent_id": "tl_0"
},
{
"id": "fl_42",
"kind": "found_label",
"text": "eyes closed",
"timestamp_unix": 1740413580,
"distance": 0.133, // fraction of reach window (0 = right at the EEG point)
"eeg_metrics": null,
"parent_id": "ep_1740413565"
},
{
"id": "ss_20260224080530",
"kind": "screenshot",
"text": null,
"timestamp_unix": 1740413130,
"distance": 0.05,
"eeg_metrics": null,
"parent_id": "ep_1740413565",
"filename": "20260224/20260224080530.webp",
"app_name": "VS Code",
"window_title": "main.rs",
"ocr_text": "fn dispatch(app: &AppHandle, command: &str…",
"ocr_similarity": 0.42,
"proj_x": 0.31,
"proj_y": -0.18,
"
*Truncated - read the full file at https://github.com/NeuroSkill-com/skill/blob/bfe84a981acf5209cba1f43152e4989377f3b19f/SKILL.md