* docs(readme): reconcile root README as the single source of truth
/README.md (renders on GitHub) and work/README.md (packaged by
work/pyproject.toml's readme = "README.md") had cross-drifted: each
had two of four correct lines. Root had the correct `cd meshai/work`
path and work/-prefixed curl URLs; work/README.md had the correct
gemini-3.1-flash-lite model (google retired the gemini-2.x lite tier
on this project's API key).
Pull the one missing fix (model name) into root/README.md. Verified
via diff that these were the only 4 lines (8 diff lines) that ever
differed between the two files.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* build(work): eliminate work/README.md as a second source of truth
The hand-maintained duplicate is what caused the cross-drift fixed in
the previous commit. Replace work/README.md with a symlink to
../README.md so there is exactly one file to edit.
This required unhooking work/'s packaging from a physical README.md:
- setuptools' pyproject reader hard-rejects readme paths outside the
project dir (`_assert_local` in setuptools/config/expand.py) — so
`readme = "../README.md"` is not an option, tested and confirmed.
- The symlink resolves fine for local packaging (pip install -e .,
python -m build --sdist/--wheel all tested passing, PKG-INFO
correctly carries the root content through the symlink).
- It does NOT resolve for the Docker image build: work/Dockerfile's
`COPY README.md .` and both work/docker-compose.yml (context: .)
and .github/workflows/docker-publish.yml (context: work) pin the
build context to work/, which does not contain the symlink's
target. Confirmed with an isolated repro: Docker COPY on a symlink
whose target is outside the build context fails with "too many
links". Repointing the build context at the repo root would touch
every COPY path in the Dockerfile plus CI — out of scope here and
not worth it for a README.
Chose the minimal fix instead: drop `readme = "README.md"` from
work/pyproject.toml (meshai isn't published to PyPI — no publish
workflow exists, only GHCR image publishing — so there's no
long_description to lose in practice) and drop the now-unnecessary
`COPY README.md .` from work/Dockerfile. Confirmed a dangling
same-named symlink left in the build context, never COPYed, does not
break context transfer.
Tested end-to-end: a full `docker build -f work/Dockerfile work`
against the real Dockerfile succeeded (frontend build, apt deps, pip
install -e ., fastembed model fetch), and the resulting image imports
meshai and reports correct `pip show` metadata with no README
involved.
Note for docs/onboarding-via-gui (PR #147) and
chore/untrack-dashboard-static: both edited work/README.md, the file
GitHub never rendered. That target is gone; their Quick-start content
needs to be re-applied to root README.md when those branches rebase.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(meshcore): self-service add/remove channel provisioning from dashboard
Adds MeshCoreTransport.add_channel()/remove_channel() (meshcore_transport.py),
scanning the full 40-slot companion channel table for a free slot (no early
empty-run cutoff, unlike _enumerate_channels) and writing/clearing slots via
the only available write opcode (set_channel, 0x20) — the lib exposes no
delete-channel opcode, so removal writes the slot back to its empty state
(blank name + all-zero 16-byte secret).
POST /api/meshcore/channels and DELETE /api/meshcore/channels/{name} routes
(mesh_send_routes.py) validate name/hex-key and return the refreshed channel
list, matching the existing secrets_routes.py HTTPException idiom.
Frontend: Add-channel row (name + optional PSK hex) and a per-channel Remove
affordance inside the existing Observe MeshCore Channels block
(MeshCoreConnection.tsx), plus addMeshcoreChannel()/removeMeshcoreChannel()
API client functions (api.ts).
Built + deployed to CT 108 (docker compose build && up -d, container
healthy); self-cleaning smoke test passed — POST/DELETE of a #meshai-test
channel left the companion table exactly as found.
* MeshCore: display per-channel key (PSK) with reveal + copy
The Observe MeshCore Channels list now shows each channel's PSK hex so
operators can share it with people who want to join. Fetches the
/api/meshcore/channels/detail endpoint (name + key) instead of the
names-only list; keys are masked by default with a per-row eye reveal
toggle and a copy-to-clipboard button. Channels with no retrievable key
show a dash. Existing observe (checkbox), remove (trash), and add-row
controls are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(llm): recommend a MeshCore channel by location + flash-lite default
Add a location-aware MeshCore channel-recommendation context block to the
single-shot LLM prompt so "what channel should I join?" gets an answer with
the channel NAME and join KEY.
- router.py: new standalone helper _build_meshcore_channel_block(config,
channel_details, position) — lists ONLY observed channels
(meshcore_context.observe_channels), each with its PSK key
(channel_details) and, where a region_routes cell maps to it, the covered
region's human geography (local name, cities, centroid) built from a
channel->regions reverse map over notifications.region_routes.cells and a
best-effort fuzzy attach to mesh_intelligence regions. Injected inside the
existing should_inject_mesh gate, right after the region-geography block;
fetches channel_details() + get_node_position() off self.connector and is
fully fail-safe (any error skips the block, never breaks the LLM path).
Empty observe_channels => empty block (LLM cannot invent channels).
- composite_transport.py: add channel_details() passthrough mirroring
known_channels().
- README.md: bump the example llm model to gemini-2.5-flash-lite so a fresh
deploy is policy-compliant (flash-lite supports Google Search grounding).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(identity): transport-aware bot identity + alert-channel awareness
Fixes the LLM system prompt being transport-blind: MeshCore DMs were
being told they were on the freq51 Meshtastic mesh network and were
physical node !27780c47 (AIDA-N2), and MeshMonitor/node-health text
(Meshtastic-only) leaked into MeshCore prompts too.
- config.py: BotConfig gains mt_mesh_name/mt_node/mc_mesh_name (generic
OSS defaults empty; name/owner defaults now generic MeshAI/Unknown).
LLMConfig.system_prompt code default no longer hardcodes freq51/Twin
Falls.
- router.py: generate_llm_response() computes transport once and
branches identity: MeshCore gets an AIDA/MeshCore-radio framing with
no MT node id; Meshtastic keeps the physical-node framing from the
new config fields. MeshMonitor block and MT node-health/gateway/packet
reporting (mesh_reporter Tier1/region/node detail + _MESH_AWARENESS_PROMPT
+ region geography) are now gated to transport == meshtastic. New
_build_alert_channels_line() adds a short identity-block line on both
transports listing region_routes-derived alert channels (MT channel
index or MC channel name). MeshCore channel-recommendation block stays
transport-neutral.
- README.md: dead gemini-2.5-flash-lite example updated to
gemini-3.1-flash-lite.
Live config (meshai_data volume, not git-tracked): bot.name=AIDA,
bot.owner=K7ZVX (local.yaml identity.owner, which overrides config.yaml
per config_loader.py LOCAL_FIELDS), mt_mesh_name/mt_node set to the
freq51/AIDA-N2 values, mc_mesh_name left empty. llm.yaml system_prompt
freq51 line removed.
Verified via in-container prompt-assembly dry run through the real
generate_llm_response() path (no LLM call) for both transports, and a
live gemini-3.1-flash-lite 3-query behavior test confirming the
MeshCore channel-recommendation feature still works correctly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit confirmed the Rich TUI (`meshai --config`) is a strict subset of the
dashboard GUI (covers FEWER sections) and nothing depends on it for bootstrap.
Strip it so the dashboard is the single config surface.
- delete meshai/cli/configurator.py (~1435 lines); cli/__init__ minimal
- main.py: drop --config + run_configurator dispatch (keep --config-file);
bootstrap log now points to config.example.yaml / the dashboard
- docker-entrypoint.sh: remove the ttyd `meshai --config` block on :7682
(default-config heredoc + bot exec loop untouched)
- docker-compose.yml + Dockerfile: drop the 7682 port + ttyd install (EXPOSE 8080)
- README / config.py header / config.example.yaml: de-reference the TUI
- drop `rich` from pyproject + requirements (grep-verified unused)
Close the only GUI config gap the audit found:
- Config.tsx weather tab: add openmeteo.url + wttr.url inputs (backend already
round-trips the nested WeatherConfig dataclasses)
Bootstrap intact: config.example.yaml copy, entrypoint default-config write,
and defaulted Config() when no file exists all remain. `python3 -m meshai`
and `--config-file` unchanged. TS/import validated at Docker build.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Native adapters vs. Central" subsection: each hazard feed chooses its
data path with feed_source (native = direct public-API fetch, the default and
original path; central = subscribe to Central's pre-aggregated NATS JetStream
firehose). Documents the environmental.central config, mutual exclusivity,
the satpass (central-only) / ducting (native-only) exceptions, runtime
auto-reconnect behavior, and the startup-needs-Central caveat.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrite the README to reflect the current project: dual-transport
(Meshtastic base + MeshCore auto-on via meshcore_host), the web dashboard
(with live screenshots), per-mesh routing, the conversational bot with
per-mesh scoped context + three privacy lanes, environmental/hazard
broadcasts, mesh-health scoring, and the RAG knowledge base. Removes the
retired subscription backend/commands, updates the LLM model + architecture,
and adds live dashboard screenshots under docs/images/.
For transparency, documents that the project was vibecoded (built with LLM
coding assistants).
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pyproject.toml references readme = "README.md" and the Dockerfile
COPYs it for pip install -e. With build context set to work/, the
file must exist there.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>