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.
- 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.4/.env.example +3 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/.githooks/pre-push +0 -3
- {asher_cli-0.0.2 → asher_cli-0.0.4}/.github/workflows/ci.yml +5 -4
- asher_cli-0.0.4/.github/workflows/claude-code-review.yml +44 -0
- asher_cli-0.0.4/.github/workflows/claude.yml +50 -0
- asher_cli-0.0.4/.github/workflows/coverage.yml +24 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/.github/workflows/release.yml +7 -7
- asher_cli-0.0.4/CLAUDE.md +193 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/PKG-INFO +91 -23
- asher_cli-0.0.4/README.md +218 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/ROADMAP.md +335 -98
- {asher_cli-0.0.2 → asher_cli-0.0.4}/asher/app.py +26 -19
- asher_cli-0.0.4/asher/cats.py +93 -0
- asher_cli-0.0.4/asher/commands/__init__.py +947 -0
- asher_cli-0.0.4/asher/commands/base.py +84 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/asher/connection/__init__.py +74 -14
- asher_cli-0.0.4/asher/constants.py +19 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/asher/helpers.py +5 -8
- asher_cli-0.0.4/asher/login_flow.py +58 -0
- 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.4/asher/slash-commands/__init__.py +19 -0
- asher_cli-0.0.4/asher/ui/__init__.py +300 -0
- asher_cli-0.0.4/asher/ui/style.tcss +124 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/pyproject.toml +21 -4
- asher_cli-0.0.4/renovate.json +22 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/tests/conftest.py +2 -2
- asher_cli-0.0.4/tests/test_app_pilot.py +214 -0
- asher_cli-0.0.4/tests/test_auth.py +61 -0
- asher_cli-0.0.4/tests/test_auth_pilot.py +95 -0
- asher_cli-0.0.4/tests/test_cats.py +64 -0
- asher_cli-0.0.4/tests/test_commands_pilot.py +293 -0
- asher_cli-0.0.4/tests/test_connection.py +96 -0
- asher_cli-0.0.4/tests/test_connection_mixin.py +19 -0
- asher_cli-0.0.4/tests/test_monitoring.py +152 -0
- 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.4/tests/test_ui.py +146 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/tests/testhelpers.py +29 -1
- {asher_cli-0.0.2 → asher_cli-0.0.4}/uv.lock +502 -8
- asher_cli-0.0.4/watchrun.py +56 -0
- asher_cli-0.0.2/CLAUDE.md +0 -137
- asher_cli-0.0.2/README.md +0 -151
- asher_cli-0.0.2/asher/cats.py +0 -40
- asher_cli-0.0.2/asher/commands/__init__.py +0 -350
- asher_cli-0.0.2/asher/monitoring/__init__.py +0 -111
- asher_cli-0.0.2/asher/slash-commands/__init__.py +0 -26
- asher_cli-0.0.2/asher/ui/__init__.py +0 -279
- {asher_cli-0.0.2 → asher_cli-0.0.4}/.claude/skills/textual/SKILL.md +0 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/.gitignore +0 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/.vscode/launch.json +0 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/.vscode/settings.json +0 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/.vscode/tasks.json +0 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/LICENSE +0 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/app.py +0 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/asher/__init__.py +0 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/asher/__main__.py +0 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/asher/auth.py +0 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/requirements.txt +0 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/test.py +0 -0
- {asher_cli-0.0.2 → asher_cli-0.0.4}/tests/__init__.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,13 +12,14 @@ 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 .
|
|
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@
|
|
31
|
-
- uses: astral-sh/setup-uv@
|
|
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@
|
|
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/
|
|
@@ -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
|