asher-cli 0.0.2__tar.gz → 0.0.4__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 (66) hide show
  1. asher_cli-0.0.4/.claude/hooks/block-env.ps1 +9 -0
  2. asher_cli-0.0.4/.claude/settings.json +40 -0
  3. asher_cli-0.0.4/.claude/skills/pylitterbot-ref/SKILL.md +73 -0
  4. asher_cli-0.0.4/.claude/skills/release/SKILL.md +43 -0
  5. asher_cli-0.0.4/.env.example +3 -0
  6. {asher_cli-0.0.2 → asher_cli-0.0.4}/.githooks/pre-push +0 -3
  7. {asher_cli-0.0.2 → asher_cli-0.0.4}/.github/workflows/ci.yml +5 -4
  8. asher_cli-0.0.4/.github/workflows/claude-code-review.yml +44 -0
  9. asher_cli-0.0.4/.github/workflows/claude.yml +50 -0
  10. asher_cli-0.0.4/.github/workflows/coverage.yml +24 -0
  11. {asher_cli-0.0.2 → asher_cli-0.0.4}/.github/workflows/release.yml +7 -7
  12. asher_cli-0.0.4/CLAUDE.md +193 -0
  13. {asher_cli-0.0.2 → asher_cli-0.0.4}/PKG-INFO +91 -23
  14. asher_cli-0.0.4/README.md +218 -0
  15. {asher_cli-0.0.2 → asher_cli-0.0.4}/ROADMAP.md +335 -98
  16. {asher_cli-0.0.2 → asher_cli-0.0.4}/asher/app.py +26 -19
  17. asher_cli-0.0.4/asher/cats.py +93 -0
  18. asher_cli-0.0.4/asher/commands/__init__.py +947 -0
  19. asher_cli-0.0.4/asher/commands/base.py +84 -0
  20. {asher_cli-0.0.2 → asher_cli-0.0.4}/asher/connection/__init__.py +74 -14
  21. asher_cli-0.0.4/asher/constants.py +19 -0
  22. {asher_cli-0.0.2 → asher_cli-0.0.4}/asher/helpers.py +5 -8
  23. asher_cli-0.0.4/asher/login_flow.py +58 -0
  24. asher_cli-0.0.4/asher/monitoring/__init__.py +185 -0
  25. asher_cli-0.0.4/asher/robot_adapters.py +152 -0
  26. asher_cli-0.0.4/asher/robot_protocol.py +58 -0
  27. asher_cli-0.0.4/asher/slash-commands/__init__.py +19 -0
  28. asher_cli-0.0.4/asher/ui/__init__.py +300 -0
  29. asher_cli-0.0.4/asher/ui/style.tcss +124 -0
  30. {asher_cli-0.0.2 → asher_cli-0.0.4}/pyproject.toml +21 -4
  31. asher_cli-0.0.4/renovate.json +22 -0
  32. {asher_cli-0.0.2 → asher_cli-0.0.4}/tests/conftest.py +2 -2
  33. asher_cli-0.0.4/tests/test_app_pilot.py +214 -0
  34. asher_cli-0.0.4/tests/test_auth.py +61 -0
  35. asher_cli-0.0.4/tests/test_auth_pilot.py +95 -0
  36. asher_cli-0.0.4/tests/test_cats.py +64 -0
  37. asher_cli-0.0.4/tests/test_commands_pilot.py +293 -0
  38. asher_cli-0.0.4/tests/test_connection.py +96 -0
  39. asher_cli-0.0.4/tests/test_connection_mixin.py +19 -0
  40. asher_cli-0.0.4/tests/test_monitoring.py +152 -0
  41. asher_cli-0.0.4/tests/test_new_commands_pilot.py +392 -0
  42. asher_cli-0.0.4/tests/test_robot_adapters.py +393 -0
  43. asher_cli-0.0.4/tests/test_ui.py +146 -0
  44. {asher_cli-0.0.2 → asher_cli-0.0.4}/tests/testhelpers.py +29 -1
  45. {asher_cli-0.0.2 → asher_cli-0.0.4}/uv.lock +502 -8
  46. asher_cli-0.0.4/watchrun.py +56 -0
  47. asher_cli-0.0.2/CLAUDE.md +0 -137
  48. asher_cli-0.0.2/README.md +0 -151
  49. asher_cli-0.0.2/asher/cats.py +0 -40
  50. asher_cli-0.0.2/asher/commands/__init__.py +0 -350
  51. asher_cli-0.0.2/asher/monitoring/__init__.py +0 -111
  52. asher_cli-0.0.2/asher/slash-commands/__init__.py +0 -26
  53. asher_cli-0.0.2/asher/ui/__init__.py +0 -279
  54. {asher_cli-0.0.2 → asher_cli-0.0.4}/.claude/skills/textual/SKILL.md +0 -0
  55. {asher_cli-0.0.2 → asher_cli-0.0.4}/.gitignore +0 -0
  56. {asher_cli-0.0.2 → asher_cli-0.0.4}/.vscode/launch.json +0 -0
  57. {asher_cli-0.0.2 → asher_cli-0.0.4}/.vscode/settings.json +0 -0
  58. {asher_cli-0.0.2 → asher_cli-0.0.4}/.vscode/tasks.json +0 -0
  59. {asher_cli-0.0.2 → asher_cli-0.0.4}/LICENSE +0 -0
  60. {asher_cli-0.0.2 → asher_cli-0.0.4}/app.py +0 -0
  61. {asher_cli-0.0.2 → asher_cli-0.0.4}/asher/__init__.py +0 -0
  62. {asher_cli-0.0.2 → asher_cli-0.0.4}/asher/__main__.py +0 -0
  63. {asher_cli-0.0.2 → asher_cli-0.0.4}/asher/auth.py +0 -0
  64. {asher_cli-0.0.2 → asher_cli-0.0.4}/requirements.txt +0 -0
  65. {asher_cli-0.0.2 → asher_cli-0.0.4}/test.py +0 -0
  66. {asher_cli-0.0.2 → asher_cli-0.0.4}/tests/__init__.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
@@ -0,0 +1,3 @@
1
+ LITTER_ROBOT_USER=email
2
+ LITTER_ROBOT_PASSWORD=password
3
+ ASHER_CLI_DEV_MODE=true
@@ -10,7 +10,4 @@ uv run ruff format --check .
10
10
  echo "► mypy"
11
11
  uv run mypy asher/ --ignore-missing-imports
12
12
 
13
- echo "► pytest"
14
- uv run pytest tests/ -v --tb=short
15
-
16
13
  echo "✓ all checks passed"
@@ -12,13 +12,14 @@ 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 .
20
20
  - run: uv run mypy asher/ --ignore-missing-imports
21
21
 
22
+
22
23
  test:
23
24
  needs: lint
24
25
  strategy:
@@ -27,7 +28,7 @@ jobs:
27
28
  os: [ubuntu-latest, windows-latest, macos-latest]
28
29
  runs-on: ${{ matrix.os }}
29
30
  steps:
30
- - uses: actions/checkout@v4
31
- - uses: astral-sh/setup-uv@v3
31
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
32
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
32
33
  - run: uv sync --dev
33
34
  - run: uv run pytest tests/ -v --tb=short
@@ -0,0 +1,44 @@
1
+ name: Claude Code Review
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, ready_for_review, reopened]
6
+ # Optional: Only run on specific file changes
7
+ # paths:
8
+ # - "src/**/*.ts"
9
+ # - "src/**/*.tsx"
10
+ # - "src/**/*.js"
11
+ # - "src/**/*.jsx"
12
+
13
+ jobs:
14
+ claude-review:
15
+ # Optional: Filter by PR author
16
+ if: |
17
+ github.event.pull_request.user.login == 'external-contributor' ||
18
+ github.event.pull_request.user.login == 'new-developer' ||
19
+ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
20
+
21
+ runs-on: ubuntu-latest
22
+ permissions:
23
+ contents: read
24
+ pull-requests: read
25
+ issues: read
26
+ id-token: write
27
+
28
+ steps:
29
+ - name: Checkout repository
30
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
31
+ with:
32
+ fetch-depth: 1
33
+
34
+ - name: Run Claude Code Review
35
+ id: claude-review
36
+ uses: anthropics/claude-code-action@428971d2ecd6e3a7cb0ee0da2a3a8b33fdb3678d # v1
37
+ with:
38
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
39
+ plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
40
+ plugins: 'code-review@claude-code-plugins'
41
+ prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
42
+ # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
43
+ # or https://code.claude.com/docs/en/cli-reference for available options
44
+
@@ -0,0 +1,50 @@
1
+ name: Claude Code
2
+
3
+ on:
4
+ issue_comment:
5
+ types: [created]
6
+ pull_request_review_comment:
7
+ types: [created]
8
+ issues:
9
+ types: [opened, assigned]
10
+ pull_request_review:
11
+ types: [submitted]
12
+
13
+ jobs:
14
+ claude:
15
+ if: |
16
+ (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17
+ (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18
+ (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19
+ (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
20
+ runs-on: ubuntu-latest
21
+ permissions:
22
+ contents: read
23
+ pull-requests: read
24
+ issues: read
25
+ id-token: write
26
+ actions: read # Required for Claude to read CI results on PRs
27
+ steps:
28
+ - name: Checkout repository
29
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
30
+ with:
31
+ fetch-depth: 1
32
+
33
+ - name: Run Claude Code
34
+ id: claude
35
+ uses: anthropics/claude-code-action@428971d2ecd6e3a7cb0ee0da2a3a8b33fdb3678d # v1
36
+ with:
37
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38
+
39
+ # This is an optional setting that allows Claude to read CI results on PRs
40
+ additional_permissions: |
41
+ actions: read
42
+
43
+ # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
44
+ # prompt: 'Update the pull request description to include a summary of changes.'
45
+
46
+ # Optional: Add claude_args to customize behavior and configuration
47
+ # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
48
+ # or https://code.claude.com/docs/en/cli-reference for available options
49
+ # claude_args: '--allowed-tools Bash(gh pr *)'
50
+
@@ -0,0 +1,24 @@
1
+ name: Coverage
2
+
3
+ on:
4
+ schedule:
5
+ - cron: "0 6 * * *" # daily at 06:00 UTC
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ coverage:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
16
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
17
+ - run: uv sync --dev
18
+ - name: Run tests with coverage
19
+ run: uv run pytest tests/ --cov=asher --cov-report=lcov --cov-report=term-missing
20
+ - name: Upload to Coveralls
21
+ uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # v2
22
+ with:
23
+ github-token: ${{ secrets.GITHUB_TOKEN }}
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,193 @@
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
+ __main__.py main() entry point
45
+ commands/
46
+ base.py Command ABC, SlashCommand, CommandRegistry
47
+ __init__.py CommandsMixin — all command classes + registry + dispatch
48
+ connection/ ConnectionMixin — keyring auth, _connect_worker, keyring helpers
49
+ monitoring/ MonitoringMixin — _poll_status_interval, _refresh_status
50
+ ui/ UIMixin — CSS, compose(), log helpers, cat helpers
51
+ slash-commands/ Convention doc
52
+
53
+ tests/
54
+ testhelpers.py unit tests for helpers.py
55
+ test_cats.py CATS dict structure
56
+ test_auth.py LoginScreen CSS / structure
57
+ test_auth_pilot.py Textual Pilot integration tests for LoginScreen
58
+ test_app_pilot.py Textual Pilot integration tests for AsherApp
59
+ test_commands_pilot.py Textual Pilot integration tests for command dispatch
60
+ test_connection.py keyring helper functions
61
+ test_connection_mixin.py ConnectionMixin structure
62
+ test_monitoring.py MonitoringMixin async methods
63
+ test_ui.py UIMixin constants, CSS, helper existence
64
+
65
+ .github/workflows/
66
+ ci.yml ruff + mypy + pytest on every push/PR
67
+ ```
68
+
69
+ ## Credentials
70
+
71
+ Priority order on startup:
72
+
73
+ 1. **OS keyring** — set automatically after first interactive login
74
+ 2. **`.env` file** — fallback for existing users / CI
75
+ 3. **Inline login flow** — shown when no credentials found anywhere (email → password prompt in command bar)
76
+
77
+ `.env` variable names (for fallback):
78
+ ```
79
+ LITTER_ROBOT_USER=...
80
+ LITTER_ROBOT_PASSWORD=...
81
+ ```
82
+
83
+ Keyring service name: `asher-cli`, keys `email` and `password`.
84
+ Helper functions in `asher/connection/__init__.py`: `_keyring_load()`, `_keyring_save()`, `_keyring_delete()`.
85
+
86
+ ## Command convention
87
+
88
+ **Normal commands** (no prefix) — robot actions only:
89
+ `clean`, `status`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto`, `night-light-brightness <level>`, `history`, `clear`, `help`
90
+
91
+ **Slash commands** (`/` prefix) — app management only:
92
+ `/login`, `/logout`, `/exit`, `/robots`, `/robot <index|name>`
93
+
94
+ **Special cases** (accepted both with and without `/`):
95
+ `exit`, `quit`, `q` — exit the app
96
+
97
+ Do not add robot-control commands as slash commands, and do not add app-management commands as bare commands.
98
+
99
+ ## Architecture
100
+
101
+ **Status bar philosophy:**
102
+ - **Top row** — ambient/settings info (robot name, online badge, night light mode + brightness). Not time-critical.
103
+ - **Second row** — important operational state and cat data (drawer %, litter %, cat weight, last visit). Could be subject to change
104
+
105
+ ```
106
+ AsherApp (textual.App)
107
+ ├── #status-bar top dock — two rows (top: name/online/night-light/lock; bottom: drawer/litter/weight/visit)
108
+ ├── #main-area
109
+ │ ├── #log RichLog — scrollable event/command output
110
+ │ └── #cat-panel animated ASCII cat sidebar
111
+ └── #bottom-dock bottom dock
112
+ ├── #input-bar / #input-row command prompt ("> " label + CmdInput)
113
+ └── #hint-bar shortcut hints / login flow prompts
114
+
115
+ LoginScreen (ModalScreen) — available in auth.py but not the primary auth path
116
+ ```
117
+
118
+ ## Key methods
119
+
120
+ | Method | Purpose |
121
+ |---|---|
122
+ | `_connect_worker()` | `@work` — resolve credentials (keyring → .env → inline login), authenticate |
123
+ | `_refresh_status()` | update all header widgets from robot state |
124
+ | `_poll_status_interval()` | `@work` — poll fallback every 300s (5 min); WebSocket is primary |
125
+ | `_tick_cat()` | advances multi-frame cat animation every 0.4s |
126
+ | `_dispatch_command(command, args)` | `@work` — calls `command.run(app, args)` from the registry |
127
+ | `on_input_submitted()` | routes input to login flow or `_dispatch_command` via `CommandRegistry` |
128
+ | `_start_login_flow()` | begin inline email/password prompt in command bar |
129
+ | `_cmd_logout()` | delete creds from keyring, disconnect |
130
+ | `make_adapter(robot)` | factory in `robot_adapters.py` — returns correct `RobotAdapter` subclass |
131
+ | `_log_ok/err/warn/info()` | timestamped log helpers |
132
+
133
+ ## Robot compatibility
134
+
135
+ 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:
136
+
137
+ - `robot.name`, `robot.serial`, `robot.is_online`
138
+ - `robot.status` (LitterBoxStatus enum)
139
+ - `robot.waste_drawer_level` (0–100)
140
+ - `robot.sleep_mode_enabled`, `robot.panel_lock_enabled`, `robot.night_light_mode_enabled`
141
+ - `robot.last_seen` (datetime)
142
+ - `robot.refresh()`, `robot.start_cleaning()`
143
+ - `robot.set_sleep_mode(bool)`, `robot.set_panel_lockout(bool)`
144
+ - `robot.set_night_light_brightness(int)` or `robot.set_night_light_mode(NightLightMode)`
145
+ - `robot.get_activity_history(limit=int)` → list of `Activity` objects with `.timestamp` and `.action` (`LitterBoxStatus` enum)
146
+
147
+ ## Development notes
148
+
149
+ - Textual and pylitterbot are both asyncio-native — compose cleanly with `@work` tasks
150
+ - All command execution runs in `@work` async workers to keep the UI responsive
151
+ - Cat modes: `idle`, `happy`, `cleaning` (animated), `sleeping`, `error`, `full`
152
+ - `VERSION` is read from `importlib.metadata.version("asher-cli")` — falls back to `"dev"` when running from source
153
+ - 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.
154
+ - `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)
155
+
156
+ ### IoT command timing — optimistic UI updates
157
+
158
+ `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.
159
+
160
+ 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.
161
+
162
+ ## Common tasks
163
+
164
+ **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())`.
165
+
166
+ **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`.
167
+
168
+ **Change poll interval:** `self.set_interval(300, ...)` in `on_mount`.
169
+
170
+ **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")`.
171
+
172
+ **File naming convention:** no underscores in filenames (except Python-required `__init__.py` and `__main__.py`).
173
+
174
+ ## Dev workflow
175
+
176
+ ```bash
177
+ uv sync # install all deps (including dev group)
178
+ uv run poe dev # run with CSS hot reload (textual --dev)
179
+ uv run poe watch # run with Python auto-restart on file change (watchfiles)
180
+ uv run poe test # run test suite
181
+ uv run poe check # ruff + mypy + pytest (same as CI)
182
+ uv run poe fix # auto-fix ruff issues
183
+ ```
184
+
185
+ Pre-push hook (`.githooks/pre-push`) runs: ruff check → ruff format --check → mypy. Tests are not in the hook — run them manually.
186
+
187
+ ## Testing notes
188
+
189
+ - Pilot-based integration tests use `app.run_test()` with `await pilot.pause()` before querying widgets
190
+ - Helper app wrappers for screens must **not** start with `Test` (pytest will try to collect them); use e.g. `LoginTestApp`
191
+ - Mock external deps with `unittest.mock.AsyncMock` for async robot/account methods
192
+ - `from pylitterbot import Account` is a local import inside `_connect_worker` — patch it at `pylitterbot.Account`, not `asher.connection.Account`
193
+ - Coverage: ~76% overall; main gaps are async exception paths and `_connect_worker` auth flow