asher-cli 1.0.2__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- asher_cli-1.1.0/.claude/hooks/block-env.py +14 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/.claude/settings.json +1 -1
- asher_cli-1.1.0/.claude/skills/mcp-bridge/SKILL.md +15 -0
- asher_cli-1.1.0/.github/FUNDING.yml +15 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/.github/workflows/release.yml +15 -12
- {asher_cli-1.0.2 → asher_cli-1.1.0}/CHANGELOG.md +19 -6
- {asher_cli-1.0.2 → asher_cli-1.1.0}/CLAUDE.md +32 -53
- {asher_cli-1.0.2 → asher_cli-1.1.0}/PKG-INFO +51 -15
- {asher_cli-1.0.2 → asher_cli-1.1.0}/README.md +50 -14
- asher_cli-1.1.0/asher/__main__.py +116 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/activity_labels.py +22 -20
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/app.py +9 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/auth.py +13 -11
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/commands/__init__.py +49 -40
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/completion.py +6 -4
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/connection/__init__.py +4 -3
- asher_cli-1.1.0/asher/constants.py +24 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/export.py +17 -42
- asher_cli-1.1.0/asher/headless.py +626 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/helpers.py +5 -4
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/history_view.py +19 -18
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/monitoring/__init__.py +35 -34
- asher_cli-1.1.0/asher/theme.py +132 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/ui/__init__.py +53 -56
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/ui/style.tcss +26 -26
- {asher_cli-1.0.2 → asher_cli-1.1.0}/cliff.toml +3 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/ROADMAP.md +3 -1
- {asher_cli-1.0.2 → asher_cli-1.1.0}/pyproject.toml +5 -2
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_activity_labels.py +6 -5
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_auth.py +15 -4
- asher_cli-1.1.0/tests/test_headless.py +529 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_ui.py +8 -4
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/testhelpers.py +5 -4
- {asher_cli-1.0.2 → asher_cli-1.1.0}/uv.lock +1 -1
- asher_cli-1.0.2/.claude/hooks/block-env.ps1 +0 -9
- asher_cli-1.0.2/.claude/skills/release/SKILL.md +0 -43
- asher_cli-1.0.2/asher/__main__.py +0 -58
- asher_cli-1.0.2/asher/constants.py +0 -22
- {asher_cli-1.0.2 → asher_cli-1.1.0}/.claude/skills/pylitterbot-ref/SKILL.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/.claude/skills/textual/SKILL.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/.env.example +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/.githooks/pre-push +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/.github/pull_request_template.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/.github/workflows/bandit.yml +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/.github/workflows/ci.yml +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/.github/workflows/claude-code-review.yml +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/.github/workflows/claude.yml +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/.github/workflows/coverage.yml +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/.gitignore +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/.vscode/launch.json +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/.vscode/settings.json +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/.vscode/tasks.json +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/CODEOWNERS +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/LICENSE +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/app.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/__init__.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/cats.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/commands/base.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/config.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/faults.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/login_flow.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/mcp_bridge.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/mcp_config.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/notifications.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/robot_adapters.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/robot_protocol.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/slash-commands/__init__.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/roadmap-archive/README.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/roadmap-archive/cat-panel-badges.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/roadmap-archive/config-persistence.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/roadmap-archive/desktop-notifications.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/roadmap-archive/fault-monitoring.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/roadmap-archive/headless-export.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/roadmap-archive/history-export.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/roadmap-archive/tab-completion.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/renovate.json +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/requirements.txt +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/test.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/__init__.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/conftest.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_app_pilot.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_auth_pilot.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_cats.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_commands_pilot.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_completion.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_config.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_connection.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_connection_mixin.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_export.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_faults.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_history_view.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_lr5_commands.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_mcp_bridge.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_mcp_command.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_mcp_config.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_missing_robot_commands.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_monitoring.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_new_commands_pilot.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_notifications.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_robot_adapters.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_version_command.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.1.0}/watchrun.py +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
payload = json.load(sys.stdin)
|
|
6
|
+
file_path = payload.get("file_path", "")
|
|
7
|
+
except (json.JSONDecodeError, ValueError):
|
|
8
|
+
sys.exit(0)
|
|
9
|
+
|
|
10
|
+
if file_path.endswith(".env"):
|
|
11
|
+
print("Blocked: .env contains real credentials — edit it manually if needed")
|
|
12
|
+
sys.exit(2)
|
|
13
|
+
|
|
14
|
+
sys.exit(0)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mcp-bridge
|
|
3
|
+
description: How asher-cli's /mcp command and the asher-mcp-launch credential bridge work. Load before changing asher/mcp_config.py, asher/mcp_bridge.py, or the /mcp slash command.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# MCP bridge (`/mcp`)
|
|
8
|
+
|
|
9
|
+
pylitterbot ships an optional MCP server (`pip install pylitterbot[mcp]`, run via `python -m pylitterbot.mcp`) that lets an MCP client like Claude Desktop monitor/control the robot directly. Its own docs configure it with plaintext credentials in the client's JSON config — asher-cli avoids that:
|
|
10
|
+
|
|
11
|
+
- `/mcp on|off|status` (in `asher/commands/__init__.py`, logic in `asher/mcp_config.py`) adds/removes an entry (named by `mcp_config._SERVER_NAME`) in every `claude_desktop_config.json` this OS's Claude Desktop might read (`mcp_config.config_paths()` — on Windows this includes both the standard installer path and any MSIX/Microsoft Store virtualized path). The entry's `command` is `sys.executable -m asher.mcp_bridge` — never the credentials themselves.
|
|
12
|
+
- `/mcp on` also auto-installs pylitterbot's `mcp` extra via `sys.executable -m pip install "pylitterbot[mcp]==<installed version>"` if the `mcp` package isn't importable yet.
|
|
13
|
+
- `asher/mcp_bridge.py` (console script `asher-mcp-launch`) is what Claude Desktop actually spawns. It reads email/password from the OS keyring at process start, sets them as `LITTER_ROBOT_USERNAME`/`LITTER_ROBOT_PASSWORD` (pylitterbot's expected names — note these differ from asher-cli's own `.env` var `LITTER_ROBOT_USER`) in that process's environment only, then execs `python -m pylitterbot.mcp`. No credentials ever touch the on-disk MCP config.
|
|
14
|
+
- `/mcp on` requires keyring credentials. If none are found but `.env` fallback credentials are set, it copies them into the keyring automatically (since the bridge process can't reliably discover a project-relative `.env` — Claude Desktop controls its working directory, not asher-cli).
|
|
15
|
+
- Requires the `mcp` extra: `uv sync --extra mcp` / `pip install asher-cli[mcp]`. Restart Claude Desktop after toggling for the change to take effect.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# These are supported funding model platforms
|
|
2
|
+
|
|
3
|
+
github: [karanshukla]
|
|
4
|
+
patreon: # Replace with a single Patreon username
|
|
5
|
+
open_collective: # Replace with a single Open Collective username
|
|
6
|
+
ko_fi: # Replace with a single Ko-fi username
|
|
7
|
+
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
8
|
+
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
9
|
+
liberapay: # Replace with a single Liberapay username
|
|
10
|
+
issuehunt: # Replace with a single IssueHunt username
|
|
11
|
+
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
12
|
+
polar: # Replace with a single Polar username
|
|
13
|
+
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
|
14
|
+
thanks_dev: # Replace with a single thanks.dev username
|
|
15
|
+
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
@@ -44,26 +44,29 @@ jobs:
|
|
|
44
44
|
needs: [build, publish]
|
|
45
45
|
runs-on: ubuntu-latest
|
|
46
46
|
steps:
|
|
47
|
-
# Full history (fetch-depth: 0) so git-cliff can walk commits/tags.
|
|
48
47
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
49
|
-
with:
|
|
50
|
-
fetch-depth: 0
|
|
51
48
|
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
52
49
|
with:
|
|
53
50
|
name: dist
|
|
54
51
|
path: dist/
|
|
55
|
-
-
|
|
56
|
-
- name: Install git-cliff
|
|
57
|
-
run: uv tool install git-cliff
|
|
58
|
-
- name: Generate release notes with git-cliff
|
|
52
|
+
- name: Extract release notes from CHANGELOG.md
|
|
59
53
|
env:
|
|
60
54
|
VERSION: ${{ needs.build.outputs.version }}
|
|
61
55
|
run: |
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
|
|
56
|
+
# CHANGELOG.md is the single source of release prose: it is generated
|
|
57
|
+
# by git-cliff (see `poe changelog-release`) but may be hand-refined
|
|
58
|
+
# before the release is cut, and re-deriving the notes from commits
|
|
59
|
+
# here would silently discard those edits.
|
|
60
|
+
awk -v hdr="## [${VERSION}]" '
|
|
61
|
+
index($0, hdr) == 1 { found = 1; next }
|
|
62
|
+
found && index($0, "## [") == 1 { exit }
|
|
63
|
+
found { print }
|
|
64
|
+
' CHANGELOG.md > RELEASE_NOTES.md
|
|
65
|
+
if [ ! -s RELEASE_NOTES.md ]; then
|
|
66
|
+
echo "::error::CHANGELOG.md has no '## [${VERSION}]' section." \
|
|
67
|
+
"Run 'uv run poe changelog-release ${VERSION}' and commit it before releasing."
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
67
70
|
- name: Create release
|
|
68
71
|
env:
|
|
69
72
|
GH_TOKEN: ${{ github.token }}
|
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
Generated by [git-cliff](https://git-cliff.org) from conventional commits.
|
|
9
9
|
|
|
10
|
+
## [1.1.0] - 2026-08-09
|
|
11
|
+
|
|
12
|
+
### 🚀 Features
|
|
13
|
+
|
|
14
|
+
- *(headless)* Every robot command now runs without the TUI as `asher <command>`, with `--json` output and `--robot` selection; `--export` stays as a deprecated alias
|
|
15
|
+
- *(theme)* Catppuccin Mocha throughout — one palette in `asher/theme.py` drives the TUI stylesheet and every Rich style, replacing ad-hoc hexes across eleven modules
|
|
16
|
+
|
|
17
|
+
### 📚 Documentation
|
|
18
|
+
|
|
19
|
+
- Point CLAUDE.md at the release process
|
|
20
|
+
|
|
21
|
+
### 👷 CI/CD
|
|
22
|
+
|
|
23
|
+
- *(release)* Build release notes from CHANGELOG.md
|
|
24
|
+
|
|
25
|
+
### 🔧 Miscellaneous
|
|
26
|
+
|
|
27
|
+
- Trim CLAUDE.md, split MCP bridge docs into a skill, fix env-guard hook
|
|
28
|
+
|
|
10
29
|
## [1.0.2] - 2026-08-05
|
|
11
30
|
|
|
12
31
|
### 🐛 Bug Fixes
|
|
@@ -23,10 +42,6 @@ Generated by [git-cliff](https://git-cliff.org) from conventional commits.
|
|
|
23
42
|
|
|
24
43
|
- *(history)* Copy-to-clipboard + discoverable footer hints
|
|
25
44
|
|
|
26
|
-
### 📚 Documentation
|
|
27
|
-
|
|
28
|
-
- *(changelog)* Cut v1.0.1
|
|
29
|
-
|
|
30
45
|
## [1.0.0] - 2026-08-02
|
|
31
46
|
|
|
32
47
|
### 🚀 Features
|
|
@@ -38,7 +53,6 @@ Generated by [git-cliff](https://git-cliff.org) from conventional commits.
|
|
|
38
53
|
|
|
39
54
|
### 📚 Documentation
|
|
40
55
|
|
|
41
|
-
- *(changelog)* Regenerate for next release, trim duplicate README list
|
|
42
56
|
- *(roadmap)* Archive completed sections to docs/roadmap-archive/
|
|
43
57
|
|
|
44
58
|
### 🔧 Miscellaneous
|
|
@@ -57,7 +71,6 @@ Generated by [git-cliff](https://git-cliff.org) from conventional commits.
|
|
|
57
71
|
|
|
58
72
|
### 📚 Documentation
|
|
59
73
|
|
|
60
|
-
- *(changelog)* Cut v0.2.0
|
|
61
74
|
- *(commands)* Sync stale command lists in CLAUDE.md and slash-commands docstring
|
|
62
75
|
- *(readme)* Sync command tables with code, add changelog
|
|
63
76
|
|
|
@@ -4,21 +4,9 @@ Terminal dashboard for Litter Robot (LR3/LR4/LR5) via the Whisker cloud API.
|
|
|
4
4
|
|
|
5
5
|
## Stack
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- **python-dotenv** — credential loading (`.env` fallback)
|
|
11
|
-
- **keyring>=24** — OS credential store (Windows Credential Manager / macOS Keychain / Linux Secret Service)
|
|
12
|
-
|
|
13
|
-
## Tooling
|
|
14
|
-
|
|
15
|
-
- **uv** — dependency management and task runner (`uv sync`, `uv run`)
|
|
16
|
-
- **poethepoet** — task aliases via `uv run poe <task>`
|
|
17
|
-
- **ruff** — linter and formatter
|
|
18
|
-
- **mypy** — static type checking
|
|
19
|
-
- **pytest + pytest-asyncio + pytest-cov** — tests
|
|
20
|
-
- **textual-dev** — CSS hot reload devtools
|
|
21
|
-
- **watchfiles** — Python auto-restart on file change
|
|
7
|
+
- **keyring** resolves to the OS-native credential store: Windows Credential Manager / macOS Keychain / Linux Secret Service.
|
|
8
|
+
|
|
9
|
+
See `pyproject.toml` for the full dependency and dev-tooling list (`[project].dependencies`, `[dependency-groups].dev`, `[tool.poe.tasks]`).
|
|
22
10
|
|
|
23
11
|
## Entry points
|
|
24
12
|
|
|
@@ -37,6 +25,7 @@ asher/
|
|
|
37
25
|
auth.py LoginScreen modal (ModalScreen[tuple[str,str]]) — available, not primary flow
|
|
38
26
|
helpers.py fmt_ago(), drawer_bar(), ts(), robot_model() (pure, testable)
|
|
39
27
|
constants.py STATUS_COLORS, ROBOT_MODELS
|
|
28
|
+
theme.py Catppuccin Mocha palette + semantic roles (BACKGROUND, MUTED, DANGER, …) + CSS_VARIABLES/apply() — the only place a hex literal belongs
|
|
40
29
|
config.py runtime settings persistence — load()/save()/update() over ~/.asher-cli/config.json; holds poll interval, cat-panel visibility/colour, active pet index, notification settings (non-secret UI prefs only; credentials stay in keyring)
|
|
41
30
|
notifications.py desktop toast + audible alert façade over plyer (fire/beep, always-safe no-op on failure/headless)
|
|
42
31
|
cats.py CATS dict (ASCII art)
|
|
@@ -47,9 +36,10 @@ asher/
|
|
|
47
36
|
mcp_bridge.py asher-mcp-launch console script — keyring-backed pylitterbot MCP launcher
|
|
48
37
|
faults.py check_faults(robot) — model-scoped safety/component fault detection (status enum + per-model attr allowlist incl. LR4 USB power fault; hopper never a fault)
|
|
49
38
|
history_view.py HistoryScreen (ModalScreen) + format_history_rows()/format_history_text() — scrollable activity-history pager pushed by the `history` command; `c` copies the full history (plain text) to the clipboard via action_copy_all()
|
|
50
|
-
export.py shared activity-history CSV core +
|
|
39
|
+
export.py shared activity-history CSV core + exit-code contract: build_history_csv(), resolve_dest(), resolve_robot(), parse_days(), EXIT_*, ExportError — no Textual imports; the TUI `export` command and `asher export` both call build_history_csv()
|
|
40
|
+
headless.py headless command surface for `asher <command>` — Session/Result/CommandError, the COMMANDS registry, and run(); plain strings only, no Textual, routes model differences through RobotAdapter
|
|
51
41
|
completion.py pure helpers for command completion: slash popup (slash_matches, enter_completes, render_completion) + inline ghost text (CommandSuggester) — fed by _registry, no Textual imports except the Suggester base class
|
|
52
|
-
__main__.py main() entry point
|
|
42
|
+
__main__.py main() entry point — argparse subcommands (headless) vs no-args (TUI); `--export` kept as a deprecated alias
|
|
53
43
|
commands/
|
|
54
44
|
base.py Command ABC, SlashCommand, CommandRegistry
|
|
55
45
|
__init__.py CommandsMixin — all command classes + registry + dispatch
|
|
@@ -76,7 +66,8 @@ tests/
|
|
|
76
66
|
test_mcp_command.py /mcp slash command dispatch
|
|
77
67
|
test_faults.py check_faults() — safety statuses, attribute faults, graceful degradation
|
|
78
68
|
test_history_view.py format_history_rows()/format_history_text() + HistoryScreen structure, copy-all + Pilot push/dismiss
|
|
79
|
-
test_export.py build_history_csv/resolve_dest/resolve_robot/parse_days (pure) +
|
|
69
|
+
test_export.py build_history_csv/resolve_dest/resolve_robot/parse_days (pure) + the legacy `--export` flag path (no Pilot, mocks _connect_headless)
|
|
70
|
+
test_headless.py headless registry/rendering (pure) + every command handler against mock robots + run() exit codes + the argparse subcommand surface
|
|
80
71
|
test_completion.py slash_matches/enter_completes/render_completion (pure) + Pilot overlay visibility/navigation/accept
|
|
81
72
|
|
|
82
73
|
.github/workflows/
|
|
@@ -101,32 +92,22 @@ LITTER_ROBOT_PASSWORD=...
|
|
|
101
92
|
Keyring service name: `asher-cli`, keys `email` and `password`.
|
|
102
93
|
Helper functions in `asher/connection/__init__.py`: `_keyring_load()`, `_keyring_save()`, `_keyring_delete()`.
|
|
103
94
|
|
|
104
|
-
## MCP bridge (`/mcp`)
|
|
105
|
-
|
|
106
|
-
pylitterbot ships an optional MCP server (`pip install pylitterbot[mcp]`, run via `python -m pylitterbot.mcp`) that lets an MCP client like Claude Desktop monitor/control the robot directly. Its own docs configure it with plaintext credentials in the client's JSON config — asher-cli avoids that:
|
|
107
|
-
|
|
108
|
-
- `/mcp on|off|status` (in `asher/commands/__init__.py`, logic in `asher/mcp_config.py`) adds/removes an entry (named by `mcp_config._SERVER_NAME`) in every `claude_desktop_config.json` this OS's Claude Desktop might read (`mcp_config.config_paths()` — on Windows this includes both the standard installer path and any MSIX/Microsoft Store virtualized path). The entry's `command` is `sys.executable -m asher.mcp_bridge` — never the credentials themselves.
|
|
109
|
-
- `/mcp on` also auto-installs pylitterbot's `mcp` extra via `sys.executable -m pip install "pylitterbot[mcp]==<installed version>"` if the `mcp` package isn't importable yet.
|
|
110
|
-
- `asher/mcp_bridge.py` (console script `asher-mcp-launch`) is what Claude Desktop actually spawns. It reads email/password from the OS keyring at process start, sets them as `LITTER_ROBOT_USERNAME`/`LITTER_ROBOT_PASSWORD` (pylitterbot's expected names — note these differ from asher-cli's own `.env` var `LITTER_ROBOT_USER`) in that process's environment only, then execs `python -m pylitterbot.mcp`. No credentials ever touch the on-disk MCP config.
|
|
111
|
-
- `/mcp on` requires keyring credentials. If none are found but `.env` fallback credentials are set, it copies them into the keyring automatically (since the bridge process can't reliably discover a project-relative `.env` — Claude Desktop controls its working directory, not asher-cli).
|
|
112
|
-
- Requires the `mcp` extra: `uv sync --extra mcp` / `pip install asher-cli[mcp]`. Restart Claude Desktop after toggling for the change to take effect.
|
|
113
|
-
|
|
114
95
|
## Command convention
|
|
115
96
|
|
|
116
|
-
|
|
117
|
-
`clean`, `status`, `info`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto`, `night-light-brightness <level>`, `panel-brightness <low|medium|high>`, `wait-time <minutes>`, `power on|off`, `rename <name>`, `insight [days|month]`, `sleep-schedule`, `privacy on|off`, `volume <0-100>`, `camera-audio on|off`, `drawer-reset`, `history [count|all]`, `export [days|month]`, `clear`, `help`, `quit`
|
|
97
|
+
Command names, slash-command names, and their args are not listed here — see the `_registry` in `asher/commands/__init__.py`, which is authoritative; `/help` renders it at runtime. `/mcp`'s credential-bridging design is documented in the `mcp-bridge` skill.
|
|
118
98
|
|
|
119
|
-
**
|
|
120
|
-
`/login`, `/logout`, `/robots`, `/robot <index|name>`, `/pets`, `/pet <index|name>`, `/cat on|off|colour <hex>`, `/refresh [seconds|off]`, `/config`, `/notify on|off|sound on|off|test`, `/version`, `/mcp on|off|status`, `/exit`
|
|
99
|
+
**Normal commands** (no prefix) are robot actions only; **slash commands** (`/` prefix) are app management only.
|
|
121
100
|
|
|
122
101
|
`/refresh`, `/cat`, `/pet`, and `/notify` persist their settings to `~/.asher-cli/config.json` (via `asher.config.update()`), so they survive restarts. Credentials and the preferred-robot serial stay in the OS keyring; the config file holds only non-secret UI preferences.
|
|
123
102
|
|
|
124
|
-
|
|
103
|
+
Do not add robot-control commands as slash commands, and do not add app-management commands as bare commands.
|
|
125
104
|
|
|
126
105
|
**Special cases** (accepted both with and without `/`):
|
|
127
106
|
`exit`, `quit`, `q` — exit the app
|
|
128
107
|
|
|
129
|
-
|
|
108
|
+
**Headless commands** (`asher <command>`) are a parallel registry in `asher/headless.py`: same robot actions, no Textual, plain-string + JSON output. Slash commands have no headless equivalent — they configure the TUI, which isn't running. A robot command worth scripting should exist in both registries; the shared logic lives in `RobotAdapter`, not in either command class.
|
|
109
|
+
|
|
110
|
+
> If you add a command, update the tables in `README.md` and the list in `asher/slash-commands/__init__.py`. If it's a robot command, consider adding it to `COMMANDS` in `asher/headless.py` too.
|
|
130
111
|
|
|
131
112
|
## Architecture
|
|
132
113
|
|
|
@@ -175,17 +156,15 @@ LoginScreen (ModalScreen) — available in auth.py but not the primary auth path
|
|
|
175
156
|
|
|
176
157
|
## Robot compatibility
|
|
177
158
|
|
|
178
|
-
pylitterbot auto-detects robot type. Commands that differ per model are handled by `RobotAdapter` subclasses in `robot_adapters.py` — `make_adapter(robot)` returns the right one based on `type(robot).__name__`. Status-bar reads use `getattr(..., default)` for graceful degradation on older models.
|
|
159
|
+
pylitterbot auto-detects robot type. Commands that differ per model are handled by `RobotAdapter` subclasses in `robot_adapters.py` — `make_adapter(robot)` returns the right one based on `type(robot).__name__`. Status-bar reads use `getattr(..., default)` for graceful degradation on older models. See the `pylitterbot-ref` skill for the confirmed API surface.
|
|
179
160
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
- `
|
|
185
|
-
-
|
|
186
|
-
- `
|
|
187
|
-
- `robot.set_night_light_brightness(int)` or `robot.set_night_light_mode(NightLightMode)`
|
|
188
|
-
- `robot.get_activity_history(limit=int)` → list of `Activity` objects with `.timestamp` and `.action` (`LitterBoxStatus` enum)
|
|
161
|
+
## Colour
|
|
162
|
+
|
|
163
|
+
Every colour comes from `asher/theme.py` (Catppuccin Mocha). Reference the **semantic roles** (`theme.MUTED`, `theme.DANGER`, …), not the raw swatches (`theme.OVERLAY0`) and never a hex literal — a re-flavour then only repoints the roles.
|
|
164
|
+
|
|
165
|
+
- **Rich styles:** `style=theme.ACCENT`, or `style=f"bold {theme.ACCENT}"`. Prefer building `Text` objects with explicit styles over `Text.from_markup` with inline colours.
|
|
166
|
+
- **`ui/style.tcss`:** use the `$asher-*` variables; `AsherApp.get_css_variables()` supplies them.
|
|
167
|
+
- **Inline `CSS`/`DEFAULT_CSS` on a Screen or Widget:** wrap the block in `theme.apply(...)`, which bakes the `$asher-*` values in at class-definition time. A screen mounted on a host app that isn't `AsherApp` (as the Pilot tests do) would otherwise fail to parse.
|
|
189
168
|
|
|
190
169
|
## Code comments
|
|
191
170
|
|
|
@@ -213,6 +192,8 @@ Commands that need a confirmed cloud state before showing a result (e.g. sleep/w
|
|
|
213
192
|
|
|
214
193
|
**Add a slash command:** create a class inheriting `SlashCommand` (sets `prefix = "/"`), implement `async def run(self, app, args)`, register it, and document in `asher/slash-commands/__init__.py`.
|
|
215
194
|
|
|
195
|
+
**Add a headless command:** write `async def _my_command(session, args) -> Result` in `asher/headless.py` and add a `HeadlessCommand(...)` entry to `COMMANDS`. The argparse subparser is generated from the registry — nothing to add in `__main__.py`. Build the `Result` with `_rows()` (read commands) or `_outcome()` (actions) so text and JSON stay in step, and raise `CommandError` rather than printing.
|
|
196
|
+
|
|
216
197
|
**Change poll interval:** `self.set_interval(300, ...)` in `on_mount`.
|
|
217
198
|
|
|
218
199
|
**Add a new cat state:** add entry to `CATS` dict in `asher/cats.py` (str for static, list[str] for animated), then call `_set_cat("name", "label")`.
|
|
@@ -221,18 +202,16 @@ Commands that need a confirmed cloud state before showing a result (e.g. sleep/w
|
|
|
221
202
|
|
|
222
203
|
## Dev workflow
|
|
223
204
|
|
|
224
|
-
|
|
225
|
-
uv sync # install all deps (including dev group)
|
|
226
|
-
uv run poe dev # run with CSS hot reload (textual --dev)
|
|
227
|
-
uv run poe watch # run with Python auto-restart on file change (watchfiles)
|
|
228
|
-
uv run poe test # run test suite
|
|
229
|
-
uv run poe check # ruff + mypy + pytest (same as CI)
|
|
230
|
-
uv run poe fix # auto-fix ruff issues
|
|
231
|
-
uv run poe security # bandit scan (same config the Bandit workflow uses)
|
|
232
|
-
```
|
|
205
|
+
See `[tool.poe.tasks]` in `pyproject.toml` for the full task list (`uv run poe <task>`).
|
|
233
206
|
|
|
234
207
|
Pre-push hook (`.githooks/pre-push`) runs: ruff check → ruff format --check → mypy. Tests are not in the hook — run them manually.
|
|
235
208
|
|
|
209
|
+
## Releasing
|
|
210
|
+
|
|
211
|
+
Follow [README § Releasing](README.md#releasing) exactly, in order: `uv run poe changelog-release X.Y.Z` and commit the result → `uv run bump-my-version bump <part>` (this commits **and tags**) → `git push && git push --tags` → only then cut and push `release/X.Y.Z`.
|
|
212
|
+
|
|
213
|
+
Regenerate with `changelog-release X.Y.Z`, never plain `changelog` — the workflow lifts the `## [X.Y.Z]` section out of the committed `CHANGELOG.md` verbatim for the GitHub Release body, and a file still saying `## [Unreleased]` fails the release job. That extraction is also why hand-refinements to a section survive into the release notes, and why re-running either changelog task afterwards silently discards them: regeneration always re-derives from commits. Refine last.
|
|
214
|
+
|
|
236
215
|
## Testing notes
|
|
237
216
|
|
|
238
217
|
- Pilot-based integration tests use `app.run_test()` with `await pilot.pause()` before querying widgets
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asher-cli
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Terminal dashboard for Litter Robot (LR3/LR4/LR5) via the Whisker cloud API
|
|
5
5
|
Project-URL: Homepage, https://github.com/karanshukla/asher-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/karanshukla/asher-cli
|
|
@@ -75,7 +75,8 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
|
|
|
75
75
|
- Slash commands for app management: `/login`, `/logout`, `/robots`, `/robot <index|name>`, `/pets`, `/pet <index|name>`, `/cat on|off|colour <hex>`, `/refresh [seconds|off]`, `/config`, `/notify on|off|sound on|off|test`, `/version`, `/mcp on|off|status`, `/exit`
|
|
76
76
|
- Slash-command tab completion — type `/` and a Claude Code-style overlay lists matching commands; `↑`/`↓` to move, `Tab` or `Enter` to accept, `Esc` to dismiss
|
|
77
77
|
- Inline ghost-text completion for bare commands — type a prefix (`cle`) and the rest (`an`) appears greyed; `Tab` or `→` to accept → `clean`
|
|
78
|
-
- Headless
|
|
78
|
+
- Headless mode — every robot command also runs without the TUI (`asher status`, `asher clean`, `asher night-light auto`, `asher export 7`), with `--json` output and documented exit codes for cron / Task Scheduler / SSH
|
|
79
|
+
- Catppuccin Mocha throughout — one palette in `asher/theme.py` drives the TUI stylesheet and every Rich style
|
|
79
80
|
- Cat animation panel that reacts to robot state
|
|
80
81
|
- Command history (↑/↓ arrows)
|
|
81
82
|
- Real-time updates via WebSocket; 5-minute poll fallback
|
|
@@ -177,38 +178,61 @@ LITTER_ROBOT_PASSWORD=yourpassword
|
|
|
177
178
|
|
|
178
179
|
**Keyboard shortcuts:** `Ctrl+L` clears the log, `Ctrl+C` quits. While typing a `/` slash command, `↑`/`↓` move through completions, `Tab` or `Enter` accepts, `Esc` dismisses. While typing a bare command, a greyed ghost suggestion appears — `Tab` or `→` accepts it.
|
|
179
180
|
|
|
180
|
-
### Headless
|
|
181
|
+
### Headless commands (cron / Task Scheduler / SSH)
|
|
181
182
|
|
|
182
|
-
|
|
183
|
+
Every robot action the dashboard offers is also a subcommand that runs **without launching the TUI** — `asher` with no arguments still opens the interactive dashboard.
|
|
183
184
|
|
|
184
185
|
```bash
|
|
185
|
-
asher
|
|
186
|
-
asher
|
|
187
|
-
asher
|
|
186
|
+
asher status at-a-glance state
|
|
187
|
+
asher info model, serial, firmware, settings
|
|
188
|
+
asher clean start a clean cycle
|
|
189
|
+
asher night-light auto set night light mode
|
|
190
|
+
asher wait-time 7 set the clean-cycle wait time
|
|
191
|
+
asher history 20 recent activity, newest first
|
|
192
|
+
asher insight 7 cycle-usage statistics
|
|
193
|
+
asher export 7 --output ~/hist.csv activity history to CSV
|
|
188
194
|
```
|
|
189
195
|
|
|
190
|
-
|
|
196
|
+
`asher --help` lists every command; `asher <command> --help` shows its arguments. The full set: `status`, `info`, `robots`, `pets`, `history`, `insight`, `sleep-schedule`, `clean`, `lock`, `unlock`, `sleep`, `wake`, `night-light`, `night-light-brightness`, `panel-brightness`, `wait-time`, `power`, `rename`, `privacy`, `volume`, `camera-audio`, `drawer-reset`, `export`.
|
|
197
|
+
|
|
198
|
+
Two flags apply to all of them:
|
|
199
|
+
|
|
200
|
+
- `--robot <index|name>` — an index or a partial, case-insensitive name (defaults to your saved preferred robot, else the first)
|
|
201
|
+
- `--json` — machine-readable output instead of aligned text, for piping into `jq`
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
asher status --json | jq -r '.drawer'
|
|
205
|
+
asher history 50 --json | jq '[.events[] | select(.event | startswith("Cat"))] | length'
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Credentials use the same keyring → `.env` priority as the TUI, but with **no interactive login prompt** — a scheduled task can't type a password, so sign in once with `/login` first.
|
|
191
209
|
|
|
192
210
|
Exit codes for scripting:
|
|
193
211
|
|
|
194
212
|
| Code | Meaning |
|
|
195
213
|
|---|---|
|
|
196
|
-
| `0` |
|
|
214
|
+
| `0` | command succeeded |
|
|
197
215
|
| `1` | no credentials found (keyring or `.env`) |
|
|
198
216
|
| `2` | connection or API failure |
|
|
199
217
|
| `3` | failed to write the CSV (permissions, disk full) |
|
|
200
218
|
| `4` | `--robot` matched no robot on the account |
|
|
219
|
+
| `5` | bad argument, or the robot/cloud rejected the command |
|
|
201
220
|
|
|
202
221
|
```bash
|
|
203
222
|
# crontab — nightly export at 03:00
|
|
204
|
-
0 3 * * * /usr/bin/env asher
|
|
223
|
+
0 3 * * * /usr/bin/env asher export 7 --output /home/me/litter-history.csv >> /var/log/asher-export.log 2>&1
|
|
224
|
+
|
|
225
|
+
# alert when the drawer passes 85%
|
|
226
|
+
0 * * * * [ "$(asher status --json | jq -r '.drawer' | tr -d %)" -gt 85 ] && notify-send "Litter drawer full"
|
|
205
227
|
```
|
|
206
228
|
|
|
207
229
|
```powershell
|
|
208
230
|
# Windows Task Scheduler action
|
|
209
|
-
asher.exe
|
|
231
|
+
asher.exe export 7 --output C:\Users\me\litter-history.csv
|
|
210
232
|
```
|
|
211
233
|
|
|
234
|
+
`asher --export [days]` remains as a deprecated alias for `asher export [days]`, so existing cron entries keep working.
|
|
235
|
+
|
|
212
236
|
## Configuration
|
|
213
237
|
|
|
214
238
|
Runtime settings persist across restarts in `~/.asher-cli/config.json`, so you don't have to re-apply `/refresh 60`, `/cat colour #ff79c6`, or `/pet 1` every launch. The file is auto-created on first change and holds six non-secret UI preferences:
|
|
@@ -227,10 +251,11 @@ Credentials and the preferred-robot serial stay in the OS keyring; `.env` vars s
|
|
|
227
251
|
## Releasing
|
|
228
252
|
|
|
229
253
|
```bash
|
|
230
|
-
# 1.
|
|
231
|
-
|
|
254
|
+
# 1. file the pending commits under the version you're about to cut, refine the
|
|
255
|
+
# prose if a squashed commit needs unpacking, then commit it
|
|
256
|
+
uv run poe changelog-release X.Y.Z
|
|
232
257
|
git add CHANGELOG.md
|
|
233
|
-
git commit -m "docs(changelog):
|
|
258
|
+
git commit -m "docs(changelog): cut vX.Y.Z"
|
|
234
259
|
|
|
235
260
|
# 2. bump version, commit, and tag in one step, then push with tags
|
|
236
261
|
uv run bump-my-version bump patch # 0.2.0 → 0.2.1
|
|
@@ -240,7 +265,7 @@ uv run bump-my-version bump major # 0.2.0 → 1.0.0
|
|
|
240
265
|
git push && git push --tags
|
|
241
266
|
|
|
242
267
|
# 3. push the release branch to trigger PyPI publish (OIDC) + GitHub Release
|
|
243
|
-
# (release notes
|
|
268
|
+
# (release notes = the `## [X.Y.Z]` section of CHANGELOG.md, verbatim)
|
|
244
269
|
git checkout -b release/X.Y.Z
|
|
245
270
|
git push origin release/X.Y.Z
|
|
246
271
|
```
|
|
@@ -250,6 +275,17 @@ conventional commits — see [`cliff.toml`](cliff.toml). Commit messages must us
|
|
|
250
275
|
`feat:`/`fix:`/`docs:`/etc. prefixes (optionally scoped, e.g. `feat(robot):`)
|
|
251
276
|
to appear in the changelog.
|
|
252
277
|
|
|
278
|
+
Use `poe changelog-release X.Y.Z` — not plain `poe changelog` — when cutting a
|
|
279
|
+
release. Both regenerate the whole file, but only the former files the pending
|
|
280
|
+
commits under `## [X.Y.Z]` instead of `## [Unreleased]`, and the release
|
|
281
|
+
workflow extracts exactly that section for the GitHub Release body. It fails
|
|
282
|
+
loudly if the section is missing, so the notes can never come out stale.
|
|
283
|
+
|
|
284
|
+
Because the workflow reads the committed file rather than re-deriving from
|
|
285
|
+
commits, any hand-refinement you make to a section survives into the release
|
|
286
|
+
notes — but re-running either task afterwards regenerates from commits and
|
|
287
|
+
discards it. Refine last, right before committing.
|
|
288
|
+
|
|
253
289
|
## Troubleshooting
|
|
254
290
|
|
|
255
291
|
**`asher` not found after `pip install asher-cli`**
|
|
@@ -24,7 +24,8 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
|
|
|
24
24
|
- Slash commands for app management: `/login`, `/logout`, `/robots`, `/robot <index|name>`, `/pets`, `/pet <index|name>`, `/cat on|off|colour <hex>`, `/refresh [seconds|off]`, `/config`, `/notify on|off|sound on|off|test`, `/version`, `/mcp on|off|status`, `/exit`
|
|
25
25
|
- Slash-command tab completion — type `/` and a Claude Code-style overlay lists matching commands; `↑`/`↓` to move, `Tab` or `Enter` to accept, `Esc` to dismiss
|
|
26
26
|
- Inline ghost-text completion for bare commands — type a prefix (`cle`) and the rest (`an`) appears greyed; `Tab` or `→` to accept → `clean`
|
|
27
|
-
- Headless
|
|
27
|
+
- Headless mode — every robot command also runs without the TUI (`asher status`, `asher clean`, `asher night-light auto`, `asher export 7`), with `--json` output and documented exit codes for cron / Task Scheduler / SSH
|
|
28
|
+
- Catppuccin Mocha throughout — one palette in `asher/theme.py` drives the TUI stylesheet and every Rich style
|
|
28
29
|
- Cat animation panel that reacts to robot state
|
|
29
30
|
- Command history (↑/↓ arrows)
|
|
30
31
|
- Real-time updates via WebSocket; 5-minute poll fallback
|
|
@@ -126,38 +127,61 @@ LITTER_ROBOT_PASSWORD=yourpassword
|
|
|
126
127
|
|
|
127
128
|
**Keyboard shortcuts:** `Ctrl+L` clears the log, `Ctrl+C` quits. While typing a `/` slash command, `↑`/`↓` move through completions, `Tab` or `Enter` accepts, `Esc` dismisses. While typing a bare command, a greyed ghost suggestion appears — `Tab` or `→` accepts it.
|
|
128
129
|
|
|
129
|
-
### Headless
|
|
130
|
+
### Headless commands (cron / Task Scheduler / SSH)
|
|
130
131
|
|
|
131
|
-
|
|
132
|
+
Every robot action the dashboard offers is also a subcommand that runs **without launching the TUI** — `asher` with no arguments still opens the interactive dashboard.
|
|
132
133
|
|
|
133
134
|
```bash
|
|
134
|
-
asher
|
|
135
|
-
asher
|
|
136
|
-
asher
|
|
135
|
+
asher status at-a-glance state
|
|
136
|
+
asher info model, serial, firmware, settings
|
|
137
|
+
asher clean start a clean cycle
|
|
138
|
+
asher night-light auto set night light mode
|
|
139
|
+
asher wait-time 7 set the clean-cycle wait time
|
|
140
|
+
asher history 20 recent activity, newest first
|
|
141
|
+
asher insight 7 cycle-usage statistics
|
|
142
|
+
asher export 7 --output ~/hist.csv activity history to CSV
|
|
137
143
|
```
|
|
138
144
|
|
|
139
|
-
|
|
145
|
+
`asher --help` lists every command; `asher <command> --help` shows its arguments. The full set: `status`, `info`, `robots`, `pets`, `history`, `insight`, `sleep-schedule`, `clean`, `lock`, `unlock`, `sleep`, `wake`, `night-light`, `night-light-brightness`, `panel-brightness`, `wait-time`, `power`, `rename`, `privacy`, `volume`, `camera-audio`, `drawer-reset`, `export`.
|
|
146
|
+
|
|
147
|
+
Two flags apply to all of them:
|
|
148
|
+
|
|
149
|
+
- `--robot <index|name>` — an index or a partial, case-insensitive name (defaults to your saved preferred robot, else the first)
|
|
150
|
+
- `--json` — machine-readable output instead of aligned text, for piping into `jq`
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
asher status --json | jq -r '.drawer'
|
|
154
|
+
asher history 50 --json | jq '[.events[] | select(.event | startswith("Cat"))] | length'
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Credentials use the same keyring → `.env` priority as the TUI, but with **no interactive login prompt** — a scheduled task can't type a password, so sign in once with `/login` first.
|
|
140
158
|
|
|
141
159
|
Exit codes for scripting:
|
|
142
160
|
|
|
143
161
|
| Code | Meaning |
|
|
144
162
|
|---|---|
|
|
145
|
-
| `0` |
|
|
163
|
+
| `0` | command succeeded |
|
|
146
164
|
| `1` | no credentials found (keyring or `.env`) |
|
|
147
165
|
| `2` | connection or API failure |
|
|
148
166
|
| `3` | failed to write the CSV (permissions, disk full) |
|
|
149
167
|
| `4` | `--robot` matched no robot on the account |
|
|
168
|
+
| `5` | bad argument, or the robot/cloud rejected the command |
|
|
150
169
|
|
|
151
170
|
```bash
|
|
152
171
|
# crontab — nightly export at 03:00
|
|
153
|
-
0 3 * * * /usr/bin/env asher
|
|
172
|
+
0 3 * * * /usr/bin/env asher export 7 --output /home/me/litter-history.csv >> /var/log/asher-export.log 2>&1
|
|
173
|
+
|
|
174
|
+
# alert when the drawer passes 85%
|
|
175
|
+
0 * * * * [ "$(asher status --json | jq -r '.drawer' | tr -d %)" -gt 85 ] && notify-send "Litter drawer full"
|
|
154
176
|
```
|
|
155
177
|
|
|
156
178
|
```powershell
|
|
157
179
|
# Windows Task Scheduler action
|
|
158
|
-
asher.exe
|
|
180
|
+
asher.exe export 7 --output C:\Users\me\litter-history.csv
|
|
159
181
|
```
|
|
160
182
|
|
|
183
|
+
`asher --export [days]` remains as a deprecated alias for `asher export [days]`, so existing cron entries keep working.
|
|
184
|
+
|
|
161
185
|
## Configuration
|
|
162
186
|
|
|
163
187
|
Runtime settings persist across restarts in `~/.asher-cli/config.json`, so you don't have to re-apply `/refresh 60`, `/cat colour #ff79c6`, or `/pet 1` every launch. The file is auto-created on first change and holds six non-secret UI preferences:
|
|
@@ -176,10 +200,11 @@ Credentials and the preferred-robot serial stay in the OS keyring; `.env` vars s
|
|
|
176
200
|
## Releasing
|
|
177
201
|
|
|
178
202
|
```bash
|
|
179
|
-
# 1.
|
|
180
|
-
|
|
203
|
+
# 1. file the pending commits under the version you're about to cut, refine the
|
|
204
|
+
# prose if a squashed commit needs unpacking, then commit it
|
|
205
|
+
uv run poe changelog-release X.Y.Z
|
|
181
206
|
git add CHANGELOG.md
|
|
182
|
-
git commit -m "docs(changelog):
|
|
207
|
+
git commit -m "docs(changelog): cut vX.Y.Z"
|
|
183
208
|
|
|
184
209
|
# 2. bump version, commit, and tag in one step, then push with tags
|
|
185
210
|
uv run bump-my-version bump patch # 0.2.0 → 0.2.1
|
|
@@ -189,7 +214,7 @@ uv run bump-my-version bump major # 0.2.0 → 1.0.0
|
|
|
189
214
|
git push && git push --tags
|
|
190
215
|
|
|
191
216
|
# 3. push the release branch to trigger PyPI publish (OIDC) + GitHub Release
|
|
192
|
-
# (release notes
|
|
217
|
+
# (release notes = the `## [X.Y.Z]` section of CHANGELOG.md, verbatim)
|
|
193
218
|
git checkout -b release/X.Y.Z
|
|
194
219
|
git push origin release/X.Y.Z
|
|
195
220
|
```
|
|
@@ -199,6 +224,17 @@ conventional commits — see [`cliff.toml`](cliff.toml). Commit messages must us
|
|
|
199
224
|
`feat:`/`fix:`/`docs:`/etc. prefixes (optionally scoped, e.g. `feat(robot):`)
|
|
200
225
|
to appear in the changelog.
|
|
201
226
|
|
|
227
|
+
Use `poe changelog-release X.Y.Z` — not plain `poe changelog` — when cutting a
|
|
228
|
+
release. Both regenerate the whole file, but only the former files the pending
|
|
229
|
+
commits under `## [X.Y.Z]` instead of `## [Unreleased]`, and the release
|
|
230
|
+
workflow extracts exactly that section for the GitHub Release body. It fails
|
|
231
|
+
loudly if the section is missing, so the notes can never come out stale.
|
|
232
|
+
|
|
233
|
+
Because the workflow reads the committed file rather than re-deriving from
|
|
234
|
+
commits, any hand-refinement you make to a section survives into the release
|
|
235
|
+
notes — but re-running either task afterwards regenerates from commits and
|
|
236
|
+
discards it. Refine last, right before committing.
|
|
237
|
+
|
|
202
238
|
## Troubleshooting
|
|
203
239
|
|
|
204
240
|
**`asher` not found after `pip install asher-cli`**
|