Skip to content

Add CLI Console: run companion commands with visible output from the dashboard#295

Merged
awolden merged 5 commits into
meshcore-dev:mainfrom
drewmccal:feature/cli-console
Jul 22, 2026
Merged

Add CLI Console: run companion commands with visible output from the dashboard#295
awolden merged 5 commits into
meshcore-dev:mainfrom
drewmccal:feature/cli-console

Conversation

@drewmccal

Copy link
Copy Markdown
Contributor

What

Adds a CLI Console for the local companion radio: run SDK commands and see their responses in a persistent on-dashboard transcript, with a clear button and an event for automations.

execute_command already returns structured responses in the Actions dev tool, but there was no way to see command output in the UI or for non-admins. This adds that visibility layer without changing the existing service.

Changes

  • Services: cli_command (wraps execute_command, records to a transcript, fires meshcore_cli_response, returns the response), cli_command_ui (reads the text.meshcore_command helper, runs it, clears the input), cli_console_clear.
  • MeshCoreCLIConsoleSensor (new): rolling transcript (last 50 lines) exposed via state + transcript/history attributes for a markdown card. Off the device page by default.
  • button.py (new): "CLI Run Command" and "CLI Clear Console" button entities (compact entity rows, not full-width cards). Off the device page by default.
  • Structured contact errors: contact-resolution failures now return {error: ..., command, argument, detail} instead of silent None, so the console shows why a command failed (pubkey_prefix_too_short, contact_not_found, not_connected).
  • Docs: new cli-commands.md reference (explains the SDK-method vocabulary vs iOS-app/meshcli/repeater-CLI naming), plus dashboard/services/events docs.
  • Config: cli_console_enabled toggle in global settings.
  • Tests for the three services and the structured contact errors.

🤖 Generated with Claude Code

drewmccal and others added 4 commits June 23, 2026 14:40
The existing command card runs execute_command_ui but discards the
response, so output is only visible in Developer Tools or the logs
(issues meshcore-dev#240, meshcore-dev#111). This adds a CLI Console: an interactive command
surface that records each command and its response into a sensor
transcript visible in the UI. It records only command/response pairs,
never the device's continuous diagnostic/noise-floor log stream.

- const: SERVICE_CLI_COMMAND / SERVICE_CLI_COMMAND_UI, CONF_CLI_CONSOLE_ENABLED,
  CLI_CONSOLE_MAX_LINES, EVENT_CLI_RESPONSE
- coordinator: bounded cli_console_history deque + record_cli_console()
- sensor: MeshCoreCLIConsoleSensor (created when CONF_CLI_CONSOLE_ENABLED);
  state = last command, attributes carry structured history + a pre-rendered
  markdown transcript for a markdown card
- services: cli_command wraps execute_command, records the result, and fires
  meshcore_cli_response; cli_command_ui drives it from text.meshcore_command
- config_flow: "Enable CLI Console" toggle in Global Settings
- services.yaml + en.json translations + docs (dashboard, services, events)
- tests: tests/test_cli_command.py

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- New docs/docs/cli-commands.md: explains these are meshcore-py SDK method
  names (not the iOS app / meshcli / repeater-CLI vocabularies), with a
  grouped reference (query, config, channels, remote-node commands),
  argument syntax, and pointers to the authoritative source.
- Cross-link it from services.md and the dashboard overview.
- Fix the CLI Console card example: use a literal block (|) for the markdown
  content so the transcript keeps its newlines instead of collapsing onto one
  line, and modern perform_action syntax.

Addresses the discoverability gap from upstream issue meshcore-dev#111.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses console testing feedback:
- No way to clear the transcript -> add coordinator.clear_cli_console(),
  a meshcore.cli_console_clear service, and a "CLI Clear Console" button.
- The Run button as a `type: button` card renders huge -> ship Run/Clear
  as compact button *entities* (button platform), attached to the device
  so they also appear on the device page automatically.
- Docs: recommend an entities-card layout (compact rows) instead of the
  oversized button card, and explain that the auto device page can't host
  the transcript card / control placement (use a dashboard sections view).

New button platform (gated on CONF_CLI_CONSOLE_ENABLED):
  button.*_cli_run   -> calls cli_command_ui
  button.*_cli_clear -> clears the console

- services.yaml + en.json for cli_console_clear
- test: cli_console_clear clears the resolved coordinator

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Device page feedback: the CLI buttons + console sensor showed under the
device's Controls/Sensors, implying a console that can't actually work
there (a device page can't render the transcript). Detach all three CLI
entities from the device and hide them by default; they're used only by
entity_id in the dashboard CLI Console card.

Error feedback: contact-typed commands that failed to resolve returned a
silent None, surfacing as a vague "(no response)". Add _contact_error()
so the response explains why — pubkey_prefix_too_short (e.g. "ae6d" is
under 6 hex chars), contact_not_found, or not_connected — with the
offending argument. Also return a structured unknown_keyword error.

- sensor/button: no device_info, entity_registry_visible_default = False
- docs: correct the device-page claims; note entities are dashboard-only
- tests: short-prefix and unknown-contact return structured errors

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dida886

dida886 commented Jul 16, 2026

Copy link
Copy Markdown

@awolden When will this feature be approved? I'm waiting for it for my Meshcore-card project https://github.com/dida886/meshcore-card/tree/main.

@awolden awolden left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, this is well put together and the structured contact errors are a win on their own. Two changes before merge:

  1. Fold the wrapper services into a flag. Instead of cli_command/cli_command_ui, add an optional record_to_console: true boolean to execute_command and execute_command_ui. When set, record the command/response pair to the transcript and fire meshcore_cli_response, exactly what cli_command does today. That keeps one command API instead of two parallel ones (the docs currently have to explain the difference), and the Run button just calls execute_command_ui with the flag set. cli_console_clear stays as-is. The sensor, docs, tests, and error handling all carry over, this mostly deletes code from your diff.

  2. Keep the transcript out of the recorder DB. Add _unrecorded_attributes = frozenset({"history", "transcript", "last_response", "last_command"}) to the console sensor (select.py:268 has the pattern), and use a neutral state like command count or last-run timestamp instead of the raw command string. Right now every command rewrites the full 50-entry transcript into the DB (a single get_contacts response can be tens of KB), and the state can land secrets like send_login <contact> <password> in recorder history.

Non-blocking: consider attaching the sensor and buttons to the root device so the entity ids are findable without Dev Tools. The buttons work fine from a device page even if the transcript card doesn't render there.

…t out of recorder

Address PR meshcore-dev#295 review:
- Replace cli_command/cli_command_ui with a record_to_console flag on
  execute_command/execute_command_ui; the Run button sets the flag. Removes
  the parallel command API, its schema, yaml, translations, and unload entries.
- Exclude history/transcript/last_command/last_response from the recorder DB
  and make the console sensor state a neutral command count instead of the raw
  command string (which could leak secrets like send_login <contact> <password>).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@drewmccal

drewmccal commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

@awolden Thanks for the review! Both blocking items are addressed in 59ac044.

1. Folded the wrapper services into a flag. Removed cli_command / cli_command_ui and added an optional record_to_console boolean to execute_command and execute_command_ui. When set, the command/response pair is recorded to the transcript and meshcore_cli_response fires — exactly what cli_command did. The Run button now calls execute_command_ui with the flag; cli_console_clear is unchanged. This deleted the parallel command API along with its schema, services.yaml, translations, and unload entries (net −328/+230). Docs and tests carried over.

2. Kept the transcript out of the recorder DB. Added _unrecorded_attributes = frozenset({"history", "transcript", "last_response", "last_command"}) to the console sensor, and changed the state from the raw command string to a neutral command count — so the always-recorded state can no longer land secrets like send_login <contact> <password>, and the tens-of-KB transcript is no longer rewritten to the DB on every command.

All 222 unit tests pass, and I deployed to a live HA instance and confirmed the flag, the Run button, the fired event, and the neutral sensor state all behave as expected.

Non-blocking (attach the sensor/buttons to the root device): left as a potential follow-up for now, happy to fold it in if you'd like it in this PR.

@awolden awolden left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fast turnaround, all addressed cleanly. Thanks for the extra negative test and the dead code cleanup.

@awolden
awolden merged commit 8b5bb52 into meshcore-dev:main Jul 22, 2026
6 checks passed
@dida886

dida886 commented Jul 22, 2026

Copy link
Copy Markdown

@drewmccal hi.

How I can read the response of CLI ?
Screenshot_20260722_114131_Home Assistant

Screenshot_20260722_114311_Home Assistant

I thought the answer would be in the logbook

@dida886

dida886 commented Jul 22, 2026

Copy link
Copy Markdown
Screenshot_20260722_121837_Home Assistant

@dida886

dida886 commented Jul 22, 2026

Copy link
Copy Markdown
Screenshot_20260722_131112_Home Assistant It seems to me that instead of incrementing the count, responses from the CLI should appear.

@drewmccal

Copy link
Copy Markdown
Contributor Author

Hi @dida886 — good news, it's actually working. The number that keeps incrementing is the sensor's state, and that's intentional: it's just a command counter, not the output.

The command and its response are deliberately kept out of the state (and out of the recorder database). The state gets written to history, and a command like send_login <contact> <password> would otherwise store your password in the database in plain text — so the state is a safe, neutral count instead.

The actual CLI output lives in the sensor's attributes, not the state and not the logbook:

  • transcript — a ready-to-display log of recent command/response pairs
  • history — the same thing as a structured list
  • last_response / last_command — just the most recent one

To see it on your dashboard, add a markdown card that renders the transcript attribute:

type: markdown
content: |

{{ state_attr('sensor.meshcore_XXXXXX_cli_console', 'transcript') }}

(Replace meshcore_XXXXXX with your node's prefix — find it in Developer Tools → States, filter cli_console. Use a literal block |, not >, or the newlines collapse onto one line.)

The logbook won't show responses — for automations, use the meshcore_cli_response event instead, which carries command / response / is_error.

For your meshcore-card project: read state_attr(entity, 'transcript') (string) or 'history' (list of {timestamp, command, response, is_error}), or 'last_response' for just the latest reply.

drewmccal added a commit to drewmccal/meshcore-ha that referenced this pull request Jul 23, 2026
…transcript attribute

Address confusion from PR meshcore-dev#295 / issue meshcore-dev#313: users expect the CLI response in
the sensor state or the logbook. Add a note that the state is a neutral counter
(to keep secrets out of the recorder) and the output lives in the transcript /
history / last_response attributes, rendered via the markdown card.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@drewmccal

Copy link
Copy Markdown
Contributor Author

@awolden I pushed documentation changes to this feature branch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants