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.
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.github/workflows/ci.yml +8 -2
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.github/workflows/claude-code-review.yml +1 -1
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.github/workflows/claude.yml +1 -1
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.github/workflows/coverage.yml +6 -1
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.github/workflows/release.yml +1 -1
- {asher_cli-1.1.0 → asher_cli-1.2.0}/CHANGELOG.md +40 -2
- {asher_cli-1.1.0 → asher_cli-1.2.0}/CLAUDE.md +43 -6
- {asher_cli-1.1.0 → asher_cli-1.2.0}/PKG-INFO +92 -14
- {asher_cli-1.1.0 → asher_cli-1.2.0}/README.md +83 -10
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/__main__.py +63 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/app.py +1 -0
- asher_cli-1.2.0/asher/autostart.py +370 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/commands/__init__.py +58 -12
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/completion.py +3 -1
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/config.py +10 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/connection/__init__.py +44 -12
- asher_cli-1.2.0/asher/daemon.py +429 -0
- asher_cli-1.2.0/asher/desktoptheme.py +192 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/helpers.py +25 -0
- asher_cli-1.2.0/asher/launcher.py +133 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/monitoring/__init__.py +14 -0
- asher_cli-1.2.0/asher/notifications.py +120 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/slash-commands/__init__.py +2 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/theme.py +6 -0
- asher_cli-1.2.0/asher/tray.py +264 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/ui/__init__.py +18 -3
- asher_cli-1.2.0/asher/updates.py +181 -0
- asher_cli-1.2.0/asher/watcher.py +393 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/pyproject.toml +31 -4
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_app_pilot.py +3 -0
- asher_cli-1.2.0/tests/test_autostart.py +341 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_commands_pilot.py +3 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_config.py +4 -4
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_connection.py +77 -0
- asher_cli-1.2.0/tests/test_daemon.py +445 -0
- asher_cli-1.2.0/tests/test_desktoptheme.py +228 -0
- asher_cli-1.2.0/tests/test_launcher.py +203 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_mcp_command.py +14 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_monitoring.py +7 -0
- asher_cli-1.2.0/tests/test_notifications.py +176 -0
- asher_cli-1.2.0/tests/test_tray.py +305 -0
- asher_cli-1.2.0/tests/test_updates.py +272 -0
- asher_cli-1.2.0/tests/test_watcher.py +379 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/testhelpers.py +21 -1
- {asher_cli-1.1.0 → asher_cli-1.2.0}/uv.lock +1121 -683
- asher_cli-1.1.0/asher/notifications.py +0 -48
- asher_cli-1.1.0/tests/test_notifications.py +0 -82
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.claude/hooks/block-env.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.claude/settings.json +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.claude/skills/mcp-bridge/SKILL.md +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.claude/skills/pylitterbot-ref/SKILL.md +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.claude/skills/textual/SKILL.md +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.env.example +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.githooks/pre-push +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.github/FUNDING.yml +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.github/pull_request_template.md +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.github/workflows/bandit.yml +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.gitignore +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.vscode/launch.json +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.vscode/settings.json +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/.vscode/tasks.json +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/CODEOWNERS +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/LICENSE +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/app.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/__init__.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/activity_labels.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/auth.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/cats.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/commands/base.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/constants.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/export.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/faults.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/headless.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/history_view.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/login_flow.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/mcp_bridge.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/mcp_config.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/robot_adapters.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/robot_protocol.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/asher/ui/style.tcss +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/cliff.toml +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/ROADMAP.md +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/roadmap-archive/README.md +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/roadmap-archive/cat-panel-badges.md +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/roadmap-archive/config-persistence.md +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/roadmap-archive/desktop-notifications.md +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/roadmap-archive/fault-monitoring.md +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/roadmap-archive/headless-export.md +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/roadmap-archive/history-export.md +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/docs/roadmap-archive/tab-completion.md +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/renovate.json +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/requirements.txt +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/test.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/__init__.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/conftest.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_activity_labels.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_auth.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_auth_pilot.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_cats.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_completion.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_connection_mixin.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_export.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_faults.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_headless.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_history_view.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_lr5_commands.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_mcp_bridge.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_mcp_config.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_missing_robot_commands.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_new_commands_pilot.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_robot_adapters.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_ui.py +0 -0
- {asher_cli-1.1.0 → asher_cli-1.2.0}/tests/test_version_command.py +0 -0
- {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@
|
|
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@
|
|
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@
|
|
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
|
-
-
|
|
15
|
-
|
|
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
|
|
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** —
|
|
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`
|
|
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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: asher-cli
|
|
3
|
-
Version: 1.
|
|
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.
|
|
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:
|
|
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`
|
|
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
|
|
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`
|
|
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
|
|
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:
|