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.
- asher_cli-0.0.4/.claude/hooks/block-env.ps1 +9 -0
- asher_cli-0.0.4/.claude/settings.json +40 -0
- asher_cli-0.0.4/.claude/skills/pylitterbot-ref/SKILL.md +73 -0
- asher_cli-0.0.4/.claude/skills/release/SKILL.md +43 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/.github/workflows/ci.yml +4 -4
- {asher_cli-0.0.3 → asher_cli-0.0.4}/.github/workflows/claude-code-review.yml +2 -2
- {asher_cli-0.0.3 → asher_cli-0.0.4}/.github/workflows/claude.yml +2 -2
- {asher_cli-0.0.3 → asher_cli-0.0.4}/.github/workflows/coverage.yml +3 -3
- {asher_cli-0.0.3 → asher_cli-0.0.4}/.github/workflows/release.yml +7 -7
- {asher_cli-0.0.3 → asher_cli-0.0.4}/CLAUDE.md +44 -25
- {asher_cli-0.0.3 → asher_cli-0.0.4}/PKG-INFO +18 -7
- {asher_cli-0.0.3 → asher_cli-0.0.4}/README.md +15 -5
- {asher_cli-0.0.3 → asher_cli-0.0.4}/ROADMAP.md +335 -100
- {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/app.py +19 -15
- asher_cli-0.0.4/asher/commands/__init__.py +947 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/commands/base.py +5 -2
- {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/connection/__init__.py +35 -9
- asher_cli-0.0.4/asher/constants.py +19 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/helpers.py +5 -8
- asher_cli-0.0.4/asher/monitoring/__init__.py +185 -0
- asher_cli-0.0.4/asher/robot_adapters.py +152 -0
- asher_cli-0.0.4/asher/robot_protocol.py +58 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/ui/__init__.py +65 -18
- {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/ui/style.tcss +0 -10
- {asher_cli-0.0.3 → asher_cli-0.0.4}/pyproject.toml +6 -4
- asher_cli-0.0.4/renovate.json +22 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/conftest.py +2 -2
- {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_app_pilot.py +6 -3
- {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_commands_pilot.py +6 -3
- {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_connection.py +3 -2
- {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_monitoring.py +39 -12
- asher_cli-0.0.4/tests/test_new_commands_pilot.py +392 -0
- asher_cli-0.0.4/tests/test_robot_adapters.py +393 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/uv.lock +100 -77
- asher_cli-0.0.4/watchrun.py +56 -0
- asher_cli-0.0.3/.github/workflows/dependency-audit.yml +0 -37
- asher_cli-0.0.3/asher/commands/__init__.py +0 -442
- asher_cli-0.0.3/asher/monitoring/__init__.py +0 -111
- {asher_cli-0.0.3 → asher_cli-0.0.4}/.claude/skills/textual/SKILL.md +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/.env.example +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/.githooks/pre-push +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/.gitignore +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/.vscode/launch.json +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/.vscode/settings.json +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/.vscode/tasks.json +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/LICENSE +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/app.py +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/__init__.py +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/__main__.py +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/auth.py +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/cats.py +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/login_flow.py +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/asher/slash-commands/__init__.py +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/requirements.txt +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/test.py +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/__init__.py +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_auth.py +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_auth_pilot.py +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_cats.py +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_connection_mixin.py +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/test_ui.py +0 -0
- {asher_cli-0.0.3 → asher_cli-0.0.4}/tests/testhelpers.py +0 -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@
|
|
16
|
-
- uses: astral-sh/setup-uv@
|
|
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@
|
|
32
|
-
- uses: astral-sh/setup-uv@
|
|
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@
|
|
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@
|
|
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@
|
|
16
|
-
- uses: astral-sh/setup-uv@
|
|
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@
|
|
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@
|
|
23
|
+
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
24
24
|
- run: uv build
|
|
25
|
-
- uses: actions/upload-artifact@
|
|
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@
|
|
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@
|
|
48
|
-
- uses: actions/download-artifact@
|
|
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(),
|
|
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/
|
|
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
|
|
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. **
|
|
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 —
|
|
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
|
-
└── #
|
|
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) —
|
|
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 →
|
|
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` —
|
|
113
|
-
| `_tick_cat()` | advances multi-frame cat animation every 0.
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `_cmd_logout()` | delete creds from keyring,
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
-
|
|
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:**
|
|
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:**
|
|
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(
|
|
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
|
+
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
|
|
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
|
[](https://pypi.org/project/asher-cli/)
|
|
47
|
-

|
|
48
49
|
[](https://pypi.org/project/asher-cli/)
|
|
49
50
|
[](LICENSE)
|
|
50
51
|
[](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
|
-
-
|
|
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` |
|
|
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
|
[](https://pypi.org/project/asher-cli/)
|
|
4
|
-

|
|
5
5
|
[](https://pypi.org/project/asher-cli/)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](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
|
-
-
|
|
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` |
|
|
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.
|