asher-cli 1.4.0__tar.gz → 1.4.1__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.4.0 → asher_cli-1.4.1}/.claude/skills/mcp-bridge/SKILL.md +3 -3
- {asher_cli-1.4.0 → asher_cli-1.4.1}/.claude/skills/pylitterbot-ref/SKILL.md +4 -2
- {asher_cli-1.4.0 → asher_cli-1.4.1}/.github/workflows/bandit.yml +2 -2
- {asher_cli-1.4.0 → asher_cli-1.4.1}/.github/workflows/ci.yml +2 -2
- {asher_cli-1.4.0 → asher_cli-1.4.1}/.github/workflows/coverage.yml +1 -1
- {asher_cli-1.4.0 → asher_cli-1.4.1}/.github/workflows/dependency-automerge.yml +1 -1
- {asher_cli-1.4.0 → asher_cli-1.4.1}/.github/workflows/release.yml +1 -1
- {asher_cli-1.4.0 → asher_cli-1.4.1}/CHANGELOG.md +29 -1
- {asher_cli-1.4.0 → asher_cli-1.4.1}/CLAUDE.md +70 -49
- {asher_cli-1.4.0 → asher_cli-1.4.1}/PKG-INFO +4 -4
- {asher_cli-1.4.0 → asher_cli-1.4.1}/README.md +1 -1
- {asher_cli-1.4.0 → asher_cli-1.4.1}/asher/__main__.py +6 -6
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/core}/config.py +1 -1
- asher_cli-1.4.1/asher/core/credentials.py +185 -0
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/core}/export.py +15 -35
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/core}/faults.py +1 -1
- asher_cli-1.4.1/asher/desktop/__init__.py +0 -0
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/desktop}/autostart.py +1 -1
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/desktop}/daemon.py +5 -4
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/desktop}/launcher.py +3 -3
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/desktop}/notifications.py +3 -3
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/desktop}/tray.py +4 -3
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/desktop}/watcher.py +10 -10
- {asher_cli-1.4.0 → asher_cli-1.4.1}/asher/headless.py +39 -16
- asher_cli-1.4.1/asher/mcp/__init__.py +0 -0
- asher_cli-1.4.0/asher/mcp_config.py → asher_cli-1.4.1/asher/mcp/config.py +1 -1
- asher_cli-1.4.1/asher/robot/__init__.py +0 -0
- asher_cli-1.4.0/asher/robot_adapters.py → asher_cli-1.4.1/asher/robot/adapters.py +2 -2
- asher_cli-1.4.1/asher/tui/__init__.py +0 -0
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/tui}/app.py +6 -6
- asher_cli-1.4.1/asher/tui/commands/__init__.py +303 -0
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/tui}/commands/base.py +4 -0
- asher_cli-1.4.1/asher/tui/commands/builtin.py +44 -0
- asher_cli-1.4.1/asher/tui/commands/lr5.py +110 -0
- asher_cli-1.4.1/asher/tui/commands/registry.py +80 -0
- asher_cli-1.4.1/asher/tui/commands/robot.py +812 -0
- asher_cli-1.4.1/asher/tui/commands/slash.py +546 -0
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/tui}/completion.py +2 -2
- asher_cli-1.4.1/asher/tui/connection.py +172 -0
- asher_cli-1.4.0/asher/history_view.py → asher_cli-1.4.1/asher/tui/history.py +2 -2
- asher_cli-1.4.0/asher/auth.py → asher_cli-1.4.1/asher/tui/loginscreen.py +1 -1
- asher_cli-1.4.0/asher/monitoring/__init__.py → asher_cli-1.4.1/asher/tui/monitoring.py +7 -7
- asher_cli-1.4.0/asher/ui/__init__.py → asher_cli-1.4.1/asher/tui/ui.py +24 -19
- asher_cli-1.4.1/docs/CONTRIBUTING.md +204 -0
- asher_cli-1.4.1/docs/ROADMAP.md +406 -0
- asher_cli-1.4.1/docs/roadmap-archive/README.md +40 -0
- asher_cli-1.4.1/docs/roadmap-archive/credentials.md +38 -0
- asher_cli-1.4.1/docs/roadmap-archive/e2e-pilot.md +55 -0
- asher_cli-1.4.1/docs/roadmap-archive/lr5-features.md +37 -0
- asher_cli-1.4.1/docs/roadmap-archive/multi-pet.md +10 -0
- asher_cli-1.4.1/docs/roadmap-archive/robot-commands.md +92 -0
- asher_cli-1.4.1/docs/roadmap-archive/runtime-slash-commands.md +79 -0
- asher_cli-1.4.1/docs/roadmap-archive/slash-command-spec.md +41 -0
- asher_cli-1.4.1/docs/roadmap-archive/sleep-schedule.md +60 -0
- asher_cli-1.4.1/docs/roadmap-archive/ui-ux.md +185 -0
- asher_cli-1.4.1/docs/roadmap-archive/version-display.md +37 -0
- asher_cli-1.4.1/docs/roadmap-archive/websocket-updates.md +22 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/pyproject.toml +5 -6
- asher_cli-1.4.1/tests/__init__.py +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_activity_labels.py +3 -3
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_app_pilot.py +32 -12
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_auth.py +3 -3
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_auth_pilot.py +2 -2
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_autostart.py +3 -3
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_cats.py +2 -2
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_commands_pilot.py +10 -10
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_completion.py +4 -4
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_config.py +34 -34
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_connection_mixin.py +2 -2
- asher_cli-1.4.0/tests/test_connection.py → asher_cli-1.4.1/tests/test_credentials.py +82 -89
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_daemon.py +28 -25
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_desktoptheme.py +2 -2
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_export.py +47 -30
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_faults.py +2 -2
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_headless.py +8 -8
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_history_view.py +6 -6
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_launcher.py +2 -2
- asher_cli-1.4.1/tests/test_layering.py +59 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_lr5_commands.py +2 -2
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_mcp_bridge.py +4 -4
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_mcp_command.py +21 -21
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_mcp_config.py +15 -15
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_missing_robot_commands.py +4 -4
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_monitoring.py +13 -13
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_new_commands_pilot.py +17 -8
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_notifications.py +2 -2
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_robot_adapters.py +1 -1
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_tray.py +8 -7
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_ui.py +6 -6
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_updates.py +3 -3
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_version_command.py +3 -3
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/test_watcher.py +15 -13
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/testhelpers.py +3 -3
- {asher_cli-1.4.0 → asher_cli-1.4.1}/uv.lock +1364 -1302
- asher_cli-1.4.0/.github/workflows/claude-code-review.yml +0 -44
- asher_cli-1.4.0/.github/workflows/claude.yml +0 -50
- asher_cli-1.4.0/asher/commands/__init__.py +0 -1816
- asher_cli-1.4.0/asher/connection/__init__.py +0 -355
- asher_cli-1.4.0/asher/slash-commands/__init__.py +0 -46
- asher_cli-1.4.0/docs/ROADMAP.md +0 -1705
- asher_cli-1.4.0/docs/roadmap-archive/README.md +0 -16
- asher_cli-1.4.0/test.py +0 -32
- {asher_cli-1.4.0 → asher_cli-1.4.1}/.claude/hooks/block-env.py +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/.claude/settings.json +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/.claude/skills/textual/SKILL.md +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/.env.example +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/.githooks/pre-push +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/.github/FUNDING.yml +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/.github/pull_request_template.md +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/.gitignore +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/.vscode/launch.json +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/.vscode/settings.json +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/.vscode/tasks.json +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/CODEOWNERS +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/LICENSE +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/app.py +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/asher/__init__.py +0 -0
- {asher_cli-1.4.0/tests → asher_cli-1.4.1/asher/core}/__init__.py +0 -0
- /asher_cli-1.4.0/asher/activity_labels.py → /asher_cli-1.4.1/asher/core/activity.py +0 -0
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/core}/constants.py +0 -0
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/core}/helpers.py +0 -0
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/core}/theme.py +0 -0
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/core}/updates.py +0 -0
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/desktop}/desktoptheme.py +0 -0
- /asher_cli-1.4.0/asher/mcp_bridge.py → /asher_cli-1.4.1/asher/mcp/bridge.py +0 -0
- /asher_cli-1.4.0/asher/robot_protocol.py → /asher_cli-1.4.1/asher/robot/protocol.py +0 -0
- {asher_cli-1.4.0/asher → asher_cli-1.4.1/asher/tui}/cats.py +0 -0
- /asher_cli-1.4.0/asher/login_flow.py → /asher_cli-1.4.1/asher/tui/loginflow.py +0 -0
- {asher_cli-1.4.0/asher/ui → asher_cli-1.4.1/asher/tui}/style.tcss +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/cliff.toml +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/docs/roadmap-archive/cat-panel-badges.md +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/docs/roadmap-archive/config-persistence.md +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/docs/roadmap-archive/desktop-notifications.md +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/docs/roadmap-archive/fault-monitoring.md +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/docs/roadmap-archive/headless-export.md +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/docs/roadmap-archive/history-export.md +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/docs/roadmap-archive/tab-completion.md +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/renovate.json +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/requirements.txt +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/tests/conftest.py +0 -0
- {asher_cli-1.4.0 → asher_cli-1.4.1}/watchrun.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mcp-bridge
|
|
3
|
-
description: How asher-cli's /mcp command and the asher-mcp-launch credential bridge work. Load before changing asher/
|
|
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
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,8 +8,8 @@ user-invocable: false
|
|
|
8
8
|
|
|
9
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
10
|
|
|
11
|
-
- `/mcp on|off|status` (in `asher/commands/
|
|
11
|
+
- `/mcp on|off|status` (in `asher/tui/commands/slash.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
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/
|
|
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
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
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.
|
|
@@ -6,7 +6,7 @@ user-invocable: false
|
|
|
6
6
|
|
|
7
7
|
# pylitterbot Confirmed API Surface
|
|
8
8
|
|
|
9
|
-
Load this skill whenever working on `asher/commands/`, `asher/monitoring
|
|
9
|
+
Load this skill whenever working on `asher/tui/commands/`, `asher/tui/monitoring.py`, `asher/robot/adapters.py`, or anything that touches `robot.*`.
|
|
10
10
|
|
|
11
11
|
## Robot attributes (LR4 confirmed)
|
|
12
12
|
|
|
@@ -32,6 +32,8 @@ await robot.set_panel_lockout(enabled: bool)
|
|
|
32
32
|
await robot.set_night_light_brightness(brightness: int) # or:
|
|
33
33
|
await robot.set_night_light_mode(mode: NightLightMode)
|
|
34
34
|
await robot.get_activity_history(limit: int) # -> list[Activity]
|
|
35
|
+
# LR4 only (2025.6.6+): start= reaches past the ~6 days the API returns without it
|
|
36
|
+
await robot.get_activity_history(limit: int, start: datetime | None = None)
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
## Activity objects
|
|
@@ -54,7 +56,7 @@ model_name = type(robot).__name__ # "LitterRobot4", "LitterRobot3", etc.
|
|
|
54
56
|
| `set_sleep_mode()` | Simple on/off | Per-weekday schedule | TBD |
|
|
55
57
|
| Night light | `set_night_light_brightness(int)` | `set_night_light_mode(NightLightMode)` | TBD |
|
|
56
58
|
|
|
57
|
-
Use `asher/
|
|
59
|
+
Use `asher/robot/adapters.py` for all model-specific dispatch — do not branch on `type(robot).__name__` inline in command handlers.
|
|
58
60
|
|
|
59
61
|
## Timing / cloud queuing gotcha
|
|
60
62
|
|
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
runs-on: ubuntu-latest
|
|
32
32
|
steps:
|
|
33
33
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
34
|
-
- uses: astral-sh/setup-uv@
|
|
34
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
35
35
|
|
|
36
36
|
- name: Bandit scan
|
|
37
37
|
# --exit-zero: findings must not fail the job, or the upload step below
|
|
@@ -41,7 +41,7 @@ jobs:
|
|
|
41
41
|
-c pyproject.toml -r . -f sarif -o bandit.sarif --exit-zero
|
|
42
42
|
|
|
43
43
|
- name: Upload SARIF to code scanning
|
|
44
|
-
uses: github/codeql-action/upload-sarif@
|
|
44
|
+
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
|
45
45
|
with:
|
|
46
46
|
sarif_file: bandit.sarif
|
|
47
47
|
category: bandit
|
|
@@ -19,7 +19,7 @@ jobs:
|
|
|
19
19
|
runs-on: ubuntu-latest
|
|
20
20
|
steps:
|
|
21
21
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
22
|
-
- uses: astral-sh/setup-uv@
|
|
22
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
23
23
|
- run: uv sync --no-default-groups --group dev
|
|
24
24
|
- run: uv run ruff check .
|
|
25
25
|
- run: uv run ruff format --check .
|
|
@@ -35,6 +35,6 @@ jobs:
|
|
|
35
35
|
runs-on: ${{ matrix.os }}
|
|
36
36
|
steps:
|
|
37
37
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
38
|
-
- uses: astral-sh/setup-uv@
|
|
38
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
39
39
|
- run: uv sync --no-default-groups --group dev
|
|
40
40
|
- run: uv run pytest tests/ -v --tb=short
|
|
@@ -18,7 +18,7 @@ jobs:
|
|
|
18
18
|
runs-on: ubuntu-latest
|
|
19
19
|
steps:
|
|
20
20
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
21
|
-
- uses: astral-sh/setup-uv@
|
|
21
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
22
22
|
- run: uv sync --no-default-groups --group dev
|
|
23
23
|
- name: Run tests with coverage
|
|
24
24
|
run: uv run pytest tests/ --cov=asher --cov-report=lcov --cov-report=term-missing
|
|
@@ -16,7 +16,7 @@ jobs:
|
|
|
16
16
|
- name: Dependabot metadata
|
|
17
17
|
if: github.event.pull_request.user.login == 'dependabot[bot]'
|
|
18
18
|
id: dependabot-metadata
|
|
19
|
-
uses: dependabot/fetch-metadata@
|
|
19
|
+
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3
|
|
20
20
|
with:
|
|
21
21
|
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
|
22
22
|
|
|
@@ -20,7 +20,7 @@ jobs:
|
|
|
20
20
|
- name: Extract version from branch name
|
|
21
21
|
id: version
|
|
22
22
|
run: echo "version=${GITHUB_REF_NAME#release/}" >> "$GITHUB_OUTPUT"
|
|
23
|
-
- uses: astral-sh/setup-uv@
|
|
23
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
24
24
|
- run: uv build
|
|
25
25
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
26
26
|
with:
|
|
@@ -7,7 +7,35 @@ 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.4.
|
|
10
|
+
## [1.4.1] - 2026-10-01
|
|
11
|
+
|
|
12
|
+
### 🐛 Bug Fixes
|
|
13
|
+
|
|
14
|
+
- *(export)* Reach the full 30 days on the LR4
|
|
15
|
+
- *(tui)* Hang wrapped log lines under their message
|
|
16
|
+
|
|
17
|
+
### 🛡️ Security
|
|
18
|
+
|
|
19
|
+
- *(deps)* Refresh `uv.lock` to patch pyjwt and urllib3 CVEs (#101)
|
|
20
|
+
|
|
21
|
+
### ♻️ Refactor
|
|
22
|
+
|
|
23
|
+
- Group the asher package into core, robot, tui, desktop and mcp
|
|
24
|
+
|
|
25
|
+
### 📚 Documentation
|
|
26
|
+
|
|
27
|
+
- Trim the roadmap to what's left, move process docs to CONTRIBUTING
|
|
28
|
+
|
|
29
|
+
### 🧪 Tests
|
|
30
|
+
|
|
31
|
+
- *(layering)* Compare paths in posix form so the check passes on Windows
|
|
32
|
+
|
|
33
|
+
### 🔧 Miscellaneous
|
|
34
|
+
|
|
35
|
+
- Remove claude.yml
|
|
36
|
+
- Remove claude-code-review.yml
|
|
37
|
+
|
|
38
|
+
## [1.4.0] - 2026-09-09
|
|
11
39
|
|
|
12
40
|
### 🐛 Bug Fixes
|
|
13
41
|
|
|
@@ -20,41 +20,61 @@ asher # after: uv sync && uv run asher OR pip install -e .
|
|
|
20
20
|
|
|
21
21
|
```
|
|
22
22
|
asher/
|
|
23
|
-
__init__.py
|
|
24
|
-
app.py AsherApp class (thin orchestrator — composes mixins)
|
|
25
|
-
auth.py LoginScreen modal (ModalScreen[tuple[str,str]]) — available, not primary flow
|
|
26
|
-
helpers.py fmt_ago(), fmt_until(), drawer_bar(), ts(), robot_model(), status_text() + argument parsing shared by the TUI and headless surfaces: hex_colour(), parse_clock(), parse_day(), split_type_flag(), activity_type() (pure, testable)
|
|
27
|
-
constants.py STATUS_COLORS, CAT_PANEL_STATUS_LABELS, ACTIVITY_TYPES, ROBOT_MODELS
|
|
28
|
-
theme.py Catppuccin Mocha palette + semantic roles (BACKGROUND, MUTED, DANGER, …) + CSS_VARIABLES/apply() — the only place a hex literal belongs
|
|
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)
|
|
30
|
-
notifications.py desktop toast + audible alert façade — plyer first, then the platform's own tool (osascript/notify-send); fire/beep are always-safe no-ops on failure/headless
|
|
31
|
-
watcher.py background notification loop with no TUI — pure WatchState (robot snapshots → Alert list) + supervising watch() that reconnects with backoff, plus WatcherRunner (asyncio on a worker thread, for the tray)
|
|
32
|
-
daemon.py detached watcher process control — `start` pre-flights `connection.credentials_available()` (presence only, no network, so starting offline still works) because the watcher claims the pid file *before* authenticating and would otherwise report a pid for a process that stops a moment later — pid file/log in ~/.asher-cli, start/stop/status/run, cross-platform detach + liveness (os.kill would *terminate* on Windows)
|
|
33
|
-
tray.py optional pystray/Pillow system-tray icon over WatcherRunner; every path degrades to a headless watcher. Icon is a panel-toned silhouette + status dot — colour rides the badge, never the whole glyph
|
|
34
|
-
launcher.py open_app() — start the TUI in a new terminal from the tray (a detached tray has none): new console on Windows, Terminal.app via AppleScript on macOS, first installed emulator on Linux (desktop's own preferred)
|
|
35
|
-
desktoptheme.py panel_is_dark() — is the tray/menu-bar background dark? kdeglobals luma / gsettings / AppleInterfaceStyle / the Personalize registry keys, behind a TTL cache; every probe degrades to a fallback, never raises
|
|
36
|
-
autostart.py login items — AutostartBackend ABC + launchd/systemd-user/registry subclasses + backend() factory; all per-user, no admin rights, disable() removes exactly what enable() wrote
|
|
37
|
-
updates.py PyPI release check — read-only over HTTPS, once a day, reports only. Never installs (see its docstring for why that stays manual)
|
|
38
|
-
cats.py CATS dict (ASCII art)
|
|
39
|
-
login_flow.py LoginFlow state machine — inline email/password prompt in command bar
|
|
40
|
-
robot_protocol.py RobotProtocol structural Protocol for pylitterbot robot objects
|
|
41
|
-
robot_adapters.py RobotAdapter ABC + LR3/LR4/LR5 subclasses + make_adapter() factory
|
|
42
|
-
mcp_config.py Claude Desktop config read/write for the /mcp slash command
|
|
43
|
-
mcp_bridge.py asher-mcp-launch console script — keyring-backed pylitterbot MCP launcher
|
|
44
|
-
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)
|
|
45
|
-
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()
|
|
46
|
-
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()
|
|
47
|
-
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
|
|
48
|
-
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
|
|
49
23
|
__main__.py main() entry point — argparse subcommands (headless) vs no-args (TUI); `--export` kept as a deprecated alias
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
24
|
+
headless.py headless command surface for `asher <command>` — Session/Result/CommandError, the COMMANDS registry, run(), run_legacy_export(); plain strings only, no Textual, routes model differences through RobotAdapter
|
|
25
|
+
|
|
26
|
+
core/ shared domain + infrastructure — no Textual, imported by every surface
|
|
27
|
+
helpers.py fmt_ago(), fmt_until(), drawer_bar(), ts(), robot_model(), status_text() + argument parsing shared by the TUI and headless surfaces: hex_colour(), parse_clock(), parse_day(), split_type_flag(), activity_type() (pure, testable)
|
|
28
|
+
constants.py STATUS_COLORS, CAT_PANEL_STATUS_LABELS, ACTIVITY_TYPES, ROBOT_MODELS
|
|
29
|
+
theme.py Catppuccin Mocha palette + semantic roles (BACKGROUND, MUTED, DANGER, …) + CSS_VARIABLES/apply() — the only place a hex literal belongs
|
|
30
|
+
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)
|
|
31
|
+
credentials.py keyring + dev-mode .env credentials — load()/save()/delete(), load_token()/save_token(), preferred robot, password_login(), available() (presence only, no network), connect_headless(), HeadlessAuthError
|
|
32
|
+
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)
|
|
33
|
+
activity.py ACTION_LABELS + format_activity() — raw pylitterbot activity strings → readable (label, colour); shared by `history` and export
|
|
34
|
+
export.py shared activity-history CSV core + exit-code contract: build_history_csv(), resolve_dest(), resolve_robot(), parse_days(), EXIT_*, ExportError — the TUI `export` command and `asher export` both call build_history_csv()
|
|
35
|
+
updates.py PyPI release check — read-only over HTTPS, once a day, reports only. Never installs (see its docstring for why that stays manual)
|
|
36
|
+
|
|
37
|
+
robot/
|
|
38
|
+
protocol.py RobotProtocol structural Protocol for pylitterbot robot objects
|
|
39
|
+
adapters.py RobotAdapter ABC + LR3/LR4/LR5 subclasses + make_adapter() factory
|
|
40
|
+
|
|
41
|
+
tui/ the Textual dashboard — nothing outside tui/ imports it except __main__
|
|
42
|
+
app.py AsherApp class (thin orchestrator — composes mixins)
|
|
43
|
+
ui.py UIMixin — compose(), log helpers (_log_ok/err/warn/info, _log_stamped for hanging-indent lines), cat helpers
|
|
44
|
+
style.tcss the app stylesheet ($asher-* variables)
|
|
45
|
+
connection.py ConnectionMixin — _connect_worker, token/password connect, post-connect setup (credentials come from core.credentials)
|
|
46
|
+
monitoring.py MonitoringMixin — _poll_status_interval, _refresh_status
|
|
47
|
+
loginflow.py LoginFlow state machine — inline email/password prompt in command bar
|
|
48
|
+
loginscreen.py LoginScreen modal (ModalScreen[tuple[str,str]]) — available, not primary flow
|
|
49
|
+
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
|
|
50
|
+
history.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()
|
|
51
|
+
cats.py CATS dict (ASCII art)
|
|
52
|
+
commands/
|
|
53
|
+
__init__.py CommandsMixin — input routing, dispatch, history nav, completion keys, help rendering
|
|
54
|
+
base.py Command ABC, SlashCommand, CommandRegistry, HINT_* bar texts
|
|
55
|
+
registry.py _registry — every command instance, registered in one place
|
|
56
|
+
robot.py robot commands (clean, status, info, lock, …, sleep-schedule, history, export)
|
|
57
|
+
lr5.py LR5-only robot commands (privacy, volume, camera-audio, drawer-reset)
|
|
58
|
+
builtin.py help, clear, quit
|
|
59
|
+
slash.py slash commands (app management) + _persist()
|
|
60
|
+
|
|
61
|
+
desktop/ background + OS integration — never imports tui/
|
|
62
|
+
watcher.py background notification loop with no TUI — pure WatchState (robot snapshots → Alert list) + supervising watch() that reconnects with backoff, plus WatcherRunner (asyncio on a worker thread, for the tray)
|
|
63
|
+
daemon.py detached watcher process control — `start` pre-flights `credentials.available()` (presence only, no network, so starting offline still works) because the watcher claims the pid file *before* authenticating and would otherwise report a pid for a process that stops a moment later — pid file/log in ~/.asher-cli, start/stop/status/run, cross-platform detach + liveness (os.kill would *terminate* on Windows)
|
|
64
|
+
tray.py optional pystray/Pillow system-tray icon over WatcherRunner; every path degrades to a headless watcher. Icon is a panel-toned silhouette + status dot — colour rides the badge, never the whole glyph
|
|
65
|
+
launcher.py open_app() — start the TUI in a new terminal from the tray (a detached tray has none): new console on Windows, Terminal.app via AppleScript on macOS, first installed emulator on Linux (desktop's own preferred)
|
|
66
|
+
desktoptheme.py panel_is_dark() — is the tray/menu-bar background dark? kdeglobals luma / gsettings / AppleInterfaceStyle / the Personalize registry keys, behind a TTL cache; every probe degrades to a fallback, never raises
|
|
67
|
+
autostart.py login items — AutostartBackend ABC + launchd/systemd-user/registry subclasses + backend() factory; all per-user, no admin rights, disable() removes exactly what enable() wrote
|
|
68
|
+
notifications.py desktop toast + audible alert façade — plyer first, then the platform's own tool (osascript/notify-send); fire/beep are always-safe no-ops on failure/headless
|
|
69
|
+
|
|
70
|
+
mcp/
|
|
71
|
+
config.py Claude Desktop config read/write for the /mcp slash command
|
|
72
|
+
bridge.py asher-mcp-launch console script — keyring-backed pylitterbot MCP launcher
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Layering** (enforced by `tests/test_layering.py`): `core/` and `robot/` import only each other; `mcp/` imports neither `tui/` nor `desktop/`; `desktop/` may use `core/`, `robot/` and `headless`, never `tui/`; only `__main__` imports `tui/`. Something both the TUI and the watcher need belongs in `core/`.
|
|
57
76
|
|
|
77
|
+
```
|
|
58
78
|
tests/
|
|
59
79
|
testhelpers.py unit tests for helpers.py
|
|
60
80
|
test_cats.py CATS dict structure
|
|
@@ -62,7 +82,8 @@ tests/
|
|
|
62
82
|
test_auth_pilot.py Textual Pilot integration tests for LoginScreen
|
|
63
83
|
test_app_pilot.py Textual Pilot integration tests for AsherApp
|
|
64
84
|
test_commands_pilot.py Textual Pilot integration tests for command dispatch
|
|
65
|
-
|
|
85
|
+
test_credentials.py keyring/env credential helpers (core.credentials)
|
|
86
|
+
test_layering.py subpackage import boundaries (AST scan of asher/)
|
|
66
87
|
test_connection_mixin.py ConnectionMixin structure
|
|
67
88
|
test_monitoring.py MonitoringMixin async methods
|
|
68
89
|
test_ui.py UIMixin constants, CSS, helper existence
|
|
@@ -109,21 +130,21 @@ LITTER_ROBOT_USER=...
|
|
|
109
130
|
LITTER_ROBOT_PASSWORD=...
|
|
110
131
|
```
|
|
111
132
|
|
|
112
|
-
Every environment read goes through `
|
|
113
|
-
`asher/
|
|
133
|
+
Every environment read goes through `credentials.from_env()` in
|
|
134
|
+
`asher/core/credentials.py` — the one place the gate is applied. Don't call
|
|
114
135
|
`os.getenv("LITTER_ROBOT_*")` anywhere else. `helpers.dev_mode()` is the shared
|
|
115
136
|
predicate (it also selects the `dev` version string).
|
|
116
137
|
|
|
117
138
|
Keyring service name: `asher-cli`, keys `email` and `password`.
|
|
118
|
-
|
|
139
|
+
Helpers live in `asher/core/credentials.py` (`load()`, `save()`, `delete()`, …); call them through the module (`credentials.load()`) so tests have one patch target.
|
|
119
140
|
|
|
120
141
|
## Command convention
|
|
121
142
|
|
|
122
|
-
Command names, slash-command names, and their args are not listed here — see
|
|
143
|
+
Command names, slash-command names, and their args are not listed here — see `_registry` in `asher/tui/commands/registry.py`, which is authoritative; `/help` renders it at runtime. `/mcp`'s credential-bridging design is documented in the `mcp-bridge` skill.
|
|
123
144
|
|
|
124
145
|
**Normal commands** (no prefix) are robot actions only; **slash commands** (`/` prefix) are app management only.
|
|
125
146
|
|
|
126
|
-
`/refresh`, `/cat`, `/pet`, and `/notify` persist their settings to `~/.asher-cli/config.json` (via `asher.config.update()`), so they survive restarts. That file is also the only channel to a running watcher, which re-reads it per alert rather than caching at startup. Credentials and the preferred-robot serial stay in the OS keyring; the config file holds only non-secret UI preferences.
|
|
147
|
+
`/refresh`, `/cat`, `/pet`, and `/notify` persist their settings to `~/.asher-cli/config.json` (via `asher.core.config.update()`), so they survive restarts. That file is also the only channel to a running watcher, which re-reads it per alert rather than caching at startup. Credentials and the preferred-robot serial stay in the OS keyring; the config file holds only non-secret UI preferences.
|
|
127
148
|
|
|
128
149
|
Do not add robot-control commands as slash commands, and do not add app-management commands as bare commands.
|
|
129
150
|
|
|
@@ -132,7 +153,7 @@ Do not add robot-control commands as slash commands, and do not add app-manageme
|
|
|
132
153
|
|
|
133
154
|
**Headless commands** (`asher <command>`) are a parallel registry in `asher/headless.py`: same robot actions, no Textual, plain-string + JSON output. `asher watch` and `asher update` are deliberately *not* in that registry — `watch` manages a long-lived process rather than doing one thing and exiting, and `update` talks to PyPI rather than a robot, so neither should be forced through `open_session()` and made to authenticate. Both declare their subparser directly in `__main__.py`. 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.
|
|
134
155
|
|
|
135
|
-
> If you add a command, update the tables in `README.md
|
|
156
|
+
> If you add a command, update the tables in `README.md`. If it's a robot command, consider adding it to `COMMANDS` in `asher/headless.py` too.
|
|
136
157
|
|
|
137
158
|
## Architecture
|
|
138
159
|
|
|
@@ -180,19 +201,19 @@ LoginScreen (ModalScreen) — available in auth.py but not the primary auth path
|
|
|
180
201
|
| `on_key()` | `↑`/`↓` history nav, plus completion nav (arrows cycle the slash popup, `Tab`/`Enter` accept, `Esc` dismisses); `Tab` also accepts the inline ghost-text suggestion when the popup is closed |
|
|
181
202
|
| `_start_login_flow()` | begin inline email/password prompt in command bar |
|
|
182
203
|
| `_cmd_logout()` | delete creds from keyring, disconnect |
|
|
183
|
-
| `make_adapter(robot)` | factory in `
|
|
204
|
+
| `make_adapter(robot)` | factory in `robot/adapters.py` — returns correct `RobotAdapter` subclass |
|
|
184
205
|
| `_log_ok/err/warn/info()` | timestamped log helpers |
|
|
185
206
|
|
|
186
207
|
## Robot compatibility
|
|
187
208
|
|
|
188
|
-
pylitterbot auto-detects robot type. Commands that differ per model are handled by `RobotAdapter` subclasses in `
|
|
209
|
+
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.
|
|
189
210
|
|
|
190
211
|
## Colour
|
|
191
212
|
|
|
192
|
-
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.
|
|
213
|
+
Every colour comes from `asher/core/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.
|
|
193
214
|
|
|
194
215
|
- **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.
|
|
195
|
-
- **`
|
|
216
|
+
- **`tui/style.tcss`:** use the `$asher-*` variables; `AsherApp.get_css_variables()` supplies them.
|
|
196
217
|
- **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.
|
|
197
218
|
|
|
198
219
|
## Code comments
|
|
@@ -206,7 +227,7 @@ Don't add comments above functions or inline unless the WHY is genuinely non-obv
|
|
|
206
227
|
- Cat modes: `idle`, `happy`, `cleaning` (animated), `sleeping`, `error`, `full`
|
|
207
228
|
- `VERSION` is read from `importlib.metadata.version("asher-cli")` — falls back to `"dev"` when running from source
|
|
208
229
|
- **No `assert` in `asher/`** — Bandit enforces this (B101 is enabled; only `tests/` is exempt, via `exclude_dirs`). For a `requires_robot` command, narrow with `if app._robot is None: return` rather than an assert: `_dispatch_command` already rejects the disconnected case, and unlike `assert` the guard survives `python -O`
|
|
209
|
-
- The primary login path is the inline flow in `
|
|
230
|
+
- The primary login path is the inline flow in `tui/loginflow.py` (`LoginFlow` state machine: `IDLE` → `AWAITING_EMAIL` → `AWAITING_PASSWORD`). `LoginScreen` (`tui/loginscreen.py`) still exists as a modal but is not used in the current main flow.
|
|
210
231
|
- `LoginScreen` uses `event.stop()` on `Input.Submitted` and `Button.Pressed` to prevent bubbling to the App's `on_input_submitted` (relevant if re-activating the modal path)
|
|
211
232
|
|
|
212
233
|
### IoT command timing — optimistic UI updates
|
|
@@ -217,15 +238,15 @@ Commands that need a confirmed cloud state before showing a result (e.g. sleep/w
|
|
|
217
238
|
|
|
218
239
|
## Common tasks
|
|
219
240
|
|
|
220
|
-
**Add a robot command:** create a class inheriting `Command` in `asher/commands/
|
|
241
|
+
**Add a robot command:** create a class inheriting `Command` in `asher/tui/commands/robot.py` (or `lr5.py`), implement `async def run(self, app, args)`, and register it in `registry.py`.
|
|
221
242
|
|
|
222
|
-
**Add a slash command:** create a class inheriting `SlashCommand` (sets `prefix = "/"`)
|
|
243
|
+
**Add a slash command:** create a class inheriting `SlashCommand` (sets `prefix = "/"`) in `asher/tui/commands/slash.py`, implement `async def run(self, app, args)`, and register it in `registry.py`.
|
|
223
244
|
|
|
224
245
|
**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.
|
|
225
246
|
|
|
226
247
|
**Change poll interval:** `self.set_interval(300, ...)` in `on_mount`.
|
|
227
248
|
|
|
228
|
-
**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")`.
|
|
249
|
+
**Add a new cat state:** add entry to `CATS` dict in `asher/tui/cats.py` (str for static, list[str] for animated), then call `_set_cat("name", "label")`.
|
|
229
250
|
|
|
230
251
|
**File naming convention:** no underscores in filenames (except Python-required `__init__.py` and `__main__.py`).
|
|
231
252
|
|
|
@@ -254,5 +275,5 @@ Regenerate with `changelog-release X.Y.Z`, never plain `changelog` — the workf
|
|
|
254
275
|
- Pilot-based integration tests use `app.run_test()` with `await pilot.pause()` before querying widgets
|
|
255
276
|
- Helper app wrappers for screens must **not** start with `Test` (pytest will try to collect them); use e.g. `LoginTestApp`
|
|
256
277
|
- Mock external deps with `unittest.mock.AsyncMock` for async robot/account methods
|
|
257
|
-
- `from pylitterbot import Account` is a local import inside `_connect_worker` — patch it at `pylitterbot.Account`, not `asher.connection.Account`
|
|
278
|
+
- `from pylitterbot import Account` is a local import inside `_connect_worker` — patch it at `pylitterbot.Account`, not `asher.tui.connection.Account`
|
|
258
279
|
- Coverage: ~76% overall; main gaps are async exception paths and `_connect_worker` auth flow
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: asher-cli
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.1
|
|
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
|
|
@@ -40,14 +40,14 @@ Classifier: Topic :: Home Automation
|
|
|
40
40
|
Requires-Python: >=3.10
|
|
41
41
|
Requires-Dist: keyring>=24.0.0
|
|
42
42
|
Requires-Dist: plyer>=2.1.0
|
|
43
|
-
Requires-Dist: pylitterbot==2025.6.
|
|
43
|
+
Requires-Dist: pylitterbot==2025.6.6
|
|
44
44
|
Requires-Dist: python-dotenv>=1.0.0
|
|
45
45
|
Requires-Dist: rich>=13.0.0
|
|
46
46
|
Requires-Dist: textual>=0.47.0
|
|
47
47
|
Requires-Dist: tzlocal>=5.4
|
|
48
48
|
Provides-Extra: mcp
|
|
49
49
|
Requires-Dist: mcp<2; extra == 'mcp'
|
|
50
|
-
Requires-Dist: pylitterbot[mcp]==2025.6.
|
|
50
|
+
Requires-Dist: pylitterbot[mcp]==2025.6.6; extra == 'mcp'
|
|
51
51
|
Provides-Extra: tray
|
|
52
52
|
Requires-Dist: pillow>=10.0; extra == 'tray'
|
|
53
53
|
Requires-Dist: pygobject>=3.42; (sys_platform == 'linux') and extra == 'tray'
|
|
@@ -85,7 +85,7 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
|
|
|
85
85
|
- 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
|
|
86
86
|
- Background watcher — `asher watch start` detaches a notifier process that keeps toasting faults, a filling drawer, and offline/online changes **after you close the terminal**; `asher watch enable` starts it at login (launchd / systemd / registry); optional system-tray icon shows live status with Open Asher / Notifications / Quit
|
|
87
87
|
- Update notices — checks PyPI once a day and tells you when a newer release exists, with the right upgrade command for how you installed it. It never installs anything itself
|
|
88
|
-
- Catppuccin Mocha throughout — one palette in `asher/theme.py` drives the TUI stylesheet and every Rich style
|
|
88
|
+
- Catppuccin Mocha throughout — one palette in `asher/core/theme.py` drives the TUI stylesheet and every Rich style
|
|
89
89
|
- Cat animation panel that reacts to robot state
|
|
90
90
|
- Command history (↑/↓ arrows)
|
|
91
91
|
- Real-time updates via WebSocket; 5-minute poll fallback
|
|
@@ -29,7 +29,7 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
|
|
|
29
29
|
- 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
|
|
30
30
|
- Background watcher — `asher watch start` detaches a notifier process that keeps toasting faults, a filling drawer, and offline/online changes **after you close the terminal**; `asher watch enable` starts it at login (launchd / systemd / registry); optional system-tray icon shows live status with Open Asher / Notifications / Quit
|
|
31
31
|
- Update notices — checks PyPI once a day and tells you when a newer release exists, with the right upgrade command for how you installed it. It never installs anything itself
|
|
32
|
-
- Catppuccin Mocha throughout — one palette in `asher/theme.py` drives the TUI stylesheet and every Rich style
|
|
32
|
+
- Catppuccin Mocha throughout — one palette in `asher/core/theme.py` drives the TUI stylesheet and every Rich style
|
|
33
33
|
- Cat animation panel that reacts to robot state
|
|
34
34
|
- Command history (↑/↓ arrows)
|
|
35
35
|
- Real-time updates via WebSocket; 5-minute poll fallback
|
|
@@ -22,7 +22,7 @@ import argparse
|
|
|
22
22
|
import asyncio
|
|
23
23
|
import sys
|
|
24
24
|
|
|
25
|
-
from .daemon import ACTIONS as DAEMON_ACTIONS
|
|
25
|
+
from .desktop.daemon import ACTIONS as DAEMON_ACTIONS
|
|
26
26
|
from .headless import COMMANDS
|
|
27
27
|
|
|
28
28
|
|
|
@@ -156,12 +156,12 @@ def main() -> None:
|
|
|
156
156
|
args = _build_parser().parse_args()
|
|
157
157
|
|
|
158
158
|
if args.command == "watch":
|
|
159
|
-
from .daemon import dispatch # noqa: PLC0415
|
|
159
|
+
from .desktop.daemon import dispatch # noqa: PLC0415
|
|
160
160
|
|
|
161
161
|
sys.exit(dispatch(args.action, robot=args.robot, tray=not args.no_tray))
|
|
162
162
|
|
|
163
163
|
if args.command == "update":
|
|
164
|
-
from .updates import report # noqa: PLC0415
|
|
164
|
+
from .core.updates import report # noqa: PLC0415
|
|
165
165
|
|
|
166
166
|
up_to_date, message = report(as_json=args.json)
|
|
167
167
|
print(message)
|
|
@@ -183,11 +183,11 @@ def main() -> None:
|
|
|
183
183
|
)
|
|
184
184
|
|
|
185
185
|
if args.export is not None:
|
|
186
|
-
from .
|
|
186
|
+
from .headless import run_legacy_export # noqa: PLC0415
|
|
187
187
|
|
|
188
|
-
sys.exit(asyncio.run(
|
|
188
|
+
sys.exit(asyncio.run(run_legacy_export(args)))
|
|
189
189
|
|
|
190
|
-
from .app import AsherApp # noqa: PLC0415
|
|
190
|
+
from .tui.app import AsherApp # noqa: PLC0415
|
|
191
191
|
|
|
192
192
|
AsherApp().run()
|
|
193
193
|
|
|
@@ -7,7 +7,7 @@ the OS keyring; this file holds only non-secret UI preferences, so a corrupt
|
|
|
7
7
|
or hand-edited file degrades to defaults rather than crashing the app.
|
|
8
8
|
|
|
9
9
|
It is also the only channel between the dashboard and a detached watcher
|
|
10
|
-
(:mod:`asher.watcher`), which is why the watcher re-reads it per alert rather
|
|
10
|
+
(:mod:`asher.desktop.watcher`), which is why the watcher re-reads it per alert rather
|
|
11
11
|
than caching at startup: toggling ``/notify`` in the TUI, or the tray's own
|
|
12
12
|
Notifications item, has to reach a process that may have been up for days.
|
|
13
13
|
"""
|