asher-cli 0.0.3__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 (62) 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.3 → asher_cli-0.0.4}/.github/workflows/ci.yml +4 -4
  6. {asher_cli-0.0.3 → asher_cli-0.0.4}/.github/workflows/claude-code-review.yml +2 -2
  7. {asher_cli-0.0.3 → asher_cli-0.0.4}/.github/workflows/claude.yml +2 -2
  8. {asher_cli-0.0.3 → asher_cli-0.0.4}/.github/workflows/coverage.yml +3 -3
  9. {asher_cli-0.0.3 → asher_cli-0.0.4}/.github/workflows/release.yml +7 -7
  10. {asher_cli-0.0.3 → asher_cli-0.0.4}/CLAUDE.md +44 -25
  11. {asher_cli-0.0.3 → asher_cli-0.0.4}/PKG-INFO +18 -7
  12. {asher_cli-0.0.3 → asher_cli-0.0.4}/README.md +15 -5
  13. {asher_cli-0.0.3 → asher_cli-0.0.4}/ROADMAP.md +335 -100
  14. {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/app.py +19 -15
  15. asher_cli-0.0.4/asher/commands/__init__.py +947 -0
  16. {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/commands/base.py +5 -2
  17. {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/connection/__init__.py +35 -9
  18. asher_cli-0.0.4/asher/constants.py +19 -0
  19. {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/helpers.py +5 -8
  20. asher_cli-0.0.4/asher/monitoring/__init__.py +185 -0
  21. asher_cli-0.0.4/asher/robot_adapters.py +152 -0
  22. asher_cli-0.0.4/asher/robot_protocol.py +58 -0
  23. {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/ui/__init__.py +65 -18
  24. {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/ui/style.tcss +0 -10
  25. {asher_cli-0.0.3 → asher_cli-0.0.4}/pyproject.toml +6 -4
  26. asher_cli-0.0.4/renovate.json +22 -0
  27. {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/conftest.py +2 -2
  28. {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_app_pilot.py +6 -3
  29. {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_commands_pilot.py +6 -3
  30. {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_connection.py +3 -2
  31. {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_monitoring.py +39 -12
  32. asher_cli-0.0.4/tests/test_new_commands_pilot.py +392 -0
  33. asher_cli-0.0.4/tests/test_robot_adapters.py +393 -0
  34. {asher_cli-0.0.3 → asher_cli-0.0.4}/uv.lock +100 -77
  35. asher_cli-0.0.4/watchrun.py +56 -0
  36. asher_cli-0.0.3/.github/workflows/dependency-audit.yml +0 -37
  37. asher_cli-0.0.3/asher/commands/__init__.py +0 -442
  38. asher_cli-0.0.3/asher/monitoring/__init__.py +0 -111
  39. {asher_cli-0.0.3 → asher_cli-0.0.4}/.claude/skills/textual/SKILL.md +0 -0
  40. {asher_cli-0.0.3 → asher_cli-0.0.4}/.env.example +0 -0
  41. {asher_cli-0.0.3 → asher_cli-0.0.4}/.githooks/pre-push +0 -0
  42. {asher_cli-0.0.3 → asher_cli-0.0.4}/.gitignore +0 -0
  43. {asher_cli-0.0.3 → asher_cli-0.0.4}/.vscode/launch.json +0 -0
  44. {asher_cli-0.0.3 → asher_cli-0.0.4}/.vscode/settings.json +0 -0
  45. {asher_cli-0.0.3 → asher_cli-0.0.4}/.vscode/tasks.json +0 -0
  46. {asher_cli-0.0.3 → asher_cli-0.0.4}/LICENSE +0 -0
  47. {asher_cli-0.0.3 → asher_cli-0.0.4}/app.py +0 -0
  48. {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/__init__.py +0 -0
  49. {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/__main__.py +0 -0
  50. {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/auth.py +0 -0
  51. {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/cats.py +0 -0
  52. {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/login_flow.py +0 -0
  53. {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/slash-commands/__init__.py +0 -0
  54. {asher_cli-0.0.3 → asher_cli-0.0.4}/requirements.txt +0 -0
  55. {asher_cli-0.0.3 → asher_cli-0.0.4}/test.py +0 -0
  56. {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/__init__.py +0 -0
  57. {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_auth.py +0 -0
  58. {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_auth_pilot.py +0 -0
  59. {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_cats.py +0 -0
  60. {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_connection_mixin.py +0 -0
  61. {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_ui.py +0 -0
  62. {asher_cli-0.0.3 → asher_cli-0.0.4}/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/
@@ -34,15 +34,21 @@ asher # after: uv sync && uv run asher OR pip install -e .
34
34
  asher/
35
35
  __init__.py
36
36
  app.py AsherApp class (thin orchestrator — composes mixins)
37
- auth.py LoginScreen modal (ModalScreen[tuple[str,str]])
38
- helpers.py fmt_ago(), drawer_bar(), ts(), STATUS_COLORS (pure, testable)
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
39
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
40
44
  __main__.py main() entry point
41
- commands/ CommandsMixin — robot commands + slash-command dispatch
45
+ commands/
46
+ base.py Command ABC, SlashCommand, CommandRegistry
47
+ __init__.py CommandsMixin — all command classes + registry + dispatch
42
48
  connection/ ConnectionMixin — keyring auth, _connect_worker, keyring helpers
43
49
  monitoring/ MonitoringMixin — _poll_status_interval, _refresh_status
44
50
  ui/ UIMixin — CSS, compose(), log helpers, cat helpers
45
- slash-commands/ Convention doc + future slash-command registry
51
+ slash-commands/ Convention doc
46
52
 
47
53
  tests/
48
54
  testhelpers.py unit tests for helpers.py
@@ -66,7 +72,7 @@ Priority order on startup:
66
72
 
67
73
  1. **OS keyring** — set automatically after first interactive login
68
74
  2. **`.env` file** — fallback for existing users / CI
69
- 3. **Interactive `LoginScreen`** — shown when no credentials found anywhere
75
+ 3. **Inline login flow** — shown when no credentials found anywhere (email → password prompt in command bar)
70
76
 
71
77
  `.env` variable names (for fallback):
72
78
  ```
@@ -80,10 +86,10 @@ Helper functions in `asher/connection/__init__.py`: `_keyring_load()`, `_keyring
80
86
  ## Command convention
81
87
 
82
88
  **Normal commands** (no prefix) — robot actions only:
83
- `clean`, `status`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off`, `history`, `clear`, `help`
89
+ `clean`, `status`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto`, `night-light-brightness <level>`, `history`, `clear`, `help`
84
90
 
85
91
  **Slash commands** (`/` prefix) — app management only:
86
- `/login`, `/logout`, `/exit`
92
+ `/login`, `/logout`, `/exit`, `/robots`, `/robot <index|name>`
87
93
 
88
94
  **Special cases** (accepted both with and without `/`):
89
95
  `exit`, `quit`, `q` — exit the app
@@ -92,61 +98,74 @@ Do not add robot-control commands as slash commands, and do not add app-manageme
92
98
 
93
99
  ## Architecture
94
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
+
95
105
  ```
96
106
  AsherApp (textual.App)
97
- ├── #status-bar top dock — robot name, online badge, drawer bar, last seen, cat weight
107
+ ├── #status-bar top dock — two rows (top: name/online/night-light/lock; bottom: drawer/litter/weight/visit)
98
108
  ├── #main-area
99
109
  │ ├── #log RichLog — scrollable event/command output
100
110
  │ └── #cat-panel animated ASCII cat sidebar
101
- └── #input-bar bottom dock — command prompt input
111
+ └── #bottom-dock bottom dock
112
+ ├── #input-bar / #input-row command prompt ("> " label + CmdInput)
113
+ └── #hint-bar shortcut hints / login flow prompts
102
114
 
103
- LoginScreen (ModalScreen) — shown on first run or after /login
115
+ LoginScreen (ModalScreen) — available in auth.py but not the primary auth path
104
116
  ```
105
117
 
106
118
  ## Key methods
107
119
 
108
120
  | Method | Purpose |
109
121
  |---|---|
110
- | `_connect_worker()` | `@work` — resolve credentials (keyring → .env → LoginScreen), authenticate |
122
+ | `_connect_worker()` | `@work` — resolve credentials (keyring → .env → inline login), authenticate |
111
123
  | `_refresh_status()` | update all header widgets from robot state |
112
- | `_poll_status_interval()` | `@work` — auto-refresh every 30s |
113
- | `_tick_cat()` | advances multi-frame cat animation every 0.9s |
114
- | `_run_cmd(raw)` | `@work` — parse and dispatch robot commands |
115
- | `_run_slash_cmd(raw)` | `@work` parse and dispatch slash commands |
116
- | `_cmd_login()` | show LoginScreen, save creds, reconnect (no exit) |
117
- | `_cmd_logout()` | delete creds from keyring, exit |
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 |
118
131
  | `_log_ok/err/warn/info()` | timestamped log helpers |
119
132
 
120
133
  ## Robot compatibility
121
134
 
122
- pylitterbot auto-detects robot type. Any attribute/method missing on a given model is caught by `getattr(..., default)` or `try/except`, so the UI degrades gracefully. Tested API surface:
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:
123
136
 
124
137
  - `robot.name`, `robot.serial`, `robot.is_online`
125
138
  - `robot.status` (LitterBoxStatus enum)
126
139
  - `robot.waste_drawer_level` (0–100)
127
- - `robot.sleeping`, `robot.panel_lockout`, `robot.night_light_mode_enabled`
140
+ - `robot.sleep_mode_enabled`, `robot.panel_lock_enabled`, `robot.night_light_mode_enabled`
128
141
  - `robot.last_seen` (datetime)
129
142
  - `robot.refresh()`, `robot.start_cleaning()`
130
143
  - `robot.set_sleep_mode(bool)`, `robot.set_panel_lockout(bool)`
131
144
  - `robot.set_night_light_brightness(int)` or `robot.set_night_light_mode(NightLightMode)`
132
- - `robot.get_activity(limit=int)` → list of activity objects with `.timestamp`, `.weight`, `.action_value`
145
+ - `robot.get_activity_history(limit=int)` → list of `Activity` objects with `.timestamp` and `.action` (`LitterBoxStatus` enum)
133
146
 
134
147
  ## Development notes
135
148
 
136
149
  - Textual and pylitterbot are both asyncio-native — compose cleanly with `@work` tasks
137
150
  - All command execution runs in `@work` async workers to keep the UI responsive
138
151
  - Cat modes: `idle`, `happy`, `cleaning` (animated), `sleeping`, `error`, `full`
139
- - `_cmd_nightlight` tries `set_night_light_brightness` first, falls back to `set_night_light_mode`
140
152
  - `VERSION` is read from `importlib.metadata.version("asher-cli")` — falls back to `"dev"` when running from source
141
- - `LoginScreen` uses `event.stop()` on `Input.Submitted` and `Button.Pressed` to prevent bubbling to the App's `on_input_submitted`
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.
142
161
 
143
162
  ## Common tasks
144
163
 
145
- **Add a robot command:** add a branch in `_run_cmd()` in `asher/commands/__init__.py` and implement `_cmd_<name>()`.
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())`.
146
165
 
147
- **Add a slash command:** add a branch in `_run_slash_cmd()` in `asher/commands/__init__.py`, implement `_slash_<name>()`, and add to `slash_cmds` list in `_show_help()`. Document in `asher/slash-commands/__init__.py`.
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`.
148
167
 
149
- **Change poll interval:** `self.set_interval(30, ...)` in `on_mount`.
168
+ **Change poll interval:** `self.set_interval(300, ...)` in `on_mount`.
150
169
 
151
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")`.
152
171
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asher-cli
3
- Version: 0.0.3
3
+ Version: 0.0.4
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,17 @@ 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
42
43
  Description-Content-Type: text/markdown
43
44
 
44
45
  # Asher CLI
45
46
 
46
47
  [![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)
48
+ ![PyPI - Version](https://img.shields.io/pypi/v/asher-cli?label=latest%20version)
48
49
  [![Python](https://img.shields.io/pypi/pyversions/asher-cli)](https://pypi.org/project/asher-cli/)
49
50
  [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
50
51
  [![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 +61,11 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
60
61
  - Live status bar — unit name, online/offline, drawer fill level, last activity, cat weight
61
62
  - Human-readable robot status — translates raw API states into plain English (`Ready`, `Cleaning`, `Cat Detected`, `Drawer Full`, etc.)
62
63
  - 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`
64
+ - Commands: `clean`, `status`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto`, `night-light-brightness`, `history`, `export [days|month]`, `help`, `quit`
65
+ - 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
66
  - Cat animation panel that reacts to robot state
66
67
  - Command history (↑/↓ arrows)
67
- - Auto-refreshes every 30 seconds
68
+ - Real-time updates via WebSocket; 5-minute poll fallback
68
69
 
69
70
  ## Install
70
71
 
@@ -124,8 +125,10 @@ LITTER_ROBOT_PASSWORD=yourpassword
124
125
  | `info` | Full dump of all robot properties (serial, firmware, all settings) |
125
126
  | `lock` / `unlock` | Toggle panel lockout |
126
127
  | `sleep` / `wake` | Toggle sleep mode |
127
- | `night-light on\|off` | Toggle night light |
128
+ | `night-light on\|off\|auto` | Set night light mode |
129
+ | `night-light-brightness <level>` | Set brightness (LR5: 0-100; LR4: 25/50/100) |
128
130
  | `history` | Show last 25 activity events |
131
+ | `export [days\|month]` | Export activity history to CSV in `~/Downloads` (default: 30 days) |
129
132
  | `clear` | Clear the log |
130
133
  | `help` | Show command list |
131
134
  | `quit` | Exit |
@@ -136,6 +139,14 @@ LITTER_ROBOT_PASSWORD=yourpassword
136
139
  |---|---|
137
140
  | `/login` | Sign in or switch accounts |
138
141
  | `/logout` | Sign out and clear saved credentials |
142
+ | `/robots` | List all robots on the account |
143
+ | `/robot <index\|name>` | Switch active robot (selection persists to keyring) |
144
+ | `/pets` | List all pets on the account |
145
+ | `/pet <index\|name>` | Switch which pet's name/weight shows in the status bar |
146
+ | `/cat on\|off` | Show or hide the cat animation panel |
147
+ | `/cat color <hex>` | Change the cat art colour (e.g. `/cat color #ff79c6`); `/cat reset` to revert |
148
+ | `/refresh [seconds\|off]` | Change the auto-poll interval or disable it (`/refresh 60`, `/refresh off`) |
149
+ | `/config` | Show current runtime settings (robot, refresh rate, cat panel, active pet) |
139
150
  | `/exit` | Exit Asher CLI |
140
151
 
141
152
  **Keyboard shortcuts:** `Ctrl+L` clears the log, `Ctrl+C` quits.
@@ -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.