asher-cli 1.1.0__tar.gz → 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. {asher_cli-1.1.0 → asher_cli-1.2.0}/.github/workflows/ci.yml +8 -2
  2. {asher_cli-1.1.0 → asher_cli-1.2.0}/.github/workflows/claude-code-review.yml +1 -1
  3. {asher_cli-1.1.0 → asher_cli-1.2.0}/.github/workflows/claude.yml +1 -1
  4. {asher_cli-1.1.0 → asher_cli-1.2.0}/.github/workflows/coverage.yml +6 -1
  5. {asher_cli-1.1.0 → asher_cli-1.2.0}/.github/workflows/release.yml +1 -1
  6. {asher_cli-1.1.0 → asher_cli-1.2.0}/CHANGELOG.md +40 -2
  7. {asher_cli-1.1.0 → asher_cli-1.2.0}/CLAUDE.md +43 -6
  8. {asher_cli-1.1.0 → asher_cli-1.2.0}/PKG-INFO +92 -14
  9. {asher_cli-1.1.0 → asher_cli-1.2.0}/README.md +83 -10
  10. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/__main__.py +63 -0
  11. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/app.py +1 -0
  12. asher_cli-1.2.0/asher/autostart.py +370 -0
  13. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/commands/__init__.py +58 -12
  14. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/completion.py +3 -1
  15. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/config.py +10 -0
  16. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/connection/__init__.py +44 -12
  17. asher_cli-1.2.0/asher/daemon.py +429 -0
  18. asher_cli-1.2.0/asher/desktoptheme.py +192 -0
  19. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/helpers.py +25 -0
  20. asher_cli-1.2.0/asher/launcher.py +133 -0
  21. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/monitoring/__init__.py +14 -0
  22. asher_cli-1.2.0/asher/notifications.py +120 -0
  23. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/slash-commands/__init__.py +2 -0
  24. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/theme.py +6 -0
  25. asher_cli-1.2.0/asher/tray.py +264 -0
  26. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/ui/__init__.py +18 -3
  27. asher_cli-1.2.0/asher/updates.py +181 -0
  28. asher_cli-1.2.0/asher/watcher.py +393 -0
  29. {asher_cli-1.1.0 → asher_cli-1.2.0}/pyproject.toml +31 -4
  30. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_app_pilot.py +3 -0
  31. asher_cli-1.2.0/tests/test_autostart.py +341 -0
  32. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_commands_pilot.py +3 -0
  33. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_config.py +4 -4
  34. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_connection.py +77 -0
  35. asher_cli-1.2.0/tests/test_daemon.py +445 -0
  36. asher_cli-1.2.0/tests/test_desktoptheme.py +228 -0
  37. asher_cli-1.2.0/tests/test_launcher.py +203 -0
  38. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_mcp_command.py +14 -0
  39. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_monitoring.py +7 -0
  40. asher_cli-1.2.0/tests/test_notifications.py +176 -0
  41. asher_cli-1.2.0/tests/test_tray.py +305 -0
  42. asher_cli-1.2.0/tests/test_updates.py +272 -0
  43. asher_cli-1.2.0/tests/test_watcher.py +379 -0
  44. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/testhelpers.py +21 -1
  45. {asher_cli-1.1.0 → asher_cli-1.2.0}/uv.lock +1121 -683
  46. asher_cli-1.1.0/asher/notifications.py +0 -48
  47. asher_cli-1.1.0/tests/test_notifications.py +0 -82
  48. {asher_cli-1.1.0 → asher_cli-1.2.0}/.claude/hooks/block-env.py +0 -0
  49. {asher_cli-1.1.0 → asher_cli-1.2.0}/.claude/settings.json +0 -0
  50. {asher_cli-1.1.0 → asher_cli-1.2.0}/.claude/skills/mcp-bridge/SKILL.md +0 -0
  51. {asher_cli-1.1.0 → asher_cli-1.2.0}/.claude/skills/pylitterbot-ref/SKILL.md +0 -0
  52. {asher_cli-1.1.0 → asher_cli-1.2.0}/.claude/skills/textual/SKILL.md +0 -0
  53. {asher_cli-1.1.0 → asher_cli-1.2.0}/.env.example +0 -0
  54. {asher_cli-1.1.0 → asher_cli-1.2.0}/.githooks/pre-push +0 -0
  55. {asher_cli-1.1.0 → asher_cli-1.2.0}/.github/FUNDING.yml +0 -0
  56. {asher_cli-1.1.0 → asher_cli-1.2.0}/.github/pull_request_template.md +0 -0
  57. {asher_cli-1.1.0 → asher_cli-1.2.0}/.github/workflows/bandit.yml +0 -0
  58. {asher_cli-1.1.0 → asher_cli-1.2.0}/.gitignore +0 -0
  59. {asher_cli-1.1.0 → asher_cli-1.2.0}/.vscode/launch.json +0 -0
  60. {asher_cli-1.1.0 → asher_cli-1.2.0}/.vscode/settings.json +0 -0
  61. {asher_cli-1.1.0 → asher_cli-1.2.0}/.vscode/tasks.json +0 -0
  62. {asher_cli-1.1.0 → asher_cli-1.2.0}/CODEOWNERS +0 -0
  63. {asher_cli-1.1.0 → asher_cli-1.2.0}/LICENSE +0 -0
  64. {asher_cli-1.1.0 → asher_cli-1.2.0}/app.py +0 -0
  65. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/__init__.py +0 -0
  66. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/activity_labels.py +0 -0
  67. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/auth.py +0 -0
  68. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/cats.py +0 -0
  69. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/commands/base.py +0 -0
  70. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/constants.py +0 -0
  71. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/export.py +0 -0
  72. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/faults.py +0 -0
  73. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/headless.py +0 -0
  74. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/history_view.py +0 -0
  75. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/login_flow.py +0 -0
  76. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/mcp_bridge.py +0 -0
  77. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/mcp_config.py +0 -0
  78. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/robot_adapters.py +0 -0
  79. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/robot_protocol.py +0 -0
  80. {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/ui/style.tcss +0 -0
  81. {asher_cli-1.1.0 → asher_cli-1.2.0}/cliff.toml +0 -0
  82. {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/ROADMAP.md +0 -0
  83. {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/roadmap-archive/README.md +0 -0
  84. {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/roadmap-archive/cat-panel-badges.md +0 -0
  85. {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/roadmap-archive/config-persistence.md +0 -0
  86. {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/roadmap-archive/desktop-notifications.md +0 -0
  87. {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/roadmap-archive/fault-monitoring.md +0 -0
  88. {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/roadmap-archive/headless-export.md +0 -0
  89. {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/roadmap-archive/history-export.md +0 -0
  90. {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/roadmap-archive/tab-completion.md +0 -0
  91. {asher_cli-1.1.0 → asher_cli-1.2.0}/renovate.json +0 -0
  92. {asher_cli-1.1.0 → asher_cli-1.2.0}/requirements.txt +0 -0
  93. {asher_cli-1.1.0 → asher_cli-1.2.0}/test.py +0 -0
  94. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/__init__.py +0 -0
  95. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/conftest.py +0 -0
  96. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_activity_labels.py +0 -0
  97. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_auth.py +0 -0
  98. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_auth_pilot.py +0 -0
  99. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_cats.py +0 -0
  100. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_completion.py +0 -0
  101. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_connection_mixin.py +0 -0
  102. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_export.py +0 -0
  103. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_faults.py +0 -0
  104. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_headless.py +0 -0
  105. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_history_view.py +0 -0
  106. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_lr5_commands.py +0 -0
  107. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_mcp_bridge.py +0 -0
  108. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_mcp_config.py +0 -0
  109. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_missing_robot_commands.py +0 -0
  110. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_new_commands_pilot.py +0 -0
  111. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_robot_adapters.py +0 -0
  112. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_ui.py +0 -0
  113. {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_version_command.py +0 -0
  114. {asher_cli-1.1.0 → asher_cli-1.2.0}/watchrun.py +0 -0
@@ -8,13 +8,19 @@ on:
8
8
  permissions:
9
9
  contents: read
10
10
 
11
+ # `uv run` re-syncs to the default groups on every invocation, which would drag
12
+ # the `tray` group (and so a PyGObject/pycairo source build) back in after the
13
+ # explicit sync below deliberately left it out. Sync once, then never re-sync.
14
+ env:
15
+ UV_NO_SYNC: "1"
16
+
11
17
  jobs:
12
18
  lint:
13
19
  runs-on: ubuntu-latest
14
20
  steps:
15
21
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
16
22
  - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
17
- - run: uv sync --dev
23
+ - run: uv sync --no-default-groups --group dev
18
24
  - run: uv run ruff check .
19
25
  - run: uv run ruff format --check .
20
26
  - run: uv run mypy asher/ --ignore-missing-imports
@@ -30,5 +36,5 @@ jobs:
30
36
  steps:
31
37
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
32
38
  - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
33
- - run: uv sync --dev
39
+ - run: uv sync --no-default-groups --group dev
34
40
  - run: uv run pytest tests/ -v --tb=short
@@ -33,7 +33,7 @@ jobs:
33
33
 
34
34
  - name: Run Claude Code Review
35
35
  id: claude-review
36
- uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
36
+ uses: anthropics/claude-code-action@6b082c41935b4c8a3b8b0ef85ba4ba4d9eeb8975 # v1
37
37
  with:
38
38
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
39
39
  plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
@@ -32,7 +32,7 @@ jobs:
32
32
 
33
33
  - name: Run Claude Code
34
34
  id: claude
35
- uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
35
+ uses: anthropics/claude-code-action@6b082c41935b4c8a3b8b0ef85ba4ba4d9eeb8975 # v1
36
36
  with:
37
37
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38
38
 
@@ -8,13 +8,18 @@ on:
8
8
  permissions:
9
9
  contents: read
10
10
 
11
+ # See ci.yml: `uv run` would otherwise re-sync the `tray` group back in and try
12
+ # to build PyGObject from source.
13
+ env:
14
+ UV_NO_SYNC: "1"
15
+
11
16
  jobs:
12
17
  coverage:
13
18
  runs-on: ubuntu-latest
14
19
  steps:
15
20
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
16
21
  - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
17
- - run: uv sync --dev
22
+ - run: uv sync --no-default-groups --group dev
18
23
  - name: Run tests with coverage
19
24
  run: uv run pytest tests/ --cov=asher --cov-report=lcov --cov-report=term-missing
20
25
  - name: Upload to Coveralls
@@ -37,7 +37,7 @@ jobs:
37
37
  with:
38
38
  name: dist
39
39
  path: dist/
40
- - uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # release/v1
40
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
41
41
 
42
42
  github-release:
43
43
  name: Create GitHub Release
@@ -7,12 +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.2.0] - 2026-08-16
11
+
12
+ ### 🚀 Features
13
+
14
+ - Drop "Clean now" from the tray menu
15
+ - Refuse to start a watcher that has no credentials
16
+ - Open the dashboard from the tray
17
+ - Tone the tray icon to the desktop panel, and stop repeating the drawer
18
+ - [**breaking**] Take credentials only from the keyring outside dev mode
19
+ - Start the watcher at login, and notify (never install) on new releases
20
+ - Background watcher and system-tray icon for notifications
21
+
22
+ ### 🐛 Bug Fixes
23
+
24
+ - Keep the tray installed across a plain `uv run`
25
+ - Read the selected completion row against its own background
26
+ - *(deps)* Update dependency pylitterbot to v2025.6.4
27
+
28
+ ### 📚 Documentation
29
+
30
+ - Trim the README, move the packaging detail to CLAUDE.md
31
+ - Cover the tray's new behaviour and keyring-only credentials
32
+
33
+ ### 🧪 Tests
34
+
35
+ - Stop four tests reading the developer's own machine
36
+ - Cover the notifier fallback on every platform
37
+ - Stop the notification tests toasting the desktop
38
+ - Compare the Windows Run command against its normalised path
39
+ - Pin platform branches in the daemon tests
40
+
41
+ ### 👷 CI/CD
42
+
43
+ - Stop `uv run` re-syncing the tray group back in
44
+
10
45
  ## [1.1.0] - 2026-08-09
11
46
 
12
47
  ### 🚀 Features
13
48
 
14
- - *(headless)* Every robot command now runs without the TUI as `asher <command>`, with `--json` output and `--robot` selection; `--export` stays as a deprecated alias
15
- - *(theme)* Catppuccin Mocha throughout — one palette in `asher/theme.py` drives the TUI stylesheet and every Rich style, replacing ad-hoc hexes across eleven modules
49
+ - Headless command surface + Catppuccin Mocha palette
50
+
51
+ ### 🐛 Bug Fixes
52
+
53
+ - *(release)* Generate notes against the release tag, not the previous one
16
54
 
17
55
  ### 📚 Documentation
18
56
 
@@ -27,7 +27,14 @@ asher/
27
27
  constants.py STATUS_COLORS, ROBOT_MODELS
28
28
  theme.py Catppuccin Mocha palette + semantic roles (BACKGROUND, MUTED, DANGER, …) + CSS_VARIABLES/apply() — the only place a hex literal belongs
29
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 over plyer (fire/beep, always-safe no-op on failure/headless)
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)
31
38
  cats.py CATS dict (ASCII art)
32
39
  login_flow.py LoginFlow state machine — inline email/password prompt in command bar
33
40
  robot_protocol.py RobotProtocol structural Protocol for pylitterbot robot objects
@@ -62,6 +69,13 @@ tests/
62
69
  test_mcp_config.py Claude Desktop config read/write
63
70
  test_config.py runtime settings persistence — load/save/update over defaults
64
71
  test_notifications.py plyer toast + beep façade — always-safe no-op paths
72
+ test_watcher.py WatchState alert transitions + snapshot/deliver (pure) + watch() against a patched open_session + WatcherRunner threading
73
+ test_daemon.py pid-file bookkeeping, start/stop/status/dispatch with Popen and os.kill mocked
74
+ test_tray.py availability probing, menu/title text, icon rendering (tone inversion + status badge), and the headless fallbacks
75
+ test_launcher.py every platform's open path driven directly (Popen always mocked), terminal preference/fallback order
76
+ test_desktoptheme.py all four panel-tone probes driven directly (so each is covered on every runner) with the readers/registry mocked, plus the TTL cache
77
+ test_autostart.py all three login-item backends driven directly (so each is covered on every runner) with launchctl/systemctl/winreg mocked
78
+ test_updates.py version comparison, the once-a-day cache, HTTPS-only fetching, and a guard that the module spawns no process
65
79
  test_mcp_bridge.py mcp_bridge launcher credential/subprocess handling
66
80
  test_mcp_command.py /mcp slash command dispatch
67
81
  test_faults.py check_faults() — safety statuses, attribute faults, graceful degradation
@@ -79,16 +93,27 @@ tests/
79
93
 
80
94
  Priority order on startup:
81
95
 
82
- 1. **OS keyring** — set automatically after first interactive login
83
- 2. **`.env` file** — fallback for existing users / CI
96
+ 1. **OS keyring** — cached OAuth token, then email/password; set automatically after first interactive login
97
+ 2. **`.env` file** — **development only**, gated on `ASHER_CLI_DEV_MODE=true`
84
98
  3. **Inline login flow** — shown when no credentials found anywhere (email → password prompt in command bar)
85
99
 
86
- `.env` variable names (for fallback):
100
+ In a real install the keyring is the **only** source of credentials. `.env` is a
101
+ working-copy convenience: without dev mode a stray `LITTER_ROBOT_USER` in a shell
102
+ profile or a checked-out `.env` would silently outrank the keyring and
103
+ authenticate as the wrong account.
104
+
105
+ `.env` variable names (dev mode only):
87
106
  ```
107
+ ASHER_CLI_DEV_MODE=true
88
108
  LITTER_ROBOT_USER=...
89
109
  LITTER_ROBOT_PASSWORD=...
90
110
  ```
91
111
 
112
+ Every environment read goes through `_env_credentials()` in
113
+ `asher/connection/__init__.py` — the one place the gate is applied. Don't call
114
+ `os.getenv("LITTER_ROBOT_*")` anywhere else. `helpers.dev_mode()` is the shared
115
+ predicate (it also selects the `dev` version string).
116
+
92
117
  Keyring service name: `asher-cli`, keys `email` and `password`.
93
118
  Helper functions in `asher/connection/__init__.py`: `_keyring_load()`, `_keyring_save()`, `_keyring_delete()`.
94
119
 
@@ -98,14 +123,14 @@ Command names, slash-command names, and their args are not listed here — see t
98
123
 
99
124
  **Normal commands** (no prefix) are robot actions only; **slash commands** (`/` prefix) are app management only.
100
125
 
101
- `/refresh`, `/cat`, `/pet`, and `/notify` persist their settings to `~/.asher-cli/config.json` (via `asher.config.update()`), so they survive restarts. Credentials and the preferred-robot serial stay in the OS keyring; the config file holds only non-secret UI preferences.
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.
102
127
 
103
128
  Do not add robot-control commands as slash commands, and do not add app-management commands as bare commands.
104
129
 
105
130
  **Special cases** (accepted both with and without `/`):
106
131
  `exit`, `quit`, `q` — exit the app
107
132
 
108
- **Headless commands** (`asher <command>`) are a parallel registry in `asher/headless.py`: same robot actions, no Textual, plain-string + JSON output. Slash commands have no headless equivalent — they configure the TUI, which isn't running. A robot command worth scripting should exist in both registries; the shared logic lives in `RobotAdapter`, not in either command class.
133
+ **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.
109
134
 
110
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.
111
136
 
@@ -144,6 +169,10 @@ LoginScreen (ModalScreen) — available in auth.py but not the primary auth path
144
169
  | `_refresh_faults(robot)` | run `check_faults()`, render `#fault-banner`, log transitions; sets cat mode to `error` while faults active |
145
170
  | `_cycling_chip()` / `_start_cycle_timer()` / `_stop_cycle_timer()` / `_tick_cycle()` | `⟳ Cycling M:SS` chip + lazy 1s elapsed timer |
146
171
  | `_poll_status_interval()` | `@work` — poll fallback every 300s (5 min); WebSocket is primary |
172
+ | `watcher.watch()` | supervise a cloud session and notify on every change worth interrupting for — the TUI's `_notify_fault` without a TUI |
173
+ | `daemon.start/stop/status()` | detached watcher process control; `running_pid()` also gates the TUI's own toasts so alerts never double up |
174
+ | `daemon.run_foreground()` | the watcher process itself — claims the pid file (`_pid_file_held`) so a login-started watcher is still visible to `status`/`stop`, and refuses to be a second one |
175
+ | `autostart.enable/disable()` | register/remove the platform login item; `describe()` feeds the `watch status` line |
147
176
  | `_tick_cat()` | advances multi-frame cat animation every 0.4s |
148
177
  | `_dispatch_command(command, args)` | `@work` — calls `command.run(app, args)` from the registry |
149
178
  | `on_input_submitted()` | routes input to login flow or `_dispatch_command` via `CommandRegistry`; Enter on a partial `/cmd` completes it instead of submitting |
@@ -200,6 +229,14 @@ Commands that need a confirmed cloud state before showing a result (e.g. sleep/w
200
229
 
201
230
  **File naming convention:** no underscores in filenames (except Python-required `__init__.py` and `__main__.py`).
202
231
 
232
+ ## Tray packaging
233
+
234
+ The `tray` extra is what a published install uses (`pip install "asher-cli[tray]"`). It is **mirrored** as a `tray` dependency-group listed in `[tool.uv] default-groups`, because `uv run` re-syncs the environment to the default *groups* on every invocation and prunes everything else — extras included. Without the group, any plain `uv run` silently removed `pystray` and left the watcher drawing an icon no desktop was hosting. Keep the two lists in step when changing either.
235
+
236
+ CI opts out with `uv sync --no-default-groups --group dev` (`ci.yml`, `coverage.yml`): PyGObject builds from source and the runners have no GObject-introspection headers. `[tool.uv] default-extras` is not an option — uv 0.11 doesn't support that key.
237
+
238
+ PyGObject is what selects pystray's `_appindicator` backend. Without it pystray falls back to a legacy XEmbed icon that KDE Plasma and GNOME no longer host: the watcher runs, draws an icon, and nothing shows it, with no error anywhere. Check with `pystray.Icon.__module__`.
239
+
203
240
  ## Dev workflow
204
241
 
205
242
  See `[tool.poe.tasks]` in `pyproject.toml` for the full task list (`uv run poe <task>`).
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: asher-cli
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: Terminal dashboard for Litter Robot (LR3/LR4/LR5) via the Whisker cloud API
5
5
  Project-URL: Homepage, https://github.com/karanshukla/asher-cli
6
6
  Project-URL: Repository, https://github.com/karanshukla/asher-cli
@@ -40,13 +40,18 @@ 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.2
43
+ Requires-Dist: pylitterbot==2025.6.4
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
- Requires-Dist: pylitterbot[mcp]==2025.6.2; extra == 'mcp'
49
+ Requires-Dist: mcp<2; extra == 'mcp'
50
+ Requires-Dist: pylitterbot[mcp]==2025.6.4; extra == 'mcp'
51
+ Provides-Extra: tray
52
+ Requires-Dist: pillow>=10.0; extra == 'tray'
53
+ Requires-Dist: pygobject>=3.42; (sys_platform == 'linux') and extra == 'tray'
54
+ Requires-Dist: pystray>=0.19; extra == 'tray'
50
55
  Description-Content-Type: text/markdown
51
56
 
52
57
  # Asher CLI
@@ -72,10 +77,12 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
72
77
  - Cat panel with mode label + status badges (status chip, lock, night light, sleep, wait time) under the art
73
78
  - Scrollable activity-history pager — `history [count|all]` opens a full-screen, paginated view (arrow keys, `Page Up`/`Page Down`, `Home`/`End`); `c` copies the whole history to the clipboard; `q`/`Esc`/`Enter` to close
74
79
  - Commands: `clean`, `status`, `info`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto`, `night-light-brightness`, `panel-brightness <low|medium|high>` (LR4/LR5), `wait-time`, `power on|off`, `rename`, `insight`, `sleep-schedule`, plus LR5 extras (`privacy`, `volume`, `camera-audio`, `drawer-reset`), `history [count|all]`, `export [days|month]`, `help`, `quit`
75
- - Slash commands for app management: `/login`, `/logout`, `/robots`, `/robot <index|name>`, `/pets`, `/pet <index|name>`, `/cat on|off|colour <hex>`, `/refresh [seconds|off]`, `/config`, `/notify on|off|sound on|off|test`, `/version`, `/mcp on|off|status`, `/exit`
80
+ - Slash commands for app management: `/login`, `/logout`, `/robots`, `/robot <index|name>`, `/pets`, `/pet <index|name>`, `/cat on|off|colour <hex>`, `/refresh [seconds|off]`, `/config`, `/notify on|off|sound on|off|test`, `/watch start|stop|status|enable|disable`, `/version`, `/mcp on|off|status`, `/exit`
76
81
  - Slash-command tab completion — type `/` and a Claude Code-style overlay lists matching commands; `↑`/`↓` to move, `Tab` or `Enter` to accept, `Esc` to dismiss
77
82
  - Inline ghost-text completion for bare commands — type a prefix (`cle`) and the rest (`an`) appears greyed; `Tab` or `→` to accept → `clean`
78
83
  - 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
+ - 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
+ - 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
79
86
  - Catppuccin Mocha throughout — one palette in `asher/theme.py` drives the TUI stylesheet and every Rich style
80
87
  - Cat animation panel that reacts to robot state
81
88
  - Command history (↑/↓ arrows)
@@ -121,12 +128,7 @@ On first run, type `/login` at the command prompt. Your credentials are saved to
121
128
 
122
129
  To sign out: `/logout`
123
130
 
124
- `.env` fallback (for CI or existing users):
125
-
126
- ```env
127
- LITTER_ROBOT_USER=your@email.com
128
- LITTER_ROBOT_PASSWORD=yourpassword
129
- ```
131
+ The keyring is the only place an installed copy reads credentials from. Working from a clone, you can use `.env` instead by opting into dev mode (see [Development](#development)).
130
132
 
131
133
  ## Commands
132
134
 
@@ -170,8 +172,10 @@ LITTER_ROBOT_PASSWORD=yourpassword
170
172
  | `/cat on\|off` | Show or hide the cat animation panel |
171
173
  | `/cat colour <hex>` | Change the cat art colour (e.g. `/cat colour #ff79c6`); `color` also accepted; `/cat reset` to revert |
172
174
  | `/refresh [seconds\|off]` | Change the auto-poll interval or disable it (`/refresh 60`, `/refresh off`) |
173
- | `/config` | Show current runtime settings (robot, refresh rate, cat panel, active pet, notifications) |
175
+ | `/config` | Show current runtime settings (robot, refresh rate, cat panel, active pet, notifications, watcher) |
174
176
  | `/notify on\|off\|sound on\|off\|test` | Toggle desktop toast notifications for fault events (cat detected, pinch, motor fault, drawer full); `test` fires a sample toast |
177
+ | `/watch start\|stop\|status` | Start, stop, or check the background watcher that keeps notifications running after you close the dashboard |
178
+ | `/watch enable\|disable` | Register (or remove) the watcher as a login item so it starts automatically |
175
179
  | `/version` | Show version info (asher-cli, Python, pylitterbot, textual) |
176
180
  | `/mcp on\|off\|status` | Toggle the Litter-Robot MCP server entry in Claude Desktop |
177
181
  | `/exit` | Exit Asher CLI |
@@ -231,11 +235,80 @@ Exit codes for scripting:
231
235
  asher.exe export 7 --output C:\Users\me\litter-history.csv
232
236
  ```
233
237
 
238
+ ### Update notices
239
+
240
+ ```bash
241
+ asher update # is there a newer release?
242
+ asher update --json
243
+ ```
244
+
245
+ Asher checks PyPI's JSON API once a day (and on `/version`) and tells you when a newer release is published, along with the right command for however you installed it — `pipx upgrade`, `uv tool upgrade`, or `pip install -U`. Exit code is `0` when current, `10` when an update exists, so a script can branch on it.
246
+
247
+ **It never installs anything.** That's deliberate, not an omission:
248
+
249
+ - The process can't reliably tell how it was installed, so a self-upgrade would be guessing at the command that owns its own files.
250
+ - Replacing files under a live interpreter can load a mix of old and new modules, and the background watcher is exactly the long-lived process that would be running while its own code got swapped.
251
+ - Most importantly, auto-installing turns one compromised release into code running everywhere with nobody choosing the moment. Checking is safe; applying should be your call.
252
+
253
+ The check is read-only over HTTPS, fetches no code, and `update_check: false` in `~/.asher-cli/config.json` turns it off entirely.
254
+
234
255
  `asher --export [days]` remains as a deprecated alias for `asher export [days]`, so existing cron entries keep working.
235
256
 
257
+ ### Background notifications (`asher watch`)
258
+
259
+ The dashboard only notifies while it's on screen — close the terminal and the toasts stop. `asher watch` runs the same fault monitoring as a detached process, so notifications keep arriving with no terminal open at all.
260
+
261
+ ```bash
262
+ asher watch start # detach a watcher; the terminal is free to close
263
+ asher watch status # is one running? plus the last few log lines
264
+ asher watch stop # shut it down
265
+ asher watch enable # also start it at every login
266
+ asher watch disable # stop starting it at login
267
+ asher watch run # same thing in the foreground, for debugging
268
+ ```
269
+
270
+ You can also drive it from inside the dashboard with `/watch start|stop|status` — handy right before you quit. While a watcher is running the dashboard suppresses its own toasts, so you never get an alert twice.
271
+
272
+ It notifies on:
273
+
274
+ - **faults appearing and clearing** — cat detected, pinch, motor/position/gas faults, drawer full (same model-scoped detection as the dashboard's fault banner)
275
+ - **the waste drawer crossing 85%** — an early warning before the robot stops on its own (tune with `watch_drawer_threshold`)
276
+ - **the robot going offline or coming back**
277
+
278
+ Notifications respect your `/notify` settings, and re-read them on every alert — toggling notifications in the dashboard reaches a watcher that's already running. Faults already showing when the watcher starts are announced immediately; connectivity is only reported when it *changes*, so startup never claims a robot "came back" that was never away.
279
+
280
+ The watcher is built to be left alone: it reconnects with backoff through Wi-Fi drops, laptop sleeps, expired tokens, and a cloud that's still unreachable at login. Only genuinely missing credentials stop it — sign in with `/login` once first. Its log lives at `~/.asher-cli/watch.log` (`~` is your home directory; `%USERPROFILE%` on Windows).
281
+
282
+ **System-tray icon** (optional):
283
+
284
+ ```bash
285
+ pip install "asher-cli[tray]"
286
+ ```
287
+
288
+ `asher watch start` then shows a tray icon: a cat silhouette toned to your desktop panel, badged green healthy / red faulted / grey offline, with the full status in its tooltip. The menu offers **Open Asher** (also the left-click action, opens the dashboard in a new terminal), a **Notifications** toggle, and **Quit**. It carries no robot actions on purpose: a misclick next to the clock shouldn't be able to start a cycle. No extras, no desktop session, or no AppIndicator host all fall back to a headless watcher rather than failing. Pass `--no-tray` (or set `watch_tray: false`) to skip the icon.
289
+
290
+ On Linux the extra pulls in PyGObject, which builds from source and needs your distro's GObject-introspection and Cairo dev packages (`gobject-introspection-devel` + `cairo-gobject-devel` on Fedora, `libgirepository1.0-dev` + `libcairo2-dev` on Debian/Ubuntu).
291
+
292
+ **Start at login:**
293
+
294
+ ```bash
295
+ asher watch enable # register a login item, and start watching now
296
+ asher watch disable # remove it
297
+ ```
298
+
299
+ `enable` uses whatever already runs things at login on your machine, so there's no extra background service to trust:
300
+
301
+ | Platform | Login item | Where |
302
+ |---|---|---|
303
+ | macOS | launchd user agent | `~/Library/LaunchAgents/com.asher-cli.watcher.plist` |
304
+ | Linux | systemd user unit | `~/.config/systemd/user/asher-watch.service` |
305
+ | Windows | registry Run value | `HKCU\...\CurrentVersion\Run\AsherWatch` |
306
+
307
+ All three are per-user and need no administrator rights, and `disable` removes exactly what `enable` created. A crashed watcher is restarted; one you stopped with `asher watch stop` stays stopped. `asher watch status` shows whether autostart is registered. If your system has no user-level systemd (some minimal distros, WSL), `enable` says so and points you at putting `asher watch start` in your shell profile instead.
308
+
236
309
  ## Configuration
237
310
 
238
- Runtime settings persist across restarts in `~/.asher-cli/config.json`, so you don't have to re-apply `/refresh 60`, `/cat colour #ff79c6`, or `/pet 1` every launch. The file is auto-created on first change and holds six non-secret UI preferences:
311
+ Runtime settings persist across restarts in `~/.asher-cli/config.json`, so you don't have to re-apply `/refresh 60`, `/cat colour #ff79c6`, or `/pet 1` every launch. The file is auto-created on first change and holds these non-secret UI preferences:
239
312
 
240
313
  | Setting | Slash command | Default |
241
314
  |---|---|---|
@@ -245,6 +318,9 @@ Runtime settings persist across restarts in `~/.asher-cli/config.json`, so you d
245
318
  | `active_pet_index` | `/pet <index\|name>` | `0` |
246
319
  | `notifications` | `/notify on\|off` | `true` |
247
320
  | `notification_sound` | `/notify sound on\|off` | `false` |
321
+ | `watch_tray` | *(file only)* | `true` — set `false` to make `asher watch` always run without a tray icon |
322
+ | `watch_drawer_threshold` | *(file only)* | `85` — drawer % that triggers an early background warning |
323
+ | `update_check` | *(file only)* | `true` — set `false` to stop the daily PyPI version check |
248
324
 
249
325
  Credentials and the preferred-robot serial stay in the OS keyring; `.env` vars stay as env vars. You generally don't need to edit the file by hand — just use the slash commands — but it's plain JSON and safe to inspect or delete (deleting it restores defaults).
250
326
 
@@ -335,11 +411,13 @@ cp .env.example .env
335
411
  ```
336
412
 
337
413
  ```env
414
+ ASHER_CLI_DEV_MODE=true # shows version as "dev", and opts the two variables below in
338
415
  LITTER_ROBOT_USER=your@email.com
339
416
  LITTER_ROBOT_PASSWORD=yourpassword
340
- ASHER_CLI_DEV_MODE=true # sets version to "dev" instead of the installed package version
341
417
  ```
342
418
 
419
+ `ASHER_CLI_DEV_MODE` is what makes the credentials below readable at all — outside dev mode the app takes credentials only from the OS keyring.
420
+
343
421
  ### 3. Run with hot reload
344
422
 
345
423
  **CSS hot reload** — Textual's devtools watch inline `CSS` strings and `.tcss` files and reload them in-place without restarting:
@@ -21,10 +21,12 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
21
21
  - Cat panel with mode label + status badges (status chip, lock, night light, sleep, wait time) under the art
22
22
  - Scrollable activity-history pager — `history [count|all]` opens a full-screen, paginated view (arrow keys, `Page Up`/`Page Down`, `Home`/`End`); `c` copies the whole history to the clipboard; `q`/`Esc`/`Enter` to close
23
23
  - Commands: `clean`, `status`, `info`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto`, `night-light-brightness`, `panel-brightness <low|medium|high>` (LR4/LR5), `wait-time`, `power on|off`, `rename`, `insight`, `sleep-schedule`, plus LR5 extras (`privacy`, `volume`, `camera-audio`, `drawer-reset`), `history [count|all]`, `export [days|month]`, `help`, `quit`
24
- - Slash commands for app management: `/login`, `/logout`, `/robots`, `/robot <index|name>`, `/pets`, `/pet <index|name>`, `/cat on|off|colour <hex>`, `/refresh [seconds|off]`, `/config`, `/notify on|off|sound on|off|test`, `/version`, `/mcp on|off|status`, `/exit`
24
+ - Slash commands for app management: `/login`, `/logout`, `/robots`, `/robot <index|name>`, `/pets`, `/pet <index|name>`, `/cat on|off|colour <hex>`, `/refresh [seconds|off]`, `/config`, `/notify on|off|sound on|off|test`, `/watch start|stop|status|enable|disable`, `/version`, `/mcp on|off|status`, `/exit`
25
25
  - Slash-command tab completion — type `/` and a Claude Code-style overlay lists matching commands; `↑`/`↓` to move, `Tab` or `Enter` to accept, `Esc` to dismiss
26
26
  - Inline ghost-text completion for bare commands — type a prefix (`cle`) and the rest (`an`) appears greyed; `Tab` or `→` to accept → `clean`
27
27
  - Headless mode — every robot command also runs without the TUI (`asher status`, `asher clean`, `asher night-light auto`, `asher export 7`), with `--json` output and documented exit codes for cron / Task Scheduler / SSH
28
+ - 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
+ - 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
28
30
  - Catppuccin Mocha throughout — one palette in `asher/theme.py` drives the TUI stylesheet and every Rich style
29
31
  - Cat animation panel that reacts to robot state
30
32
  - Command history (↑/↓ arrows)
@@ -70,12 +72,7 @@ On first run, type `/login` at the command prompt. Your credentials are saved to
70
72
 
71
73
  To sign out: `/logout`
72
74
 
73
- `.env` fallback (for CI or existing users):
74
-
75
- ```env
76
- LITTER_ROBOT_USER=your@email.com
77
- LITTER_ROBOT_PASSWORD=yourpassword
78
- ```
75
+ The keyring is the only place an installed copy reads credentials from. Working from a clone, you can use `.env` instead by opting into dev mode (see [Development](#development)).
79
76
 
80
77
  ## Commands
81
78
 
@@ -119,8 +116,10 @@ LITTER_ROBOT_PASSWORD=yourpassword
119
116
  | `/cat on\|off` | Show or hide the cat animation panel |
120
117
  | `/cat colour <hex>` | Change the cat art colour (e.g. `/cat colour #ff79c6`); `color` also accepted; `/cat reset` to revert |
121
118
  | `/refresh [seconds\|off]` | Change the auto-poll interval or disable it (`/refresh 60`, `/refresh off`) |
122
- | `/config` | Show current runtime settings (robot, refresh rate, cat panel, active pet, notifications) |
119
+ | `/config` | Show current runtime settings (robot, refresh rate, cat panel, active pet, notifications, watcher) |
123
120
  | `/notify on\|off\|sound on\|off\|test` | Toggle desktop toast notifications for fault events (cat detected, pinch, motor fault, drawer full); `test` fires a sample toast |
121
+ | `/watch start\|stop\|status` | Start, stop, or check the background watcher that keeps notifications running after you close the dashboard |
122
+ | `/watch enable\|disable` | Register (or remove) the watcher as a login item so it starts automatically |
124
123
  | `/version` | Show version info (asher-cli, Python, pylitterbot, textual) |
125
124
  | `/mcp on\|off\|status` | Toggle the Litter-Robot MCP server entry in Claude Desktop |
126
125
  | `/exit` | Exit Asher CLI |
@@ -180,11 +179,80 @@ Exit codes for scripting:
180
179
  asher.exe export 7 --output C:\Users\me\litter-history.csv
181
180
  ```
182
181
 
182
+ ### Update notices
183
+
184
+ ```bash
185
+ asher update # is there a newer release?
186
+ asher update --json
187
+ ```
188
+
189
+ Asher checks PyPI's JSON API once a day (and on `/version`) and tells you when a newer release is published, along with the right command for however you installed it — `pipx upgrade`, `uv tool upgrade`, or `pip install -U`. Exit code is `0` when current, `10` when an update exists, so a script can branch on it.
190
+
191
+ **It never installs anything.** That's deliberate, not an omission:
192
+
193
+ - The process can't reliably tell how it was installed, so a self-upgrade would be guessing at the command that owns its own files.
194
+ - Replacing files under a live interpreter can load a mix of old and new modules, and the background watcher is exactly the long-lived process that would be running while its own code got swapped.
195
+ - Most importantly, auto-installing turns one compromised release into code running everywhere with nobody choosing the moment. Checking is safe; applying should be your call.
196
+
197
+ The check is read-only over HTTPS, fetches no code, and `update_check: false` in `~/.asher-cli/config.json` turns it off entirely.
198
+
183
199
  `asher --export [days]` remains as a deprecated alias for `asher export [days]`, so existing cron entries keep working.
184
200
 
201
+ ### Background notifications (`asher watch`)
202
+
203
+ The dashboard only notifies while it's on screen — close the terminal and the toasts stop. `asher watch` runs the same fault monitoring as a detached process, so notifications keep arriving with no terminal open at all.
204
+
205
+ ```bash
206
+ asher watch start # detach a watcher; the terminal is free to close
207
+ asher watch status # is one running? plus the last few log lines
208
+ asher watch stop # shut it down
209
+ asher watch enable # also start it at every login
210
+ asher watch disable # stop starting it at login
211
+ asher watch run # same thing in the foreground, for debugging
212
+ ```
213
+
214
+ You can also drive it from inside the dashboard with `/watch start|stop|status` — handy right before you quit. While a watcher is running the dashboard suppresses its own toasts, so you never get an alert twice.
215
+
216
+ It notifies on:
217
+
218
+ - **faults appearing and clearing** — cat detected, pinch, motor/position/gas faults, drawer full (same model-scoped detection as the dashboard's fault banner)
219
+ - **the waste drawer crossing 85%** — an early warning before the robot stops on its own (tune with `watch_drawer_threshold`)
220
+ - **the robot going offline or coming back**
221
+
222
+ Notifications respect your `/notify` settings, and re-read them on every alert — toggling notifications in the dashboard reaches a watcher that's already running. Faults already showing when the watcher starts are announced immediately; connectivity is only reported when it *changes*, so startup never claims a robot "came back" that was never away.
223
+
224
+ The watcher is built to be left alone: it reconnects with backoff through Wi-Fi drops, laptop sleeps, expired tokens, and a cloud that's still unreachable at login. Only genuinely missing credentials stop it — sign in with `/login` once first. Its log lives at `~/.asher-cli/watch.log` (`~` is your home directory; `%USERPROFILE%` on Windows).
225
+
226
+ **System-tray icon** (optional):
227
+
228
+ ```bash
229
+ pip install "asher-cli[tray]"
230
+ ```
231
+
232
+ `asher watch start` then shows a tray icon: a cat silhouette toned to your desktop panel, badged green healthy / red faulted / grey offline, with the full status in its tooltip. The menu offers **Open Asher** (also the left-click action, opens the dashboard in a new terminal), a **Notifications** toggle, and **Quit**. It carries no robot actions on purpose: a misclick next to the clock shouldn't be able to start a cycle. No extras, no desktop session, or no AppIndicator host all fall back to a headless watcher rather than failing. Pass `--no-tray` (or set `watch_tray: false`) to skip the icon.
233
+
234
+ On Linux the extra pulls in PyGObject, which builds from source and needs your distro's GObject-introspection and Cairo dev packages (`gobject-introspection-devel` + `cairo-gobject-devel` on Fedora, `libgirepository1.0-dev` + `libcairo2-dev` on Debian/Ubuntu).
235
+
236
+ **Start at login:**
237
+
238
+ ```bash
239
+ asher watch enable # register a login item, and start watching now
240
+ asher watch disable # remove it
241
+ ```
242
+
243
+ `enable` uses whatever already runs things at login on your machine, so there's no extra background service to trust:
244
+
245
+ | Platform | Login item | Where |
246
+ |---|---|---|
247
+ | macOS | launchd user agent | `~/Library/LaunchAgents/com.asher-cli.watcher.plist` |
248
+ | Linux | systemd user unit | `~/.config/systemd/user/asher-watch.service` |
249
+ | Windows | registry Run value | `HKCU\...\CurrentVersion\Run\AsherWatch` |
250
+
251
+ All three are per-user and need no administrator rights, and `disable` removes exactly what `enable` created. A crashed watcher is restarted; one you stopped with `asher watch stop` stays stopped. `asher watch status` shows whether autostart is registered. If your system has no user-level systemd (some minimal distros, WSL), `enable` says so and points you at putting `asher watch start` in your shell profile instead.
252
+
185
253
  ## Configuration
186
254
 
187
- Runtime settings persist across restarts in `~/.asher-cli/config.json`, so you don't have to re-apply `/refresh 60`, `/cat colour #ff79c6`, or `/pet 1` every launch. The file is auto-created on first change and holds six non-secret UI preferences:
255
+ Runtime settings persist across restarts in `~/.asher-cli/config.json`, so you don't have to re-apply `/refresh 60`, `/cat colour #ff79c6`, or `/pet 1` every launch. The file is auto-created on first change and holds these non-secret UI preferences:
188
256
 
189
257
  | Setting | Slash command | Default |
190
258
  |---|---|---|
@@ -194,6 +262,9 @@ Runtime settings persist across restarts in `~/.asher-cli/config.json`, so you d
194
262
  | `active_pet_index` | `/pet <index\|name>` | `0` |
195
263
  | `notifications` | `/notify on\|off` | `true` |
196
264
  | `notification_sound` | `/notify sound on\|off` | `false` |
265
+ | `watch_tray` | *(file only)* | `true` — set `false` to make `asher watch` always run without a tray icon |
266
+ | `watch_drawer_threshold` | *(file only)* | `85` — drawer % that triggers an early background warning |
267
+ | `update_check` | *(file only)* | `true` — set `false` to stop the daily PyPI version check |
197
268
 
198
269
  Credentials and the preferred-robot serial stay in the OS keyring; `.env` vars stay as env vars. You generally don't need to edit the file by hand — just use the slash commands — but it's plain JSON and safe to inspect or delete (deleting it restores defaults).
199
270
 
@@ -284,11 +355,13 @@ cp .env.example .env
284
355
  ```
285
356
 
286
357
  ```env
358
+ ASHER_CLI_DEV_MODE=true # shows version as "dev", and opts the two variables below in
287
359
  LITTER_ROBOT_USER=your@email.com
288
360
  LITTER_ROBOT_PASSWORD=yourpassword
289
- ASHER_CLI_DEV_MODE=true # sets version to "dev" instead of the installed package version
290
361
  ```
291
362
 
363
+ `ASHER_CLI_DEV_MODE` is what makes the credentials below readable at all — outside dev mode the app takes credentials only from the OS keyring.
364
+
292
365
  ### 3. Run with hot reload
293
366
 
294
367
  **CSS hot reload** — Textual's devtools watch inline `CSS` strings and `.tcss` files and reload them in-place without restarting: