asher-cli 1.3.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.
Files changed (142) hide show
  1. {asher_cli-1.3.0 → asher_cli-1.4.1}/.claude/skills/mcp-bridge/SKILL.md +3 -3
  2. {asher_cli-1.3.0 → asher_cli-1.4.1}/.claude/skills/pylitterbot-ref/SKILL.md +4 -2
  3. {asher_cli-1.3.0 → asher_cli-1.4.1}/.github/workflows/bandit.yml +2 -2
  4. {asher_cli-1.3.0 → asher_cli-1.4.1}/.github/workflows/ci.yml +2 -2
  5. {asher_cli-1.3.0 → asher_cli-1.4.1}/.github/workflows/coverage.yml +1 -1
  6. asher_cli-1.4.1/.github/workflows/dependency-automerge.yml +66 -0
  7. {asher_cli-1.3.0 → asher_cli-1.4.1}/.github/workflows/release.yml +1 -1
  8. {asher_cli-1.3.0 → asher_cli-1.4.1}/CHANGELOG.md +44 -0
  9. {asher_cli-1.3.0 → asher_cli-1.4.1}/CLAUDE.md +70 -49
  10. {asher_cli-1.3.0 → asher_cli-1.4.1}/PKG-INFO +7 -5
  11. {asher_cli-1.3.0 → asher_cli-1.4.1}/README.md +4 -2
  12. {asher_cli-1.3.0 → asher_cli-1.4.1}/asher/__main__.py +6 -6
  13. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/core}/config.py +1 -1
  14. asher_cli-1.4.1/asher/core/credentials.py +185 -0
  15. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/core}/export.py +15 -35
  16. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/core}/faults.py +1 -1
  17. asher_cli-1.4.1/asher/desktop/__init__.py +0 -0
  18. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/desktop}/autostart.py +1 -1
  19. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/desktop}/daemon.py +5 -4
  20. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/desktop}/launcher.py +3 -3
  21. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/desktop}/notifications.py +3 -3
  22. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/desktop}/tray.py +4 -3
  23. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/desktop}/watcher.py +10 -10
  24. {asher_cli-1.3.0 → asher_cli-1.4.1}/asher/headless.py +39 -16
  25. asher_cli-1.4.1/asher/mcp/__init__.py +0 -0
  26. asher_cli-1.3.0/asher/mcp_config.py → asher_cli-1.4.1/asher/mcp/config.py +1 -1
  27. asher_cli-1.4.1/asher/robot/__init__.py +0 -0
  28. asher_cli-1.3.0/asher/robot_adapters.py → asher_cli-1.4.1/asher/robot/adapters.py +2 -2
  29. asher_cli-1.4.1/asher/tui/__init__.py +0 -0
  30. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/tui}/app.py +6 -6
  31. asher_cli-1.4.1/asher/tui/commands/__init__.py +303 -0
  32. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/tui}/commands/base.py +4 -0
  33. asher_cli-1.4.1/asher/tui/commands/builtin.py +44 -0
  34. asher_cli-1.4.1/asher/tui/commands/lr5.py +110 -0
  35. asher_cli-1.4.1/asher/tui/commands/registry.py +80 -0
  36. asher_cli-1.4.1/asher/tui/commands/robot.py +812 -0
  37. asher_cli-1.4.1/asher/tui/commands/slash.py +546 -0
  38. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/tui}/completion.py +2 -2
  39. asher_cli-1.4.1/asher/tui/connection.py +172 -0
  40. asher_cli-1.3.0/asher/history_view.py → asher_cli-1.4.1/asher/tui/history.py +2 -2
  41. asher_cli-1.3.0/asher/auth.py → asher_cli-1.4.1/asher/tui/loginscreen.py +1 -1
  42. asher_cli-1.3.0/asher/monitoring/__init__.py → asher_cli-1.4.1/asher/tui/monitoring.py +7 -7
  43. asher_cli-1.3.0/asher/ui/__init__.py → asher_cli-1.4.1/asher/tui/ui.py +24 -19
  44. asher_cli-1.4.1/docs/CONTRIBUTING.md +204 -0
  45. asher_cli-1.4.1/docs/ROADMAP.md +406 -0
  46. asher_cli-1.4.1/docs/roadmap-archive/README.md +40 -0
  47. asher_cli-1.4.1/docs/roadmap-archive/credentials.md +38 -0
  48. asher_cli-1.4.1/docs/roadmap-archive/e2e-pilot.md +55 -0
  49. asher_cli-1.4.1/docs/roadmap-archive/lr5-features.md +37 -0
  50. asher_cli-1.4.1/docs/roadmap-archive/multi-pet.md +10 -0
  51. asher_cli-1.4.1/docs/roadmap-archive/robot-commands.md +92 -0
  52. asher_cli-1.4.1/docs/roadmap-archive/runtime-slash-commands.md +79 -0
  53. asher_cli-1.4.1/docs/roadmap-archive/slash-command-spec.md +41 -0
  54. asher_cli-1.4.1/docs/roadmap-archive/sleep-schedule.md +60 -0
  55. asher_cli-1.4.1/docs/roadmap-archive/ui-ux.md +185 -0
  56. asher_cli-1.4.1/docs/roadmap-archive/version-display.md +37 -0
  57. asher_cli-1.4.1/docs/roadmap-archive/websocket-updates.md +22 -0
  58. {asher_cli-1.3.0 → asher_cli-1.4.1}/pyproject.toml +5 -6
  59. asher_cli-1.4.1/renovate.json +48 -0
  60. asher_cli-1.4.1/tests/__init__.py +0 -0
  61. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_activity_labels.py +3 -3
  62. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_app_pilot.py +32 -12
  63. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_auth.py +3 -3
  64. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_auth_pilot.py +2 -2
  65. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_autostart.py +3 -3
  66. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_cats.py +2 -2
  67. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_commands_pilot.py +10 -10
  68. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_completion.py +4 -4
  69. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_config.py +34 -34
  70. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_connection_mixin.py +2 -2
  71. asher_cli-1.3.0/tests/test_connection.py → asher_cli-1.4.1/tests/test_credentials.py +82 -89
  72. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_daemon.py +28 -25
  73. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_desktoptheme.py +2 -2
  74. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_export.py +47 -30
  75. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_faults.py +2 -2
  76. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_headless.py +8 -8
  77. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_history_view.py +6 -6
  78. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_launcher.py +2 -2
  79. asher_cli-1.4.1/tests/test_layering.py +59 -0
  80. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_lr5_commands.py +2 -2
  81. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_mcp_bridge.py +4 -4
  82. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_mcp_command.py +21 -21
  83. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_mcp_config.py +15 -15
  84. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_missing_robot_commands.py +4 -4
  85. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_monitoring.py +13 -13
  86. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_new_commands_pilot.py +17 -8
  87. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_notifications.py +2 -2
  88. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_robot_adapters.py +1 -1
  89. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_tray.py +8 -7
  90. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_ui.py +6 -6
  91. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_updates.py +3 -3
  92. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_version_command.py +3 -3
  93. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/test_watcher.py +15 -13
  94. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/testhelpers.py +3 -3
  95. {asher_cli-1.3.0 → asher_cli-1.4.1}/uv.lock +1359 -1297
  96. asher_cli-1.3.0/.github/workflows/claude-code-review.yml +0 -44
  97. asher_cli-1.3.0/.github/workflows/claude.yml +0 -50
  98. asher_cli-1.3.0/asher/commands/__init__.py +0 -1816
  99. asher_cli-1.3.0/asher/connection/__init__.py +0 -355
  100. asher_cli-1.3.0/asher/slash-commands/__init__.py +0 -46
  101. asher_cli-1.3.0/docs/ROADMAP.md +0 -1705
  102. asher_cli-1.3.0/docs/roadmap-archive/README.md +0 -16
  103. asher_cli-1.3.0/renovate.json +0 -22
  104. asher_cli-1.3.0/test.py +0 -32
  105. {asher_cli-1.3.0 → asher_cli-1.4.1}/.claude/hooks/block-env.py +0 -0
  106. {asher_cli-1.3.0 → asher_cli-1.4.1}/.claude/settings.json +0 -0
  107. {asher_cli-1.3.0 → asher_cli-1.4.1}/.claude/skills/textual/SKILL.md +0 -0
  108. {asher_cli-1.3.0 → asher_cli-1.4.1}/.env.example +0 -0
  109. {asher_cli-1.3.0 → asher_cli-1.4.1}/.githooks/pre-push +0 -0
  110. {asher_cli-1.3.0 → asher_cli-1.4.1}/.github/FUNDING.yml +0 -0
  111. {asher_cli-1.3.0 → asher_cli-1.4.1}/.github/pull_request_template.md +0 -0
  112. {asher_cli-1.3.0 → asher_cli-1.4.1}/.gitignore +0 -0
  113. {asher_cli-1.3.0 → asher_cli-1.4.1}/.vscode/launch.json +0 -0
  114. {asher_cli-1.3.0 → asher_cli-1.4.1}/.vscode/settings.json +0 -0
  115. {asher_cli-1.3.0 → asher_cli-1.4.1}/.vscode/tasks.json +0 -0
  116. {asher_cli-1.3.0 → asher_cli-1.4.1}/CODEOWNERS +0 -0
  117. {asher_cli-1.3.0 → asher_cli-1.4.1}/LICENSE +0 -0
  118. {asher_cli-1.3.0 → asher_cli-1.4.1}/app.py +0 -0
  119. {asher_cli-1.3.0 → asher_cli-1.4.1}/asher/__init__.py +0 -0
  120. {asher_cli-1.3.0/tests → asher_cli-1.4.1/asher/core}/__init__.py +0 -0
  121. /asher_cli-1.3.0/asher/activity_labels.py → /asher_cli-1.4.1/asher/core/activity.py +0 -0
  122. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/core}/constants.py +0 -0
  123. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/core}/helpers.py +0 -0
  124. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/core}/theme.py +0 -0
  125. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/core}/updates.py +0 -0
  126. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/desktop}/desktoptheme.py +0 -0
  127. /asher_cli-1.3.0/asher/mcp_bridge.py → /asher_cli-1.4.1/asher/mcp/bridge.py +0 -0
  128. /asher_cli-1.3.0/asher/robot_protocol.py → /asher_cli-1.4.1/asher/robot/protocol.py +0 -0
  129. {asher_cli-1.3.0/asher → asher_cli-1.4.1/asher/tui}/cats.py +0 -0
  130. /asher_cli-1.3.0/asher/login_flow.py → /asher_cli-1.4.1/asher/tui/loginflow.py +0 -0
  131. {asher_cli-1.3.0/asher/ui → asher_cli-1.4.1/asher/tui}/style.tcss +0 -0
  132. {asher_cli-1.3.0 → asher_cli-1.4.1}/cliff.toml +0 -0
  133. {asher_cli-1.3.0 → asher_cli-1.4.1}/docs/roadmap-archive/cat-panel-badges.md +0 -0
  134. {asher_cli-1.3.0 → asher_cli-1.4.1}/docs/roadmap-archive/config-persistence.md +0 -0
  135. {asher_cli-1.3.0 → asher_cli-1.4.1}/docs/roadmap-archive/desktop-notifications.md +0 -0
  136. {asher_cli-1.3.0 → asher_cli-1.4.1}/docs/roadmap-archive/fault-monitoring.md +0 -0
  137. {asher_cli-1.3.0 → asher_cli-1.4.1}/docs/roadmap-archive/headless-export.md +0 -0
  138. {asher_cli-1.3.0 → asher_cli-1.4.1}/docs/roadmap-archive/history-export.md +0 -0
  139. {asher_cli-1.3.0 → asher_cli-1.4.1}/docs/roadmap-archive/tab-completion.md +0 -0
  140. {asher_cli-1.3.0 → asher_cli-1.4.1}/requirements.txt +0 -0
  141. {asher_cli-1.3.0 → asher_cli-1.4.1}/tests/conftest.py +0 -0
  142. {asher_cli-1.3.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/mcp_config.py, asher/mcp_bridge.py, or the /mcp slash command.
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/__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.
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/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.
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/`, `asher/robot_adapters.py`, or anything that touches `robot.*`.
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/robot_adapters.py` for all model-specific dispatch — do not branch on `type(robot).__name__` inline in command handlers.
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@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
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@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
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@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
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@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
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@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
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
@@ -0,0 +1,66 @@
1
+ name: Dependency auto-merge
2
+
3
+ on:
4
+ pull_request_target:
5
+ types: [opened, synchronize, reopened, edited, labeled]
6
+
7
+ permissions:
8
+ contents: write
9
+ pull-requests: write
10
+
11
+ jobs:
12
+ automerge:
13
+ runs-on: ubuntu-latest
14
+ if: github.event.pull_request.base.repo.full_name == github.repository
15
+ steps:
16
+ - name: Dependabot metadata
17
+ if: github.event.pull_request.user.login == 'dependabot[bot]'
18
+ id: dependabot-metadata
19
+ uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3
20
+ with:
21
+ github-token: "${{ secrets.GITHUB_TOKEN }}"
22
+
23
+ - name: Decide eligibility
24
+ id: decide
25
+ env:
26
+ TITLE: ${{ github.event.pull_request.title }}
27
+ ACTOR: ${{ github.event.pull_request.user.login }}
28
+ LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
29
+ DEPENDABOT_UPDATE_TYPE: ${{ steps.dependabot-metadata.outputs.update-type }}
30
+ run: |
31
+ safe=false
32
+ if [ "$ACTOR" = "dependabot[bot]" ]; then
33
+ # Only semver patch/minor bumps auto-merge; majors always wait for review.
34
+ case "$DEPENDABOT_UPDATE_TYPE" in
35
+ version-update:semver-patch|version-update:semver-minor) safe=true ;;
36
+ esac
37
+ elif [ "$ACTOR" = "karanshukla" ]; then
38
+ # The dependency-update routine commits under this account rather than a
39
+ # bot identity. The `dependencies` label is the reliable signal; the title
40
+ # pattern is a fallback for when it is missing. Either way, a title
41
+ # mentioning "major"/"breaking" is left for manual merge. Only the
42
+ # title is scanned -- dependency-audit bodies routinely mention other
43
+ # packages' major versions, which used to veto safe patch-level PRs.
44
+ matched=false
45
+ case ",$LABELS," in
46
+ *,dependencies,*) matched=true ;;
47
+ esac
48
+ if [ "$matched" = false ]; then
49
+ if printf '%s' "$TITLE" | grep -Eiq '^(chore|fix)(\([a-z0-9/_-]+\))?:.*(dependenc|advisory|CVE|vulnerab|bump)'; then
50
+ matched=true
51
+ fi
52
+ fi
53
+ if [ "$matched" = true ]; then
54
+ if ! printf '%s' "$TITLE" | grep -Eiq '\b(major|breaking)\b'; then
55
+ safe=true
56
+ fi
57
+ fi
58
+ fi
59
+ echo "safe=$safe" >> "$GITHUB_OUTPUT"
60
+
61
+ - name: Enable auto-merge
62
+ if: steps.decide.outputs.safe == 'true'
63
+ env:
64
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
65
+ PR_URL: ${{ github.event.pull_request.html_url }}
66
+ run: gh pr merge --auto --squash "$PR_URL"
@@ -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@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
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,6 +7,50 @@ 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.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
39
+
40
+ ### 🐛 Bug Fixes
41
+
42
+ - *(ci)* Only scan the PR title for major/breaking in auto-merge (#86)
43
+ - *(deps)* Update dependency pylitterbot to v2025.6.5 (#82)
44
+
45
+ ### 👷 CI/CD
46
+
47
+ - Auto-merge non-major dependency PRs once checks pass (#85)
48
+
49
+ ### 🔧 Miscellaneous
50
+
51
+ - *(renovate)* Squash automerged updates (#84)
52
+ - *(renovate)* Auto-merge non-major updates once checks pass (#83)
53
+
10
54
  ## [1.3.0] - 2026-08-23
11
55
 
12
56
  ### 🚀 Features
@@ -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
- commands/
51
- base.py Command ABC, SlashCommand, CommandRegistry
52
- __init__.py CommandsMixin — all command classes + registry + dispatch
53
- connection/ ConnectionMixin — keyring auth, _connect_worker, keyring helpers, _connect_headless() (no-UI auth for `asher --export`)
54
- monitoring/ MonitoringMixin — _poll_status_interval, _refresh_status
55
- ui/ UIMixin — CSS, compose(), log helpers, cat helpers
56
- slash-commands/ Convention doc
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
- test_connection.py keyring helper functions
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 `_env_credentials()` in
113
- `asher/connection/__init__.py` — the one place the gate is applied. Don't call
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
- Helper functions in `asher/connection/__init__.py`: `_keyring_load()`, `_keyring_save()`, `_keyring_delete()`.
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 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.
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` 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.
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 `robot_adapters.py` — returns correct `RobotAdapter` subclass |
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 `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.
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
- - **`ui/style.tcss`:** use the `$asher-*` variables; `AsherApp.get_css_variables()` supplies them.
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 `login_flow.py` (`LoginFlow` state machine: `IDLE` → `AWAITING_EMAIL` → `AWAITING_PASSWORD`). `LoginScreen` (`auth.py`) still exists as a modal but is not used in the current main flow.
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/__init__.py`, implement `async def run(self, app, args)`, and call `_registry.register(MyCommand())`.
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 = "/"`), implement `async def run(self, app, args)`, register it, and document in `asher/slash-commands/__init__.py`.
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.3.0
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.4
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.4; extra == 'mcp'
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'
@@ -66,7 +66,9 @@ Description-Content-Type: text/markdown
66
66
 
67
67
  A Claude Code-style terminal dashboard for monitoring and controlling Litter Robot via the Whisker cloud API.
68
68
 
69
- <img width="808" height="351" alt="image" src="https://github.com/user-attachments/assets/6599966f-837c-419c-8692-bfda3533e730" />
69
+ <img width="1165" height="885" alt="image" src="https://github.com/user-attachments/assets/03be7ed3-95f6-45cf-93db-cf5bf84d6499" />
70
+
71
+ <img width="1165" height="885" alt="image" src="https://github.com/user-attachments/assets/5aa60ff1-6e6b-4c71-8376-d96d52d96809" />
70
72
 
71
73
  ## Features
72
74
 
@@ -83,7 +85,7 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
83
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
84
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
85
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
86
- - 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
87
89
  - Cat animation panel that reacts to robot state
88
90
  - Command history (↑/↓ arrows)
89
91
  - Real-time updates via WebSocket; 5-minute poll fallback
@@ -10,7 +10,9 @@
10
10
 
11
11
  A Claude Code-style terminal dashboard for monitoring and controlling Litter Robot via the Whisker cloud API.
12
12
 
13
- <img width="808" height="351" alt="image" src="https://github.com/user-attachments/assets/6599966f-837c-419c-8692-bfda3533e730" />
13
+ <img width="1165" height="885" alt="image" src="https://github.com/user-attachments/assets/03be7ed3-95f6-45cf-93db-cf5bf84d6499" />
14
+
15
+ <img width="1165" height="885" alt="image" src="https://github.com/user-attachments/assets/5aa60ff1-6e6b-4c71-8376-d96d52d96809" />
14
16
 
15
17
  ## Features
16
18
 
@@ -27,7 +29,7 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
27
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
28
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
29
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
30
- - 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
31
33
  - Cat animation panel that reacts to robot state
32
34
  - Command history (↑/↓ arrows)
33
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 .export import _run_headless_export # noqa: PLC0415
186
+ from .headless import run_legacy_export # noqa: PLC0415
187
187
 
188
- sys.exit(asyncio.run(_run_headless_export(args)))
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
  """