asher-cli 0.0.3__tar.gz → 0.0.5__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 (68) hide show
  1. asher_cli-0.0.5/.claude/hooks/block-env.ps1 +9 -0
  2. asher_cli-0.0.5/.claude/settings.json +40 -0
  3. asher_cli-0.0.5/.claude/skills/pylitterbot-ref/SKILL.md +73 -0
  4. asher_cli-0.0.5/.claude/skills/release/SKILL.md +43 -0
  5. {asher_cli-0.0.3 → asher_cli-0.0.5}/.github/workflows/ci.yml +4 -4
  6. {asher_cli-0.0.3 → asher_cli-0.0.5}/.github/workflows/claude-code-review.yml +2 -2
  7. {asher_cli-0.0.3 → asher_cli-0.0.5}/.github/workflows/claude.yml +2 -2
  8. {asher_cli-0.0.3 → asher_cli-0.0.5}/.github/workflows/coverage.yml +3 -3
  9. {asher_cli-0.0.3 → asher_cli-0.0.5}/.github/workflows/release.yml +7 -7
  10. asher_cli-0.0.5/CLAUDE.md +208 -0
  11. {asher_cli-0.0.3 → asher_cli-0.0.5}/PKG-INFO +24 -9
  12. {asher_cli-0.0.3 → asher_cli-0.0.5}/README.md +19 -7
  13. {asher_cli-0.0.3 → asher_cli-0.0.5}/ROADMAP.md +440 -100
  14. {asher_cli-0.0.3 → asher_cli-0.0.5}/asher/app.py +19 -15
  15. asher_cli-0.0.5/asher/commands/__init__.py +1045 -0
  16. {asher_cli-0.0.3 → asher_cli-0.0.5}/asher/commands/base.py +5 -2
  17. {asher_cli-0.0.3 → asher_cli-0.0.5}/asher/connection/__init__.py +35 -9
  18. asher_cli-0.0.5/asher/constants.py +19 -0
  19. {asher_cli-0.0.3 → asher_cli-0.0.5}/asher/helpers.py +5 -8
  20. asher_cli-0.0.5/asher/mcp_bridge.py +41 -0
  21. asher_cli-0.0.5/asher/mcp_config.py +107 -0
  22. asher_cli-0.0.5/asher/monitoring/__init__.py +185 -0
  23. asher_cli-0.0.5/asher/robot_adapters.py +152 -0
  24. asher_cli-0.0.5/asher/robot_protocol.py +58 -0
  25. {asher_cli-0.0.3 → asher_cli-0.0.5}/asher/ui/__init__.py +65 -18
  26. {asher_cli-0.0.3 → asher_cli-0.0.5}/asher/ui/style.tcss +0 -10
  27. {asher_cli-0.0.3 → asher_cli-0.0.5}/pyproject.toml +10 -4
  28. asher_cli-0.0.5/renovate.json +22 -0
  29. {asher_cli-0.0.3 → asher_cli-0.0.5}/tests/conftest.py +2 -2
  30. {asher_cli-0.0.3 → asher_cli-0.0.5}/tests/test_app_pilot.py +6 -3
  31. {asher_cli-0.0.3 → asher_cli-0.0.5}/tests/test_commands_pilot.py +6 -3
  32. {asher_cli-0.0.3 → asher_cli-0.0.5}/tests/test_connection.py +3 -2
  33. asher_cli-0.0.5/tests/test_mcp_bridge.py +43 -0
  34. asher_cli-0.0.5/tests/test_mcp_command.py +117 -0
  35. asher_cli-0.0.5/tests/test_mcp_config.py +130 -0
  36. {asher_cli-0.0.3 → asher_cli-0.0.5}/tests/test_monitoring.py +39 -12
  37. asher_cli-0.0.5/tests/test_new_commands_pilot.py +392 -0
  38. asher_cli-0.0.5/tests/test_robot_adapters.py +393 -0
  39. {asher_cli-0.0.3 → asher_cli-0.0.5}/uv.lock +568 -82
  40. asher_cli-0.0.5/watchrun.py +56 -0
  41. asher_cli-0.0.3/.github/workflows/dependency-audit.yml +0 -37
  42. asher_cli-0.0.3/CLAUDE.md +0 -174
  43. asher_cli-0.0.3/asher/commands/__init__.py +0 -442
  44. asher_cli-0.0.3/asher/monitoring/__init__.py +0 -111
  45. {asher_cli-0.0.3 → asher_cli-0.0.5}/.claude/skills/textual/SKILL.md +0 -0
  46. {asher_cli-0.0.3 → asher_cli-0.0.5}/.env.example +0 -0
  47. {asher_cli-0.0.3 → asher_cli-0.0.5}/.githooks/pre-push +0 -0
  48. {asher_cli-0.0.3 → asher_cli-0.0.5}/.gitignore +0 -0
  49. {asher_cli-0.0.3 → asher_cli-0.0.5}/.vscode/launch.json +0 -0
  50. {asher_cli-0.0.3 → asher_cli-0.0.5}/.vscode/settings.json +0 -0
  51. {asher_cli-0.0.3 → asher_cli-0.0.5}/.vscode/tasks.json +0 -0
  52. {asher_cli-0.0.3 → asher_cli-0.0.5}/LICENSE +0 -0
  53. {asher_cli-0.0.3 → asher_cli-0.0.5}/app.py +0 -0
  54. {asher_cli-0.0.3 → asher_cli-0.0.5}/asher/__init__.py +0 -0
  55. {asher_cli-0.0.3 → asher_cli-0.0.5}/asher/__main__.py +0 -0
  56. {asher_cli-0.0.3 → asher_cli-0.0.5}/asher/auth.py +0 -0
  57. {asher_cli-0.0.3 → asher_cli-0.0.5}/asher/cats.py +0 -0
  58. {asher_cli-0.0.3 → asher_cli-0.0.5}/asher/login_flow.py +0 -0
  59. {asher_cli-0.0.3 → asher_cli-0.0.5}/asher/slash-commands/__init__.py +0 -0
  60. {asher_cli-0.0.3 → asher_cli-0.0.5}/requirements.txt +0 -0
  61. {asher_cli-0.0.3 → asher_cli-0.0.5}/test.py +0 -0
  62. {asher_cli-0.0.3 → asher_cli-0.0.5}/tests/__init__.py +0 -0
  63. {asher_cli-0.0.3 → asher_cli-0.0.5}/tests/test_auth.py +0 -0
  64. {asher_cli-0.0.3 → asher_cli-0.0.5}/tests/test_auth_pilot.py +0 -0
  65. {asher_cli-0.0.3 → asher_cli-0.0.5}/tests/test_cats.py +0 -0
  66. {asher_cli-0.0.3 → asher_cli-0.0.5}/tests/test_connection_mixin.py +0 -0
  67. {asher_cli-0.0.3 → asher_cli-0.0.5}/tests/test_ui.py +0 -0
  68. {asher_cli-0.0.3 → asher_cli-0.0.5}/tests/testhelpers.py +0 -0
@@ -0,0 +1,9 @@
1
+ $raw = [Console]::In.ReadToEnd()
2
+ try {
3
+ $j = $raw | ConvertFrom-Json
4
+ if ($j.file_path -match '\.env$') {
5
+ Write-Output "Blocked: .env contains real credentials — edit it manually if needed"
6
+ exit 2
7
+ }
8
+ } catch {}
9
+ exit 0
@@ -0,0 +1,40 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(uv run *)",
5
+ "Bash(git *)",
6
+ "Bash(python -c ' *)",
7
+ "Bash(python *)",
8
+ "mcp__plugin_context7_context7__resolve-library-id",
9
+ "mcp__plugin_context7_context7__query-docs",
10
+ "mcp__plugin_github_github__*",
11
+ "WebSearch",
12
+ "WebFetch",
13
+ "Bash(gh api *)"
14
+ ]
15
+ },
16
+ "hooks": {
17
+ "PostToolUse": [
18
+ {
19
+ "matcher": "Edit|Write",
20
+ "hooks": [
21
+ {
22
+ "type": "command",
23
+ "command": "uv run ruff check --fix . --quiet; uv run ruff format . --quiet"
24
+ }
25
+ ]
26
+ }
27
+ ],
28
+ "PreToolUse": [
29
+ {
30
+ "matcher": "Edit|Write",
31
+ "hooks": [
32
+ {
33
+ "type": "command",
34
+ "command": "powershell -NonInteractive -ExecutionPolicy Bypass -File .claude/hooks/block-env.ps1"
35
+ }
36
+ ]
37
+ }
38
+ ]
39
+ }
40
+ }
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: pylitterbot-ref
3
+ description: Reference for the confirmed pylitterbot API surface as used in asher-cli. Load before making changes to robot command handling, monitoring, or adapter code. Prevents attribute-name guessing and eliminates runtime inspection commands.
4
+ user-invocable: false
5
+ ---
6
+
7
+ # pylitterbot Confirmed API Surface
8
+
9
+ Load this skill whenever working on `asher/commands/`, `asher/monitoring/`, `asher/robot_adapters.py`, or anything that touches `robot.*`.
10
+
11
+ ## Robot attributes (LR4 confirmed)
12
+
13
+ ```python
14
+ robot.name # str
15
+ robot.serial # str
16
+ robot.is_online # bool
17
+ robot.status # LitterBoxStatus enum
18
+ robot.waste_drawer_level # int, 0–100
19
+ robot.sleep_mode_enabled # bool — NOT "sleeping"
20
+ robot.panel_lock_enabled # bool — NOT "panel_lockout"
21
+ robot.night_light_mode_enabled # bool
22
+ robot.last_seen # datetime | None
23
+ ```
24
+
25
+ ## Robot methods (all async, await required)
26
+
27
+ ```python
28
+ await robot.refresh()
29
+ await robot.start_cleaning()
30
+ await robot.set_sleep_mode(enabled: bool)
31
+ await robot.set_panel_lockout(enabled: bool)
32
+ await robot.set_night_light_brightness(brightness: int) # or:
33
+ await robot.set_night_light_mode(mode: NightLightMode)
34
+ await robot.get_activity_history(limit: int) # -> list[Activity]
35
+ ```
36
+
37
+ ## Activity objects
38
+
39
+ ```python
40
+ activity.timestamp # datetime
41
+ activity.action # LitterBoxStatus enum (same as robot.status values)
42
+ ```
43
+
44
+ ## Model detection
45
+
46
+ ```python
47
+ model_name = type(robot).__name__ # "LitterRobot4", "LitterRobot3", etc.
48
+ ```
49
+
50
+ ### Model-specific behaviour
51
+
52
+ | Feature | LR3 | LR4 | LR5 |
53
+ |---|---|---|---|
54
+ | `set_sleep_mode()` | Simple on/off | Per-weekday schedule | TBD |
55
+ | Night light | `set_night_light_brightness(int)` | `set_night_light_mode(NightLightMode)` | TBD |
56
+
57
+ Use `asher/robot_adapters.py` for all model-specific dispatch — do not branch on `type(robot).__name__` inline in command handlers.
58
+
59
+ ## Timing / cloud queuing gotcha
60
+
61
+ `sendLitterRobot4Command` returns when the cloud **queues** the command, not when the robot applies it.
62
+ - Toggle commands (lock, unlock, night-light): use optimistic UI updates, not `refresh()` after the call.
63
+ - State-unknown commands (sleep, wake): use `asyncio.sleep(2)` + `refresh()` + `_refresh_status()` as best-effort.
64
+
65
+ ## Safe attribute access pattern
66
+
67
+ ```python
68
+ level = getattr(robot, "waste_drawer_level", None)
69
+ if level is not None:
70
+ ...
71
+ ```
72
+
73
+ Use `getattr(..., default)` or `try/except AttributeError` for any attribute not guaranteed on all models.
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: release
3
+ description: Cut a new asher-cli release. Runs preflight checks, bumps the version with bump-my-version, pushes the tag, and confirms the GitHub Actions release workflow is triggered. Invoke when user says "release", "cut a release", "bump version", "publish", or "ship".
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Release Skill
8
+
9
+ Walk through the release process in order. Stop and report if any step fails.
10
+
11
+ ## Steps
12
+
13
+ 1. **Check working tree is clean**
14
+ Run `git status --porcelain`. If there are uncommitted changes, stop and tell the user to commit or stash them first.
15
+
16
+ 2. **Run full CI checks locally**
17
+ Run `uv run poe check` (lint + format + types + tests). All must pass before bumping.
18
+
19
+ 3. **Confirm the bump level**
20
+ Ask the user: patch / minor / major? Default to `patch` for bug fixes, `minor` for new features.
21
+
22
+ 4. **Bump the version**
23
+ Run `uv run bump-my-version bump <level>`.
24
+ This automatically:
25
+ - Updates `version` in `pyproject.toml`
26
+ - Runs `uv lock --no-sync` (pre-commit hook)
27
+ - Stages `uv.lock` and commits with a version bump message
28
+ - Creates a git tag `v<new_version>`
29
+
30
+ 5. **Push the commit and tag**
31
+ Run `git push origin main --tags`.
32
+ The `release.yml` GitHub Actions workflow triggers on new tags matching `v*`.
33
+
34
+ 6. **Confirm the workflow triggered**
35
+ Run `gh run list --workflow=release.yml --limit=3` and show the user the latest run status.
36
+
37
+ 7. **Report done**
38
+ Show the new version number and the expected PyPI URL once the workflow completes.
39
+
40
+ ## Notes
41
+ - The pre-push hook (`.githooks/pre-push`) runs ruff + mypy — it must be active: `git config core.hooksPath .githooks`
42
+ - If bump-my-version fails due to a dirty tree (e.g. uv.lock not staged), check that the `pre_commit_hooks` in `pyproject.toml` ran correctly
43
+ - Never `--skip` the pre-push hook
@@ -12,8 +12,8 @@ jobs:
12
12
  lint:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
- - uses: actions/checkout@v4
16
- - uses: astral-sh/setup-uv@v3
15
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
16
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
17
17
  - run: uv sync --dev
18
18
  - run: uv run ruff check .
19
19
  - run: uv run ruff format --check .
@@ -28,7 +28,7 @@ jobs:
28
28
  os: [ubuntu-latest, windows-latest, macos-latest]
29
29
  runs-on: ${{ matrix.os }}
30
30
  steps:
31
- - uses: actions/checkout@v4
32
- - uses: astral-sh/setup-uv@v3
31
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
32
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
33
33
  - run: uv sync --dev
34
34
  - run: uv run pytest tests/ -v --tb=short
@@ -27,13 +27,13 @@ jobs:
27
27
 
28
28
  steps:
29
29
  - name: Checkout repository
30
- uses: actions/checkout@v4
30
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
31
31
  with:
32
32
  fetch-depth: 1
33
33
 
34
34
  - name: Run Claude Code Review
35
35
  id: claude-review
36
- uses: anthropics/claude-code-action@v1
36
+ uses: anthropics/claude-code-action@428971d2ecd6e3a7cb0ee0da2a3a8b33fdb3678d # 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'
@@ -26,13 +26,13 @@ jobs:
26
26
  actions: read # Required for Claude to read CI results on PRs
27
27
  steps:
28
28
  - name: Checkout repository
29
- uses: actions/checkout@v4
29
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
30
30
  with:
31
31
  fetch-depth: 1
32
32
 
33
33
  - name: Run Claude Code
34
34
  id: claude
35
- uses: anthropics/claude-code-action@v1
35
+ uses: anthropics/claude-code-action@428971d2ecd6e3a7cb0ee0da2a3a8b33fdb3678d # v1
36
36
  with:
37
37
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38
38
 
@@ -12,13 +12,13 @@ jobs:
12
12
  coverage:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
- - uses: actions/checkout@v4
16
- - uses: astral-sh/setup-uv@v3
15
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
16
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
17
17
  - run: uv sync --dev
18
18
  - name: Run tests with coverage
19
19
  run: uv run pytest tests/ --cov=asher --cov-report=lcov --cov-report=term-missing
20
20
  - name: Upload to Coveralls
21
- uses: coverallsapp/github-action@v2
21
+ uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # v2
22
22
  with:
23
23
  github-token: ${{ secrets.GITHUB_TOKEN }}
24
24
  path-to-lcov: coverage.lcov
@@ -16,13 +16,13 @@ jobs:
16
16
  outputs:
17
17
  version: ${{ steps.version.outputs.version }}
18
18
  steps:
19
- - uses: actions/checkout@v4
19
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
20
20
  - name: Extract version from branch name
21
21
  id: version
22
22
  run: echo "version=${GITHUB_REF_NAME#release/}" >> "$GITHUB_OUTPUT"
23
- - uses: astral-sh/setup-uv@v3
23
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
24
24
  - run: uv build
25
- - uses: actions/upload-artifact@v4
25
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
26
26
  with:
27
27
  name: dist
28
28
  path: dist/
@@ -33,19 +33,19 @@ jobs:
33
33
  runs-on: ubuntu-latest
34
34
  environment: pypi
35
35
  steps:
36
- - uses: actions/download-artifact@v4
36
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
37
37
  with:
38
38
  name: dist
39
39
  path: dist/
40
- - uses: pypa/gh-action-pypi-publish@release/v1
40
+ - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
41
41
 
42
42
  github-release:
43
43
  name: Create GitHub Release
44
44
  needs: [build, publish]
45
45
  runs-on: ubuntu-latest
46
46
  steps:
47
- - uses: actions/checkout@v4
48
- - uses: actions/download-artifact@v4
47
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
48
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
49
49
  with:
50
50
  name: dist
51
51
  path: dist/
@@ -0,0 +1,208 @@
1
+ # Asher CLI — CLAUDE.md
2
+
3
+ Terminal dashboard for Litter Robot (LR3/LR4/LR5) via the Whisker cloud API.
4
+
5
+ ## Stack
6
+
7
+ - **Python 3.10+**
8
+ - **Textual** — async TUI framework (`textual>=0.47`)
9
+ - **pylitterbot** — unofficial Whisker API wrapper (`pylitterbot>=3.0`)
10
+ - **python-dotenv** — credential loading (`.env` fallback)
11
+ - **keyring>=24** — OS credential store (Windows Credential Manager / macOS Keychain / Linux Secret Service)
12
+
13
+ ## Tooling
14
+
15
+ - **uv** — dependency management and task runner (`uv sync`, `uv run`)
16
+ - **poethepoet** — task aliases via `uv run poe <task>`
17
+ - **ruff** — linter and formatter
18
+ - **mypy** — static type checking
19
+ - **pytest + pytest-asyncio + pytest-cov** — tests
20
+ - **textual-dev** — CSS hot reload devtools
21
+ - **watchfiles** — Python auto-restart on file change
22
+
23
+ ## Entry points
24
+
25
+ ```
26
+ python app.py # compatibility shim (calls asher/__main__.py)
27
+ python -m asher # run as module
28
+ asher # after: uv sync && uv run asher OR pip install -e .
29
+ ```
30
+
31
+ ## Package structure
32
+
33
+ ```
34
+ asher/
35
+ __init__.py
36
+ app.py AsherApp class (thin orchestrator — composes mixins)
37
+ auth.py LoginScreen modal (ModalScreen[tuple[str,str]]) — available, not primary flow
38
+ helpers.py fmt_ago(), drawer_bar(), ts(), robot_model() (pure, testable)
39
+ constants.py STATUS_COLORS, ROBOT_MODELS
40
+ cats.py CATS dict (ASCII art)
41
+ login_flow.py LoginFlow state machine — inline email/password prompt in command bar
42
+ robot_protocol.py RobotProtocol structural Protocol for pylitterbot robot objects
43
+ robot_adapters.py RobotAdapter ABC + LR3/LR4/LR5 subclasses + make_adapter() factory
44
+ mcp_config.py Claude Desktop config read/write for the /mcp slash command
45
+ mcp_bridge.py asher-mcp-launch console script — keyring-backed pylitterbot MCP launcher
46
+ __main__.py main() entry point
47
+ commands/
48
+ base.py Command ABC, SlashCommand, CommandRegistry
49
+ __init__.py CommandsMixin — all command classes + registry + dispatch
50
+ connection/ ConnectionMixin — keyring auth, _connect_worker, keyring helpers
51
+ monitoring/ MonitoringMixin — _poll_status_interval, _refresh_status
52
+ ui/ UIMixin — CSS, compose(), log helpers, cat helpers
53
+ slash-commands/ Convention doc
54
+
55
+ tests/
56
+ testhelpers.py unit tests for helpers.py
57
+ test_cats.py CATS dict structure
58
+ test_auth.py LoginScreen CSS / structure
59
+ test_auth_pilot.py Textual Pilot integration tests for LoginScreen
60
+ test_app_pilot.py Textual Pilot integration tests for AsherApp
61
+ test_commands_pilot.py Textual Pilot integration tests for command dispatch
62
+ test_connection.py keyring helper functions
63
+ test_connection_mixin.py ConnectionMixin structure
64
+ test_monitoring.py MonitoringMixin async methods
65
+ test_ui.py UIMixin constants, CSS, helper existence
66
+ test_mcp_config.py Claude Desktop config read/write
67
+ test_mcp_bridge.py mcp_bridge launcher credential/subprocess handling
68
+ test_mcp_command.py /mcp slash command dispatch
69
+
70
+ .github/workflows/
71
+ ci.yml ruff + mypy + pytest on every push/PR
72
+ ```
73
+
74
+ ## Credentials
75
+
76
+ Priority order on startup:
77
+
78
+ 1. **OS keyring** — set automatically after first interactive login
79
+ 2. **`.env` file** — fallback for existing users / CI
80
+ 3. **Inline login flow** — shown when no credentials found anywhere (email → password prompt in command bar)
81
+
82
+ `.env` variable names (for fallback):
83
+ ```
84
+ LITTER_ROBOT_USER=...
85
+ LITTER_ROBOT_PASSWORD=...
86
+ ```
87
+
88
+ Keyring service name: `asher-cli`, keys `email` and `password`.
89
+ Helper functions in `asher/connection/__init__.py`: `_keyring_load()`, `_keyring_save()`, `_keyring_delete()`.
90
+
91
+ ## MCP bridge (`/mcp`)
92
+
93
+ pylitterbot ships an optional MCP server (`pip install pylitterbot[mcp]`, run via `python -m pylitterbot.mcp`) that lets an MCP client like Claude Desktop monitor/control the robot directly. Its own docs configure it with plaintext credentials in the client's JSON config — asher-cli avoids that:
94
+
95
+ - `/mcp on|off|status` (in `asher/commands/__init__.py`, logic in `asher/mcp_config.py`) adds/removes an entry (named by `mcp_config._SERVER_NAME`) in every `claude_desktop_config.json` this OS's Claude Desktop might read (`mcp_config.config_paths()` — on Windows this includes both the standard installer path and any MSIX/Microsoft Store virtualized path). The entry's `command` is `sys.executable -m asher.mcp_bridge` — never the credentials themselves.
96
+ - `/mcp on` also auto-installs pylitterbot's `mcp` extra via `sys.executable -m pip install "pylitterbot[mcp]==<installed version>"` if the `mcp` package isn't importable yet.
97
+ - `asher/mcp_bridge.py` (console script `asher-mcp-launch`) is what Claude Desktop actually spawns. It reads email/password from the OS keyring at process start, sets them as `LITTER_ROBOT_USERNAME`/`LITTER_ROBOT_PASSWORD` (pylitterbot's expected names — note these differ from asher-cli's own `.env` var `LITTER_ROBOT_USER`) in that process's environment only, then execs `python -m pylitterbot.mcp`. No credentials ever touch the on-disk MCP config.
98
+ - `/mcp on` requires keyring credentials. If none are found but `.env` fallback credentials are set, it copies them into the keyring automatically (since the bridge process can't reliably discover a project-relative `.env` — Claude Desktop controls its working directory, not asher-cli).
99
+ - Requires the `mcp` extra: `uv sync --extra mcp` / `pip install asher-cli[mcp]`. Restart Claude Desktop after toggling for the change to take effect.
100
+
101
+ ## Command convention
102
+
103
+ **Normal commands** (no prefix) — robot actions only:
104
+ `clean`, `status`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto`, `night-light-brightness <level>`, `history`, `clear`, `help`
105
+
106
+ **Slash commands** (`/` prefix) — app management only:
107
+ `/login`, `/logout`, `/exit`, `/robots`, `/robot <index|name>`, `/pets`, `/pet <index|name>`, `/cat on|off|color <hex>`, `/refresh <seconds|off>`, `/config`, `/mcp on|off|status`
108
+
109
+ **Special cases** (accepted both with and without `/`):
110
+ `exit`, `quit`, `q` — exit the app
111
+
112
+ Do not add robot-control commands as slash commands, and do not add app-management commands as bare commands.
113
+
114
+ ## Architecture
115
+
116
+ **Status bar philosophy:**
117
+ - **Top row** — ambient/settings info (robot name, online badge, night light mode + brightness). Not time-critical.
118
+ - **Second row** — important operational state and cat data (drawer %, litter %, cat weight, last visit). Could be subject to change
119
+
120
+ ```
121
+ AsherApp (textual.App)
122
+ ├── #status-bar top dock — two rows (top: name/online/night-light/lock; bottom: drawer/litter/weight/visit)
123
+ ├── #main-area
124
+ │ ├── #log RichLog — scrollable event/command output
125
+ │ └── #cat-panel animated ASCII cat sidebar
126
+ └── #bottom-dock bottom dock
127
+ ├── #input-bar / #input-row command prompt ("> " label + CmdInput)
128
+ └── #hint-bar shortcut hints / login flow prompts
129
+
130
+ LoginScreen (ModalScreen) — available in auth.py but not the primary auth path
131
+ ```
132
+
133
+ ## Key methods
134
+
135
+ | Method | Purpose |
136
+ |---|---|
137
+ | `_connect_worker()` | `@work` — resolve credentials (keyring → .env → inline login), authenticate |
138
+ | `_refresh_status()` | update all header widgets from robot state |
139
+ | `_poll_status_interval()` | `@work` — poll fallback every 300s (5 min); WebSocket is primary |
140
+ | `_tick_cat()` | advances multi-frame cat animation every 0.4s |
141
+ | `_dispatch_command(command, args)` | `@work` — calls `command.run(app, args)` from the registry |
142
+ | `on_input_submitted()` | routes input to login flow or `_dispatch_command` via `CommandRegistry` |
143
+ | `_start_login_flow()` | begin inline email/password prompt in command bar |
144
+ | `_cmd_logout()` | delete creds from keyring, disconnect |
145
+ | `make_adapter(robot)` | factory in `robot_adapters.py` — returns correct `RobotAdapter` subclass |
146
+ | `_log_ok/err/warn/info()` | timestamped log helpers |
147
+
148
+ ## Robot compatibility
149
+
150
+ pylitterbot auto-detects robot type. Commands that differ per model are handled by `RobotAdapter` subclasses in `robot_adapters.py` — `make_adapter(robot)` returns the right one based on `type(robot).__name__`. Status-bar reads use `getattr(..., default)` for graceful degradation on older models. Tested API surface:
151
+
152
+ - `robot.name`, `robot.serial`, `robot.is_online`
153
+ - `robot.status` (LitterBoxStatus enum)
154
+ - `robot.waste_drawer_level` (0–100)
155
+ - `robot.sleep_mode_enabled`, `robot.panel_lock_enabled`, `robot.night_light_mode_enabled`
156
+ - `robot.last_seen` (datetime)
157
+ - `robot.refresh()`, `robot.start_cleaning()`
158
+ - `robot.set_sleep_mode(bool)`, `robot.set_panel_lockout(bool)`
159
+ - `robot.set_night_light_brightness(int)` or `robot.set_night_light_mode(NightLightMode)`
160
+ - `robot.get_activity_history(limit=int)` → list of `Activity` objects with `.timestamp` and `.action` (`LitterBoxStatus` enum)
161
+
162
+ ## Development notes
163
+
164
+ - Textual and pylitterbot are both asyncio-native — compose cleanly with `@work` tasks
165
+ - All command execution runs in `@work` async workers to keep the UI responsive
166
+ - Cat modes: `idle`, `happy`, `cleaning` (animated), `sleeping`, `error`, `full`
167
+ - `VERSION` is read from `importlib.metadata.version("asher-cli")` — falls back to `"dev"` when running from source
168
+ - The primary login path is the inline flow in `login_flow.py` (`LoginFlow` state machine: `IDLE` → `AWAITING_EMAIL` → `AWAITING_PASSWORD`). `LoginScreen` (`auth.py`) still exists as a modal but is not used in the current main flow.
169
+ - `LoginScreen` uses `event.stop()` on `Input.Submitted` and `Button.Pressed` to prevent bubbling to the App's `on_input_submitted` (relevant if re-activating the modal path)
170
+
171
+ ### IoT command timing — optimistic UI updates
172
+
173
+ `sendLitterRobot4Command` (and equivalents) return as soon as the cloud **queues** the command, not when the robot applies it. Calling `robot.refresh()` immediately after gets stale data. The fix for toggle/mode commands (lock, unlock, night-light on/off/auto) is to **update the status bar widget directly** after a successful API call, without waiting for a refresh — the WebSocket subscription will confirm the final state later. Do **not** add `asyncio.sleep(N)` + `refresh()` + `_refresh_status()` for these commands.
174
+
175
+ Commands that need a confirmed cloud state before showing a result (e.g. sleep/wake, where the state isn't known from the command arg alone) use `asyncio.sleep(2)` + `refresh()` + `_refresh_status()` as a best-effort workaround, accepting the risk of briefly stale display.
176
+
177
+ ## Common tasks
178
+
179
+ **Add a robot command:** create a class inheriting `Command` in `asher/commands/__init__.py`, implement `async def run(self, app, args)`, and call `_registry.register(MyCommand())`.
180
+
181
+ **Add a slash command:** create a class inheriting `SlashCommand` (sets `prefix = "/"`), implement `async def run(self, app, args)`, register it, and document in `asher/slash-commands/__init__.py`.
182
+
183
+ **Change poll interval:** `self.set_interval(300, ...)` in `on_mount`.
184
+
185
+ **Add a new cat state:** add entry to `CATS` dict in `asher/cats.py` (str for static, list[str] for animated), then call `_set_cat("name", "label")`.
186
+
187
+ **File naming convention:** no underscores in filenames (except Python-required `__init__.py` and `__main__.py`).
188
+
189
+ ## Dev workflow
190
+
191
+ ```bash
192
+ uv sync # install all deps (including dev group)
193
+ uv run poe dev # run with CSS hot reload (textual --dev)
194
+ uv run poe watch # run with Python auto-restart on file change (watchfiles)
195
+ uv run poe test # run test suite
196
+ uv run poe check # ruff + mypy + pytest (same as CI)
197
+ uv run poe fix # auto-fix ruff issues
198
+ ```
199
+
200
+ Pre-push hook (`.githooks/pre-push`) runs: ruff check → ruff format --check → mypy. Tests are not in the hook — run them manually.
201
+
202
+ ## Testing notes
203
+
204
+ - Pilot-based integration tests use `app.run_test()` with `await pilot.pause()` before querying widgets
205
+ - Helper app wrappers for screens must **not** start with `Test` (pytest will try to collect them); use e.g. `LoginTestApp`
206
+ - Mock external deps with `unittest.mock.AsyncMock` for async robot/account methods
207
+ - `from pylitterbot import Account` is a local import inside `_connect_worker` — patch it at `pylitterbot.Account`, not `asher.connection.Account`
208
+ - Coverage: ~76% overall; main gaps are async exception paths and `_connect_worker` auth flow
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asher-cli
3
- Version: 0.0.3
3
+ Version: 0.0.5
4
4
  Summary: Terminal dashboard for Litter Robot (LR3/LR4/LR5) via the Whisker cloud API
5
5
  License: MIT License
6
6
 
@@ -35,16 +35,19 @@ Classifier: Programming Language :: Python :: 3.12
35
35
  Classifier: Topic :: Home Automation
36
36
  Requires-Python: >=3.10
37
37
  Requires-Dist: keyring>=24.0.0
38
- Requires-Dist: pylitterbot>=3.0.0
38
+ Requires-Dist: pylitterbot==2025.5.0
39
39
  Requires-Dist: python-dotenv>=1.0.0
40
40
  Requires-Dist: rich>=13.0.0
41
41
  Requires-Dist: textual>=0.47.0
42
+ Requires-Dist: tzlocal>=5.4
43
+ Provides-Extra: mcp
44
+ Requires-Dist: pylitterbot[mcp]==2025.5.0; extra == 'mcp'
42
45
  Description-Content-Type: text/markdown
43
46
 
44
47
  # Asher CLI
45
48
 
46
49
  [![PyPI](https://img.shields.io/badge/PyPI-asher--cli-blue?logo=pypi&logoColor=white)](https://pypi.org/project/asher-cli/)
47
- ![Version](https://img.shields.io/pypi/v/asher-cli?label=version)
50
+ ![PyPI - Version](https://img.shields.io/pypi/v/asher-cli?label=latest%20version)
48
51
  [![Python](https://img.shields.io/pypi/pyversions/asher-cli)](https://pypi.org/project/asher-cli/)
49
52
  [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
50
53
  [![CI](https://github.com/karanshukla/asher-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/karanshukla/asher-cli/actions/workflows/ci.yml)
@@ -60,11 +63,11 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
60
63
  - Live status bar — unit name, online/offline, drawer fill level, last activity, cat weight
61
64
  - Human-readable robot status — translates raw API states into plain English (`Ready`, `Cleaning`, `Cat Detected`, `Drawer Full`, etc.)
62
65
  - Scrollable activity log with timestamps
63
- - Commands: `clean`, `status`, `lock`, `unlock`, `sleep`, `wake`, `night-light`, `history`, `help`, `quit`
64
- - Slash commands for app management: `/login`, `/logout`, `/exit`
66
+ - Commands: `clean`, `status`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto`, `night-light-brightness`, `history`, `export [days|month]`, `help`, `quit`
67
+ - Slash commands for app management: `/login`, `/logout`, `/robots`, `/robot <index|name>`, `/pets`, `/pet <index|name>`, `/cat on|off|color <hex>`, `/refresh [seconds|off]`, `/config`, `/exit`
65
68
  - Cat animation panel that reacts to robot state
66
69
  - Command history (↑/↓ arrows)
67
- - Auto-refreshes every 30 seconds
70
+ - Real-time updates via WebSocket; 5-minute poll fallback
68
71
 
69
72
  ## Install
70
73
 
@@ -124,8 +127,10 @@ LITTER_ROBOT_PASSWORD=yourpassword
124
127
  | `info` | Full dump of all robot properties (serial, firmware, all settings) |
125
128
  | `lock` / `unlock` | Toggle panel lockout |
126
129
  | `sleep` / `wake` | Toggle sleep mode |
127
- | `night-light on\|off` | Toggle night light |
130
+ | `night-light on\|off\|auto` | Set night light mode |
131
+ | `night-light-brightness <level>` | Set brightness (LR5: 0-100; LR4: 25/50/100) |
128
132
  | `history` | Show last 25 activity events |
133
+ | `export [days\|month]` | Export activity history to CSV in `~/Downloads` (default: 30 days) |
129
134
  | `clear` | Clear the log |
130
135
  | `help` | Show command list |
131
136
  | `quit` | Exit |
@@ -136,6 +141,14 @@ LITTER_ROBOT_PASSWORD=yourpassword
136
141
  |---|---|
137
142
  | `/login` | Sign in or switch accounts |
138
143
  | `/logout` | Sign out and clear saved credentials |
144
+ | `/robots` | List all robots on the account |
145
+ | `/robot <index\|name>` | Switch active robot (selection persists to keyring) |
146
+ | `/pets` | List all pets on the account |
147
+ | `/pet <index\|name>` | Switch which pet's name/weight shows in the status bar |
148
+ | `/cat on\|off` | Show or hide the cat animation panel |
149
+ | `/cat color <hex>` | Change the cat art colour (e.g. `/cat color #ff79c6`); `/cat reset` to revert |
150
+ | `/refresh [seconds\|off]` | Change the auto-poll interval or disable it (`/refresh 60`, `/refresh off`) |
151
+ | `/config` | Show current runtime settings (robot, refresh rate, cat panel, active pet) |
139
152
  | `/exit` | Exit Asher CLI |
140
153
 
141
154
  **Keyboard shortcuts:** `Ctrl+L` clears the log, `Ctrl+C` quits.
@@ -143,12 +156,14 @@ LITTER_ROBOT_PASSWORD=yourpassword
143
156
  ## Releasing
144
157
 
145
158
  ```bash
146
- # bump version, commit, and tag in one step
159
+ # bump version, commit, and tag in one step, then push with tags
147
160
  uv run bump-my-version bump patch # 0.0.1 → 0.0.2
148
161
  uv run bump-my-version bump minor # 0.0.2 → 0.1.0
149
162
  uv run bump-my-version bump major # 0.1.0 → 1.0.0
150
163
 
151
- # then push the release branch to trigger PyPI publish
164
+ git push && git push --tags
165
+
166
+ # then push the release branch to trigger PyPI publish with the new semver
152
167
  git checkout -b release/0.0.2
153
168
  git push origin release/0.0.2
154
169
  ```
@@ -1,7 +1,7 @@
1
1
  # Asher CLI
2
2
 
3
3
  [![PyPI](https://img.shields.io/badge/PyPI-asher--cli-blue?logo=pypi&logoColor=white)](https://pypi.org/project/asher-cli/)
4
- ![Version](https://img.shields.io/pypi/v/asher-cli?label=version)
4
+ ![PyPI - Version](https://img.shields.io/pypi/v/asher-cli?label=latest%20version)
5
5
  [![Python](https://img.shields.io/pypi/pyversions/asher-cli)](https://pypi.org/project/asher-cli/)
6
6
  [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7
7
  [![CI](https://github.com/karanshukla/asher-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/karanshukla/asher-cli/actions/workflows/ci.yml)
@@ -17,11 +17,11 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
17
17
  - Live status bar — unit name, online/offline, drawer fill level, last activity, cat weight
18
18
  - Human-readable robot status — translates raw API states into plain English (`Ready`, `Cleaning`, `Cat Detected`, `Drawer Full`, etc.)
19
19
  - Scrollable activity log with timestamps
20
- - Commands: `clean`, `status`, `lock`, `unlock`, `sleep`, `wake`, `night-light`, `history`, `help`, `quit`
21
- - Slash commands for app management: `/login`, `/logout`, `/exit`
20
+ - Commands: `clean`, `status`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto`, `night-light-brightness`, `history`, `export [days|month]`, `help`, `quit`
21
+ - Slash commands for app management: `/login`, `/logout`, `/robots`, `/robot <index|name>`, `/pets`, `/pet <index|name>`, `/cat on|off|color <hex>`, `/refresh [seconds|off]`, `/config`, `/exit`
22
22
  - Cat animation panel that reacts to robot state
23
23
  - Command history (↑/↓ arrows)
24
- - Auto-refreshes every 30 seconds
24
+ - Real-time updates via WebSocket; 5-minute poll fallback
25
25
 
26
26
  ## Install
27
27
 
@@ -81,8 +81,10 @@ LITTER_ROBOT_PASSWORD=yourpassword
81
81
  | `info` | Full dump of all robot properties (serial, firmware, all settings) |
82
82
  | `lock` / `unlock` | Toggle panel lockout |
83
83
  | `sleep` / `wake` | Toggle sleep mode |
84
- | `night-light on\|off` | Toggle night light |
84
+ | `night-light on\|off\|auto` | Set night light mode |
85
+ | `night-light-brightness <level>` | Set brightness (LR5: 0-100; LR4: 25/50/100) |
85
86
  | `history` | Show last 25 activity events |
87
+ | `export [days\|month]` | Export activity history to CSV in `~/Downloads` (default: 30 days) |
86
88
  | `clear` | Clear the log |
87
89
  | `help` | Show command list |
88
90
  | `quit` | Exit |
@@ -93,6 +95,14 @@ LITTER_ROBOT_PASSWORD=yourpassword
93
95
  |---|---|
94
96
  | `/login` | Sign in or switch accounts |
95
97
  | `/logout` | Sign out and clear saved credentials |
98
+ | `/robots` | List all robots on the account |
99
+ | `/robot <index\|name>` | Switch active robot (selection persists to keyring) |
100
+ | `/pets` | List all pets on the account |
101
+ | `/pet <index\|name>` | Switch which pet's name/weight shows in the status bar |
102
+ | `/cat on\|off` | Show or hide the cat animation panel |
103
+ | `/cat color <hex>` | Change the cat art colour (e.g. `/cat color #ff79c6`); `/cat reset` to revert |
104
+ | `/refresh [seconds\|off]` | Change the auto-poll interval or disable it (`/refresh 60`, `/refresh off`) |
105
+ | `/config` | Show current runtime settings (robot, refresh rate, cat panel, active pet) |
96
106
  | `/exit` | Exit Asher CLI |
97
107
 
98
108
  **Keyboard shortcuts:** `Ctrl+L` clears the log, `Ctrl+C` quits.
@@ -100,12 +110,14 @@ LITTER_ROBOT_PASSWORD=yourpassword
100
110
  ## Releasing
101
111
 
102
112
  ```bash
103
- # bump version, commit, and tag in one step
113
+ # bump version, commit, and tag in one step, then push with tags
104
114
  uv run bump-my-version bump patch # 0.0.1 → 0.0.2
105
115
  uv run bump-my-version bump minor # 0.0.2 → 0.1.0
106
116
  uv run bump-my-version bump major # 0.1.0 → 1.0.0
107
117
 
108
- # then push the release branch to trigger PyPI publish
118
+ git push && git push --tags
119
+
120
+ # then push the release branch to trigger PyPI publish with the new semver
109
121
  git checkout -b release/0.0.2
110
122
  git push origin release/0.0.2
111
123
  ```