asher-cli 1.2.0__tar.gz → 1.4.0__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 (117) hide show
  1. {asher_cli-1.2.0 → asher_cli-1.4.0}/.github/workflows/bandit.yml +2 -2
  2. {asher_cli-1.2.0 → asher_cli-1.4.0}/.github/workflows/ci.yml +2 -2
  3. {asher_cli-1.2.0 → asher_cli-1.4.0}/.github/workflows/claude-code-review.yml +1 -1
  4. {asher_cli-1.2.0 → asher_cli-1.4.0}/.github/workflows/claude.yml +1 -1
  5. {asher_cli-1.2.0 → asher_cli-1.4.0}/.github/workflows/coverage.yml +1 -1
  6. asher_cli-1.4.0/.github/workflows/dependency-automerge.yml +66 -0
  7. {asher_cli-1.2.0 → asher_cli-1.4.0}/.github/workflows/release.yml +1 -1
  8. {asher_cli-1.2.0 → asher_cli-1.4.0}/CHANGELOG.md +31 -0
  9. {asher_cli-1.2.0 → asher_cli-1.4.0}/CLAUDE.md +2 -2
  10. {asher_cli-1.2.0 → asher_cli-1.4.0}/PKG-INFO +15 -8
  11. {asher_cli-1.2.0 → asher_cli-1.4.0}/README.md +12 -5
  12. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/__main__.py +18 -1
  13. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/activity_labels.py +7 -0
  14. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/commands/__init__.py +105 -34
  15. asher_cli-1.4.0/asher/constants.py +61 -0
  16. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/headless.py +78 -24
  17. asher_cli-1.4.0/asher/helpers.py +182 -0
  18. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/monitoring/__init__.py +5 -4
  19. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/robot_adapters.py +149 -6
  20. {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/ROADMAP.md +51 -29
  21. {asher_cli-1.2.0 → asher_cli-1.4.0}/pyproject.toml +4 -4
  22. asher_cli-1.4.0/renovate.json +48 -0
  23. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_activity_labels.py +21 -0
  24. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_headless.py +169 -0
  25. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_lr5_commands.py +110 -0
  26. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_missing_robot_commands.py +139 -0
  27. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_monitoring.py +62 -0
  28. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_robot_adapters.py +192 -0
  29. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_version_command.py +12 -0
  30. asher_cli-1.4.0/tests/testhelpers.py +245 -0
  31. {asher_cli-1.2.0 → asher_cli-1.4.0}/uv.lock +275 -268
  32. asher_cli-1.2.0/asher/constants.py +0 -24
  33. asher_cli-1.2.0/asher/helpers.py +0 -71
  34. asher_cli-1.2.0/renovate.json +0 -22
  35. asher_cli-1.2.0/tests/testhelpers.py +0 -113
  36. {asher_cli-1.2.0 → asher_cli-1.4.0}/.claude/hooks/block-env.py +0 -0
  37. {asher_cli-1.2.0 → asher_cli-1.4.0}/.claude/settings.json +0 -0
  38. {asher_cli-1.2.0 → asher_cli-1.4.0}/.claude/skills/mcp-bridge/SKILL.md +0 -0
  39. {asher_cli-1.2.0 → asher_cli-1.4.0}/.claude/skills/pylitterbot-ref/SKILL.md +0 -0
  40. {asher_cli-1.2.0 → asher_cli-1.4.0}/.claude/skills/textual/SKILL.md +0 -0
  41. {asher_cli-1.2.0 → asher_cli-1.4.0}/.env.example +0 -0
  42. {asher_cli-1.2.0 → asher_cli-1.4.0}/.githooks/pre-push +0 -0
  43. {asher_cli-1.2.0 → asher_cli-1.4.0}/.github/FUNDING.yml +0 -0
  44. {asher_cli-1.2.0 → asher_cli-1.4.0}/.github/pull_request_template.md +0 -0
  45. {asher_cli-1.2.0 → asher_cli-1.4.0}/.gitignore +0 -0
  46. {asher_cli-1.2.0 → asher_cli-1.4.0}/.vscode/launch.json +0 -0
  47. {asher_cli-1.2.0 → asher_cli-1.4.0}/.vscode/settings.json +0 -0
  48. {asher_cli-1.2.0 → asher_cli-1.4.0}/.vscode/tasks.json +0 -0
  49. {asher_cli-1.2.0 → asher_cli-1.4.0}/CODEOWNERS +0 -0
  50. {asher_cli-1.2.0 → asher_cli-1.4.0}/LICENSE +0 -0
  51. {asher_cli-1.2.0 → asher_cli-1.4.0}/app.py +0 -0
  52. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/__init__.py +0 -0
  53. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/app.py +0 -0
  54. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/auth.py +0 -0
  55. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/autostart.py +0 -0
  56. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/cats.py +0 -0
  57. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/commands/base.py +0 -0
  58. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/completion.py +0 -0
  59. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/config.py +0 -0
  60. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/connection/__init__.py +0 -0
  61. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/daemon.py +0 -0
  62. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/desktoptheme.py +0 -0
  63. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/export.py +0 -0
  64. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/faults.py +0 -0
  65. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/history_view.py +0 -0
  66. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/launcher.py +0 -0
  67. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/login_flow.py +0 -0
  68. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/mcp_bridge.py +0 -0
  69. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/mcp_config.py +0 -0
  70. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/notifications.py +0 -0
  71. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/robot_protocol.py +0 -0
  72. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/slash-commands/__init__.py +0 -0
  73. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/theme.py +0 -0
  74. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/tray.py +0 -0
  75. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/ui/__init__.py +0 -0
  76. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/ui/style.tcss +0 -0
  77. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/updates.py +0 -0
  78. {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/watcher.py +0 -0
  79. {asher_cli-1.2.0 → asher_cli-1.4.0}/cliff.toml +0 -0
  80. {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/roadmap-archive/README.md +0 -0
  81. {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/roadmap-archive/cat-panel-badges.md +0 -0
  82. {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/roadmap-archive/config-persistence.md +0 -0
  83. {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/roadmap-archive/desktop-notifications.md +0 -0
  84. {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/roadmap-archive/fault-monitoring.md +0 -0
  85. {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/roadmap-archive/headless-export.md +0 -0
  86. {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/roadmap-archive/history-export.md +0 -0
  87. {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/roadmap-archive/tab-completion.md +0 -0
  88. {asher_cli-1.2.0 → asher_cli-1.4.0}/requirements.txt +0 -0
  89. {asher_cli-1.2.0 → asher_cli-1.4.0}/test.py +0 -0
  90. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/__init__.py +0 -0
  91. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/conftest.py +0 -0
  92. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_app_pilot.py +0 -0
  93. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_auth.py +0 -0
  94. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_auth_pilot.py +0 -0
  95. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_autostart.py +0 -0
  96. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_cats.py +0 -0
  97. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_commands_pilot.py +0 -0
  98. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_completion.py +0 -0
  99. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_config.py +0 -0
  100. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_connection.py +0 -0
  101. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_connection_mixin.py +0 -0
  102. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_daemon.py +0 -0
  103. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_desktoptheme.py +0 -0
  104. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_export.py +0 -0
  105. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_faults.py +0 -0
  106. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_history_view.py +0 -0
  107. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_launcher.py +0 -0
  108. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_mcp_bridge.py +0 -0
  109. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_mcp_command.py +0 -0
  110. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_mcp_config.py +0 -0
  111. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_new_commands_pilot.py +0 -0
  112. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_notifications.py +0 -0
  113. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_tray.py +0 -0
  114. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_ui.py +0 -0
  115. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_updates.py +0 -0
  116. {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_watcher.py +0 -0
  117. {asher_cli-1.2.0 → asher_cli-1.4.0}/watchrun.py +0 -0
@@ -31,7 +31,7 @@ jobs:
31
31
  runs-on: ubuntu-latest
32
32
  steps:
33
33
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
34
- - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
34
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
35
35
 
36
36
  - name: Bandit scan
37
37
  # --exit-zero: findings must not fail the job, or the upload step below
@@ -41,7 +41,7 @@ jobs:
41
41
  -c pyproject.toml -r . -f sarif -o bandit.sarif --exit-zero
42
42
 
43
43
  - name: Upload SARIF to code scanning
44
- uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
44
+ uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
45
45
  with:
46
46
  sarif_file: bandit.sarif
47
47
  category: bandit
@@ -19,7 +19,7 @@ jobs:
19
19
  runs-on: ubuntu-latest
20
20
  steps:
21
21
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
22
- - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
22
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
23
23
  - run: uv sync --no-default-groups --group dev
24
24
  - run: uv run ruff check .
25
25
  - run: uv run ruff format --check .
@@ -35,6 +35,6 @@ jobs:
35
35
  runs-on: ${{ matrix.os }}
36
36
  steps:
37
37
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
38
- - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
38
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
39
39
  - run: uv sync --no-default-groups --group dev
40
40
  - run: uv run pytest tests/ -v --tb=short
@@ -33,7 +33,7 @@ jobs:
33
33
 
34
34
  - name: Run Claude Code Review
35
35
  id: claude-review
36
- uses: anthropics/claude-code-action@6b082c41935b4c8a3b8b0ef85ba4ba4d9eeb8975 # v1
36
+ uses: anthropics/claude-code-action@9c5ddab2e6d17b83ea679153b31f1d5f023cf636 # 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'
@@ -32,7 +32,7 @@ jobs:
32
32
 
33
33
  - name: Run Claude Code
34
34
  id: claude
35
- uses: anthropics/claude-code-action@6b082c41935b4c8a3b8b0ef85ba4ba4d9eeb8975 # v1
35
+ uses: anthropics/claude-code-action@9c5ddab2e6d17b83ea679153b31f1d5f023cf636 # v1
36
36
  with:
37
37
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38
38
 
@@ -18,7 +18,7 @@ jobs:
18
18
  runs-on: ubuntu-latest
19
19
  steps:
20
20
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
21
- - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
21
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
22
22
  - run: uv sync --no-default-groups --group dev
23
23
  - name: Run tests with coverage
24
24
  run: uv run pytest tests/ --cov=asher --cov-report=lcov --cov-report=term-missing
@@ -0,0 +1,66 @@
1
+ name: Dependency auto-merge
2
+
3
+ on:
4
+ pull_request_target:
5
+ types: [opened, synchronize, reopened, edited, labeled]
6
+
7
+ permissions:
8
+ contents: write
9
+ pull-requests: write
10
+
11
+ jobs:
12
+ automerge:
13
+ runs-on: ubuntu-latest
14
+ if: github.event.pull_request.base.repo.full_name == github.repository
15
+ steps:
16
+ - name: Dependabot metadata
17
+ if: github.event.pull_request.user.login == 'dependabot[bot]'
18
+ id: dependabot-metadata
19
+ uses: dependabot/fetch-metadata@v2
20
+ with:
21
+ github-token: "${{ secrets.GITHUB_TOKEN }}"
22
+
23
+ - name: Decide eligibility
24
+ id: decide
25
+ env:
26
+ TITLE: ${{ github.event.pull_request.title }}
27
+ ACTOR: ${{ github.event.pull_request.user.login }}
28
+ LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
29
+ DEPENDABOT_UPDATE_TYPE: ${{ steps.dependabot-metadata.outputs.update-type }}
30
+ run: |
31
+ safe=false
32
+ if [ "$ACTOR" = "dependabot[bot]" ]; then
33
+ # Only semver patch/minor bumps auto-merge; majors always wait for review.
34
+ case "$DEPENDABOT_UPDATE_TYPE" in
35
+ version-update:semver-patch|version-update:semver-minor) safe=true ;;
36
+ esac
37
+ elif [ "$ACTOR" = "karanshukla" ]; then
38
+ # The dependency-update routine commits under this account rather than a
39
+ # bot identity. The `dependencies` label is the reliable signal; the title
40
+ # pattern is a fallback for when it is missing. Either way, a title
41
+ # mentioning "major"/"breaking" is left for manual merge. Only the
42
+ # title is scanned -- dependency-audit bodies routinely mention other
43
+ # packages' major versions, which used to veto safe patch-level PRs.
44
+ matched=false
45
+ case ",$LABELS," in
46
+ *,dependencies,*) matched=true ;;
47
+ esac
48
+ if [ "$matched" = false ]; then
49
+ if printf '%s' "$TITLE" | grep -Eiq '^(chore|fix)(\([a-z0-9/_-]+\))?:.*(dependenc|advisory|CVE|vulnerab|bump)'; then
50
+ matched=true
51
+ fi
52
+ fi
53
+ if [ "$matched" = true ]; then
54
+ if ! printf '%s' "$TITLE" | grep -Eiq '\b(major|breaking)\b'; then
55
+ safe=true
56
+ fi
57
+ fi
58
+ fi
59
+ echo "safe=$safe" >> "$GITHUB_OUTPUT"
60
+
61
+ - name: Enable auto-merge
62
+ if: steps.decide.outputs.safe == 'true'
63
+ env:
64
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
65
+ PR_URL: ${{ github.event.pull_request.html_url }}
66
+ run: gh pr merge --auto --squash "$PR_URL"
@@ -20,7 +20,7 @@ jobs:
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@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
23
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
24
24
  - run: uv build
25
25
  - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
26
26
  with:
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  Generated by [git-cliff](https://git-cliff.org) from conventional commits.
9
9
 
10
+ ## [1.4.0] - 2026-09-13
11
+
12
+ ### 🐛 Bug Fixes
13
+
14
+ - *(ci)* Only scan the PR title for major/breaking in auto-merge (#86)
15
+ - *(deps)* Update dependency pylitterbot to v2025.6.5 (#82)
16
+
17
+ ### 👷 CI/CD
18
+
19
+ - Auto-merge non-major dependency PRs once checks pass (#85)
20
+
21
+ ### 🔧 Miscellaneous
22
+
23
+ - *(renovate)* Squash automerged updates (#84)
24
+ - *(renovate)* Auto-merge non-major updates once checks pass (#83)
25
+
26
+ ## [1.3.0] - 2026-08-23
27
+
28
+ ### 🚀 Features
29
+
30
+ - *(commands)* Close the remaining robot-command gaps in the roadmap
31
+ - *(monitoring)* Shorten cat-panel status labels that overflow the panel width
32
+
33
+ ### 🐛 Bug Fixes
34
+
35
+ - *(monitoring)* Render readable status text in cat panel, not raw cloud code
36
+
37
+ ### 🧪 Tests
38
+
39
+ - *(version)* Keep the /version pilot tests off the network
40
+
10
41
  ## [1.2.0] - 2026-08-16
11
42
 
12
43
  ### 🚀 Features
@@ -23,8 +23,8 @@ asher/
23
23
  __init__.py
24
24
  app.py AsherApp class (thin orchestrator — composes mixins)
25
25
  auth.py LoginScreen modal (ModalScreen[tuple[str,str]]) — available, not primary flow
26
- helpers.py fmt_ago(), drawer_bar(), ts(), robot_model() (pure, testable)
27
- constants.py STATUS_COLORS, ROBOT_MODELS
26
+ helpers.py fmt_ago(), fmt_until(), drawer_bar(), ts(), robot_model(), status_text() + argument parsing shared by the TUI and headless surfaces: hex_colour(), parse_clock(), parse_day(), split_type_flag(), activity_type() (pure, testable)
27
+ constants.py STATUS_COLORS, CAT_PANEL_STATUS_LABELS, ACTIVITY_TYPES, ROBOT_MODELS
28
28
  theme.py Catppuccin Mocha palette + semantic roles (BACKGROUND, MUTED, DANGER, …) + CSS_VARIABLES/apply() — the only place a hex literal belongs
29
29
  config.py runtime settings persistence — load()/save()/update() over ~/.asher-cli/config.json; holds poll interval, cat-panel visibility/colour, active pet index, notification settings (non-secret UI prefs only; credentials stay in keyring)
30
30
  notifications.py desktop toast + audible alert façade — plyer first, then the platform's own tool (osascript/notify-send); fire/beep are always-safe no-ops on failure/headless
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: asher-cli
3
- Version: 1.2.0
3
+ Version: 1.4.0
4
4
  Summary: Terminal dashboard for Litter Robot (LR3/LR4/LR5) via the Whisker cloud API
5
5
  Project-URL: Homepage, https://github.com/karanshukla/asher-cli
6
6
  Project-URL: Repository, https://github.com/karanshukla/asher-cli
@@ -40,14 +40,14 @@ Classifier: Topic :: Home Automation
40
40
  Requires-Python: >=3.10
41
41
  Requires-Dist: keyring>=24.0.0
42
42
  Requires-Dist: plyer>=2.1.0
43
- Requires-Dist: pylitterbot==2025.6.4
43
+ Requires-Dist: pylitterbot==2025.6.5
44
44
  Requires-Dist: python-dotenv>=1.0.0
45
45
  Requires-Dist: rich>=13.0.0
46
46
  Requires-Dist: textual>=0.47.0
47
47
  Requires-Dist: tzlocal>=5.4
48
48
  Provides-Extra: mcp
49
49
  Requires-Dist: mcp<2; extra == 'mcp'
50
- Requires-Dist: pylitterbot[mcp]==2025.6.4; extra == 'mcp'
50
+ Requires-Dist: pylitterbot[mcp]==2025.6.5; extra == 'mcp'
51
51
  Provides-Extra: tray
52
52
  Requires-Dist: pillow>=10.0; extra == 'tray'
53
53
  Requires-Dist: pygobject>=3.42; (sys_platform == 'linux') and extra == 'tray'
@@ -66,7 +66,9 @@ Description-Content-Type: text/markdown
66
66
 
67
67
  A Claude Code-style terminal dashboard for monitoring and controlling Litter Robot via the Whisker cloud API.
68
68
 
69
- <img width="808" height="351" alt="image" src="https://github.com/user-attachments/assets/6599966f-837c-419c-8692-bfda3533e730" />
69
+ <img width="1165" height="885" alt="image" src="https://github.com/user-attachments/assets/03be7ed3-95f6-45cf-93db-cf5bf84d6499" />
70
+
71
+ <img width="1165" height="885" alt="image" src="https://github.com/user-attachments/assets/5aa60ff1-6e6b-4c71-8376-d96d52d96809" />
70
72
 
71
73
  ## Features
72
74
 
@@ -75,8 +77,8 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
75
77
  - Real-time cycling indicator with elapsed time (`⟳ Cycling M:SS`)
76
78
  - Fault & safety monitoring — model-scoped in-panel alerts for cat detected, pinch, motor/position/gas faults (LR5: bonnet/laser/drawer); press `d` to dismiss
77
79
  - Cat panel with mode label + status badges (status chip, lock, night light, sleep, wait time) under the art
78
- - Scrollable activity-history pager — `history [count|all]` opens a full-screen, paginated view (arrow keys, `Page Up`/`Page Down`, `Home`/`End`); `c` copies the whole history to the clipboard; `q`/`Esc`/`Enter` to close
79
- - Commands: `clean`, `status`, `info`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto`, `night-light-brightness`, `panel-brightness <low|medium|high>` (LR4/LR5), `wait-time`, `power on|off`, `rename`, `insight`, `sleep-schedule`, plus LR5 extras (`privacy`, `volume`, `camera-audio`, `drawer-reset`), `history [count|all]`, `export [days|month]`, `help`, `quit`
80
+ - Scrollable activity-history pager — `history [count|all] [--type <kind>]` opens a full-screen, paginated view (arrow keys, `Page Up`/`Page Down`, `Home`/`End`); `c` copies the whole history to the clipboard; `q`/`Esc`/`Enter` to close
81
+ - Commands: `clean`, `status`, `info`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto|color <hex>`, `night-light-brightness`, `panel-brightness <low|medium|high>` (LR4/LR5), `wait-time`, `power on|off`, `rename`, `insight`, `sleep-schedule [set|disable]`, plus LR5 extras (`privacy`, `volume`, `camera-audio`, `drawer-reset`), `history [count|all] [--type <kind>]`, `export [days|month]`, `help`, `quit`
80
82
  - Slash commands for app management: `/login`, `/logout`, `/robots`, `/robot <index|name>`, `/pets`, `/pet <index|name>`, `/cat on|off|colour <hex>`, `/refresh [seconds|off]`, `/config`, `/notify on|off|sound on|off|test`, `/watch start|stop|status|enable|disable`, `/version`, `/mcp on|off|status`, `/exit`
81
83
  - Slash-command tab completion — type `/` and a Claude Code-style overlay lists matching commands; `↑`/`↓` to move, `Tab` or `Enter` to accept, `Esc` to dismiss
82
84
  - Inline ghost-text completion for bare commands — type a prefix (`cle`) and the rest (`an`) appears greyed; `Tab` or `→` to accept → `clean`
@@ -142,18 +144,21 @@ The keyring is the only place an installed copy reads credentials from. Working
142
144
  | `lock` / `unlock` | Toggle panel lockout |
143
145
  | `sleep` / `wake` | Toggle sleep mode |
144
146
  | `night-light on\|off\|auto` | Set night light mode |
147
+ | `night-light color <hex>` | Set the LR5 night light colour (`#RRGGBB`, or the 3-digit shorthand) |
145
148
  | `night-light-brightness <level>` | Set brightness (LR5: 0-100; LR4: 25/50/100) |
146
149
  | `panel-brightness <low\|medium\|high>` | Set control-panel brightness (LR4/LR5 only; shows current if omitted) |
147
150
  | `wait-time <minutes>` | Set clean-cycle wait time (shows valid values / current if omitted) |
148
151
  | `power on\|off` | Hard-power the unit on or off |
149
152
  | `rename <new name>` | Rename the unit in the Whisker cloud |
150
153
  | `insight [days\|month]` | Show cycle-usage statistics (default: 30 days) |
151
- | `sleep-schedule` | Show the per-day sleep schedule (read-only) |
154
+ | `sleep-schedule` | Show the per-day sleep schedule |
155
+ | `sleep-schedule set <day\|all> <HH:MM> <HH:MM>` | Set the sleep → wake window (LR3: one window for every day, wake time fixed; LR4: read-only, use the Whisker app) |
156
+ | `sleep-schedule disable` | Turn every scheduled sleep window off |
152
157
  | `privacy on\|off` | Toggle LR5 privacy mode |
153
158
  | `volume <0-100>` | Set LR5 sound volume |
154
159
  | `camera-audio on\|off` | Toggle LR5 camera audio |
155
160
  | `drawer-reset` | Reset the LR5 waste drawer level indicator |
156
- | `history [count\|all]` | Show recent activity in a scrollable pager (default: 50 events) |
161
+ | `history [count\|all] [--type <kind>]` | Show recent activity in a scrollable pager (default: 50 events); `--type cat\|clean\|litter\|offline\|…` filters on the LR5 |
157
162
  | `export [days\|month]` | Export activity history to CSV in `~/Downloads` (default: 30 days) |
158
163
  | `clear` | Clear the log |
159
164
  | `help` | Show command list |
@@ -193,6 +198,8 @@ asher clean start a clean cycle
193
198
  asher night-light auto set night light mode
194
199
  asher wait-time 7 set the clean-cycle wait time
195
200
  asher history 20 recent activity, newest first
201
+ asher history 20 --type cat only cat visits (LR5)
202
+ asher sleep-schedule set all 22:00 07:00 sleep 22:00 → 07:00 every day
196
203
  asher insight 7 cycle-usage statistics
197
204
  asher export 7 --output ~/hist.csv activity history to CSV
198
205
  ```
@@ -10,7 +10,9 @@
10
10
 
11
11
  A Claude Code-style terminal dashboard for monitoring and controlling Litter Robot via the Whisker cloud API.
12
12
 
13
- <img width="808" height="351" alt="image" src="https://github.com/user-attachments/assets/6599966f-837c-419c-8692-bfda3533e730" />
13
+ <img width="1165" height="885" alt="image" src="https://github.com/user-attachments/assets/03be7ed3-95f6-45cf-93db-cf5bf84d6499" />
14
+
15
+ <img width="1165" height="885" alt="image" src="https://github.com/user-attachments/assets/5aa60ff1-6e6b-4c71-8376-d96d52d96809" />
14
16
 
15
17
  ## Features
16
18
 
@@ -19,8 +21,8 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
19
21
  - Real-time cycling indicator with elapsed time (`⟳ Cycling M:SS`)
20
22
  - Fault & safety monitoring — model-scoped in-panel alerts for cat detected, pinch, motor/position/gas faults (LR5: bonnet/laser/drawer); press `d` to dismiss
21
23
  - Cat panel with mode label + status badges (status chip, lock, night light, sleep, wait time) under the art
22
- - Scrollable activity-history pager — `history [count|all]` opens a full-screen, paginated view (arrow keys, `Page Up`/`Page Down`, `Home`/`End`); `c` copies the whole history to the clipboard; `q`/`Esc`/`Enter` to close
23
- - Commands: `clean`, `status`, `info`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto`, `night-light-brightness`, `panel-brightness <low|medium|high>` (LR4/LR5), `wait-time`, `power on|off`, `rename`, `insight`, `sleep-schedule`, plus LR5 extras (`privacy`, `volume`, `camera-audio`, `drawer-reset`), `history [count|all]`, `export [days|month]`, `help`, `quit`
24
+ - Scrollable activity-history pager — `history [count|all] [--type <kind>]` opens a full-screen, paginated view (arrow keys, `Page Up`/`Page Down`, `Home`/`End`); `c` copies the whole history to the clipboard; `q`/`Esc`/`Enter` to close
25
+ - Commands: `clean`, `status`, `info`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto|color <hex>`, `night-light-brightness`, `panel-brightness <low|medium|high>` (LR4/LR5), `wait-time`, `power on|off`, `rename`, `insight`, `sleep-schedule [set|disable]`, plus LR5 extras (`privacy`, `volume`, `camera-audio`, `drawer-reset`), `history [count|all] [--type <kind>]`, `export [days|month]`, `help`, `quit`
24
26
  - Slash commands for app management: `/login`, `/logout`, `/robots`, `/robot <index|name>`, `/pets`, `/pet <index|name>`, `/cat on|off|colour <hex>`, `/refresh [seconds|off]`, `/config`, `/notify on|off|sound on|off|test`, `/watch start|stop|status|enable|disable`, `/version`, `/mcp on|off|status`, `/exit`
25
27
  - Slash-command tab completion — type `/` and a Claude Code-style overlay lists matching commands; `↑`/`↓` to move, `Tab` or `Enter` to accept, `Esc` to dismiss
26
28
  - Inline ghost-text completion for bare commands — type a prefix (`cle`) and the rest (`an`) appears greyed; `Tab` or `→` to accept → `clean`
@@ -86,18 +88,21 @@ The keyring is the only place an installed copy reads credentials from. Working
86
88
  | `lock` / `unlock` | Toggle panel lockout |
87
89
  | `sleep` / `wake` | Toggle sleep mode |
88
90
  | `night-light on\|off\|auto` | Set night light mode |
91
+ | `night-light color <hex>` | Set the LR5 night light colour (`#RRGGBB`, or the 3-digit shorthand) |
89
92
  | `night-light-brightness <level>` | Set brightness (LR5: 0-100; LR4: 25/50/100) |
90
93
  | `panel-brightness <low\|medium\|high>` | Set control-panel brightness (LR4/LR5 only; shows current if omitted) |
91
94
  | `wait-time <minutes>` | Set clean-cycle wait time (shows valid values / current if omitted) |
92
95
  | `power on\|off` | Hard-power the unit on or off |
93
96
  | `rename <new name>` | Rename the unit in the Whisker cloud |
94
97
  | `insight [days\|month]` | Show cycle-usage statistics (default: 30 days) |
95
- | `sleep-schedule` | Show the per-day sleep schedule (read-only) |
98
+ | `sleep-schedule` | Show the per-day sleep schedule |
99
+ | `sleep-schedule set <day\|all> <HH:MM> <HH:MM>` | Set the sleep → wake window (LR3: one window for every day, wake time fixed; LR4: read-only, use the Whisker app) |
100
+ | `sleep-schedule disable` | Turn every scheduled sleep window off |
96
101
  | `privacy on\|off` | Toggle LR5 privacy mode |
97
102
  | `volume <0-100>` | Set LR5 sound volume |
98
103
  | `camera-audio on\|off` | Toggle LR5 camera audio |
99
104
  | `drawer-reset` | Reset the LR5 waste drawer level indicator |
100
- | `history [count\|all]` | Show recent activity in a scrollable pager (default: 50 events) |
105
+ | `history [count\|all] [--type <kind>]` | Show recent activity in a scrollable pager (default: 50 events); `--type cat\|clean\|litter\|offline\|…` filters on the LR5 |
101
106
  | `export [days\|month]` | Export activity history to CSV in `~/Downloads` (default: 30 days) |
102
107
  | `clear` | Clear the log |
103
108
  | `help` | Show command list |
@@ -137,6 +142,8 @@ asher clean start a clean cycle
137
142
  asher night-light auto set night light mode
138
143
  asher wait-time 7 set the clean-cycle wait time
139
144
  asher history 20 recent activity, newest first
145
+ asher history 20 --type cat only cat visits (LR5)
146
+ asher sleep-schedule set all 22:00 07:00 sleep 22:00 → 07:00 every day
140
147
  asher insight 7 cycle-usage statistics
141
148
  asher export 7 --output ~/hist.csv activity history to CSV
142
149
  ```
@@ -132,9 +132,26 @@ def _build_parser() -> argparse.ArgumentParser:
132
132
  metavar="PATH",
133
133
  help="write to this path instead of ~/Downloads.",
134
134
  )
135
+ if command.name == "history":
136
+ sub.add_argument(
137
+ "--type",
138
+ metavar="KIND",
139
+ help="only events of this kind (LR5 only), e.g. cat, clean, litter.",
140
+ )
135
141
  return parser
136
142
 
137
143
 
144
+ def _handler_args(parsed: argparse.Namespace) -> list[str]:
145
+ """Rebuild the word list a headless handler parses.
146
+
147
+ The handlers take raw words so the TUI and the CLI read the same grammar;
148
+ argparse only declares the flags it needs in order to document them in
149
+ ``--help``, and they are folded back in here.
150
+ """
151
+ kind = getattr(parsed, "type", None)
152
+ return [*parsed.args, "--type", kind] if kind else list(parsed.args)
153
+
154
+
138
155
  def main() -> None:
139
156
  args = _build_parser().parse_args()
140
157
 
@@ -157,7 +174,7 @@ def main() -> None:
157
174
  asyncio.run(
158
175
  run(
159
176
  args.command,
160
- args.args,
177
+ _handler_args(args),
161
178
  robot=args.robot,
162
179
  output=args.output,
163
180
  as_json=getattr(args, "json", False),
@@ -35,6 +35,13 @@ ACTION_LABELS: dict[str, tuple[str, str]] = {
35
35
  "motor fault": ("Motor fault", theme.DANGER),
36
36
  "pinch detect": ("Pinch detected", theme.DANGER),
37
37
  "timing fault": ("Timing fault", theme.WARN),
38
+ # The LR5 activity endpoint reports SCREAMING_SNAKE type codes rather than
39
+ # the sentence-style text the LR3/LR4 history returns.
40
+ "pet_visit": ("Cat visit", theme.WARN),
41
+ "cat_detect": ("Cat detected", theme.WARN),
42
+ "cycle_completed": ("Clean cycle complete", theme.OK),
43
+ "cycle_interrupted": ("Cycle interrupted", theme.WARN),
44
+ "litter_low": ("Litter low", theme.WARN),
38
45
  }
39
46
 
40
47
  # Fallback colour for unknown event types — muted grey rather than crashing.
@@ -7,7 +7,7 @@ import contextlib
7
7
  import shutil
8
8
  import subprocess
9
9
  import sys
10
- from datetime import time
10
+ from datetime import datetime, time
11
11
  from importlib.metadata import PackageNotFoundError
12
12
  from importlib.metadata import version as pkg_version
13
13
  from pathlib import Path
@@ -26,8 +26,21 @@ from textual.widgets import Input, RichLog, Static
26
26
 
27
27
  from .. import theme
28
28
  from ..completion import enter_completes, render_completion, slash_matches
29
+ from ..constants import ACTIVITY_TYPES
29
30
  from ..export import ExportError, build_history_csv, resolve_dest
30
- from ..helpers import fmt_ago, robot_model, ts
31
+ from ..helpers import (
32
+ DAY_NAMES,
33
+ activity_type,
34
+ fmt_ago,
35
+ fmt_until,
36
+ hex_colour,
37
+ parse_clock,
38
+ parse_day,
39
+ robot_model,
40
+ split_type_flag,
41
+ status_text,
42
+ ts,
43
+ )
31
44
  from ..history_view import HistoryScreen
32
45
  from ..login_flow import LoginFlow, LoginState
33
46
  from .base import Command, CommandRegistry, SlashCommand
@@ -43,22 +56,6 @@ def _fmt_wait_time(minutes: object) -> str:
43
56
  return "—"
44
57
 
45
58
 
46
- def _status_text(status: object) -> str:
47
- """Render a robot status as human-readable text.
48
-
49
- ``LitterBoxStatus`` is a plain ``Enum`` (not ``StrEnum``), so ``str()`` yields
50
- the verbose ``"LitterBoxStatus.READY"``; the readable form ("Ready") lives in
51
- its ``text`` property. Falls back to ``str()`` for plain strings, and to an
52
- em dash when status is missing.
53
- """
54
- if status is None:
55
- return "—"
56
- text = getattr(status, "text", None)
57
- if isinstance(text, str):
58
- return text
59
- return str(status)
60
-
61
-
62
59
  _POWER_LABELS = {"AC": "AC (mains)", "DC": "Battery", "NC": "Off/unknown"}
63
60
 
64
61
 
@@ -188,7 +185,7 @@ class StatusCommand(Command):
188
185
  last_seen = getattr(app, "_last_cat_seen", None) or getattr(r, "last_seen", None)
189
186
  rows = [
190
187
  ("Online", "yes" if getattr(r, "is_online", False) else "no"),
191
- ("Status", _status_text(getattr(r, "status", None))),
188
+ ("Status", status_text(getattr(r, "status", None))),
192
189
  ("Drawer", f"{float(getattr(r, 'waste_drawer_level', 0) or 0):.0f}%"),
193
190
  ("Last seen", fmt_ago(last_seen)),
194
191
  ("Cat weight", weight),
@@ -251,6 +248,9 @@ class InfoCommand(Command):
251
248
  ("Online", _yn(getattr(r, "is_online", False))),
252
249
  ("Last seen", fmt_ago(last_seen)),
253
250
  ]
251
+ filter_due = getattr(r, "next_filter_replacement_date", None)
252
+ if isinstance(filter_due, datetime):
253
+ rows.insert(-2, ("Filter due", fmt_until(filter_due)))
254
254
  log = app.query_one("#log", RichLog)
255
255
  for k, v in rows:
256
256
  t = Text()
@@ -348,19 +348,22 @@ class WakeCommand(Command):
348
348
  class NightLightCommand(Command):
349
349
  name = "night-light"
350
350
  aliases = ("nightlight", "nl")
351
- description = "set night light mode"
351
+ description = "on|off|auto|color <hex> set night light mode"
352
352
  requires_robot = True
353
353
 
354
354
  @property
355
355
  def display_name(self) -> str:
356
- return "night-light on|off|auto"
356
+ return "night-light"
357
357
 
358
358
  async def run(self, app: AsherApp, args: list[str]) -> None:
359
359
  if app._adapter is None:
360
360
  return
361
361
  arg = args[0].lower() if args else ""
362
+ if arg in ("color", "colour"):
363
+ await self._set_colour(app, args[1:])
364
+ return
362
365
  if arg not in ("on", "off", "auto"):
363
- app._log_warn("Usage: night-light on|off|auto")
366
+ app._log_warn("Usage: night-light on|off|auto|color <hex>")
364
367
  return
365
368
  ok, msg = await app._adapter.set_night_light(arg)
366
369
  if ok:
@@ -375,6 +378,19 @@ class NightLightCommand(Command):
375
378
  else:
376
379
  app._log_warn(msg)
377
380
 
381
+ async def _set_colour(self, app: AsherApp, args: list[str]) -> None:
382
+ if app._adapter is None:
383
+ return
384
+ colour = hex_colour(args[0]) if args else None
385
+ if colour is None:
386
+ app._log_warn("Usage: night-light color <hex> e.g. night-light color #FF9E64")
387
+ return
388
+ ok, msg = await app._adapter.set_night_light_color(colour)
389
+ if ok:
390
+ app._log_ok(msg)
391
+ else:
392
+ app._log_warn(msg)
393
+
378
394
 
379
395
  class NightLightBrightnessCommand(Command):
380
396
  name = "night-light-brightness"
@@ -440,13 +456,14 @@ class PanelBrightnessCommand(Command):
440
456
  class HistoryCommand(Command):
441
457
  name = "history"
442
458
  aliases = ("hist",)
443
- description = "[count|all] show recent activity in a scrollable pager"
459
+ description = "[count|all] [--type <kind>] recent activity in a scrollable pager"
444
460
  requires_robot = True
445
461
 
446
462
  async def run(self, app: AsherApp, args: list[str]) -> None:
447
- if app._robot is None:
463
+ if app._robot is None or app._adapter is None:
448
464
  return
449
- raw = args[0].lower() if args else ""
465
+ counts, wanted_type = split_type_flag(args)
466
+ raw = counts[0].lower() if counts else ""
450
467
  if raw in ("all", "max"):
451
468
  limit = 500
452
469
  elif raw:
@@ -457,10 +474,15 @@ class HistoryCommand(Command):
457
474
  return
458
475
  else:
459
476
  limit = 50
460
- try:
461
- acts = await app._robot.get_activity_history(limit=limit)
462
- except Exception as exc:
463
- app._log_err(f"Failed to get history: {exc}")
477
+
478
+ wanted = activity_type(wanted_type)
479
+ if wanted_type is not None and wanted is None:
480
+ app._log_warn(f"Usage: history [count|all] --type <{'|'.join(ACTIVITY_TYPES)}>")
481
+ return
482
+
483
+ acts, problem = await app._adapter.get_history(limit, wanted)
484
+ if acts is None:
485
+ app._log_err(problem)
464
486
  return
465
487
  robot_name = getattr(app._robot, "name", "robot")
466
488
  app.push_screen(HistoryScreen(acts, app._pets, robot_name))
@@ -616,9 +638,6 @@ class InsightCommand(Command):
616
638
  log.write(t)
617
639
 
618
640
 
619
- _DAY_NAMES = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
620
-
621
-
622
641
  def _fmt_sleep_time(t: object) -> str:
623
642
  if t is None:
624
643
  return "—"
@@ -645,10 +664,16 @@ def _parse_sleep_day(day: Any) -> tuple[int, bool, object, object] | None:
645
664
  return None
646
665
 
647
666
 
667
+ _EVERY_DAY = ("all", "every", "daily", "everyday")
668
+ _SCHEDULE_USAGE = (
669
+ "Usage: sleep-schedule · sleep-schedule set <day|all> <HH:MM> <HH:MM> · sleep-schedule disable"
670
+ )
671
+
672
+
648
673
  class SleepScheduleCommand(Command):
649
674
  name = "sleep-schedule"
650
675
  aliases = ("sleepschedule",)
651
- description = "show the per-day sleep schedule (read-only)"
676
+ description = "[set <day|all> <HH:MM> <HH:MM>|disable] per-day sleep windows"
652
677
  requires_robot = True
653
678
 
654
679
  @property
@@ -656,6 +681,52 @@ class SleepScheduleCommand(Command):
656
681
  return "sleep-schedule"
657
682
 
658
683
  async def run(self, app: AsherApp, args: list[str]) -> None:
684
+ action = args[0].lower() if args else ""
685
+ if action == "set":
686
+ await self._set_window(app, args[1:])
687
+ return
688
+ if action in ("disable", "off"):
689
+ await self._disable(app)
690
+ return
691
+ if action:
692
+ app._log_warn(_SCHEDULE_USAGE)
693
+ return
694
+ await self._show(app)
695
+
696
+ async def _set_window(self, app: AsherApp, args: list[str]) -> None:
697
+ if app._adapter is None:
698
+ return
699
+ if len(args) < 3:
700
+ app._log_warn("Usage: sleep-schedule set <day|all> <HH:MM> <HH:MM>")
701
+ return
702
+ day_arg = args[0].lower()
703
+ weekday = None if day_arg in _EVERY_DAY else parse_day(day_arg)
704
+ if weekday is None and day_arg not in _EVERY_DAY:
705
+ app._log_warn(f"Unknown day '{args[0]}' — use {'/'.join(DAY_NAMES)} or 'all'")
706
+ return
707
+ sleep, wake = parse_clock(args[1]), parse_clock(args[2])
708
+ if sleep is None or wake is None:
709
+ app._log_warn("Times must be 24-hour HH:MM — e.g. sleep-schedule set all 22:00 07:00")
710
+ return
711
+
712
+ ok, msg = await app._adapter.set_sleep_window(weekday, sleep, wake)
713
+ if not ok:
714
+ app._log_warn(msg)
715
+ return
716
+ app._log_ok(msg)
717
+ await app._refresh_status()
718
+
719
+ async def _disable(self, app: AsherApp) -> None:
720
+ if app._adapter is None:
721
+ return
722
+ ok, msg = await app._adapter.disable_sleep_schedule()
723
+ if not ok:
724
+ app._log_warn(msg)
725
+ return
726
+ app._log_ok(msg)
727
+ await app._refresh_status()
728
+
729
+ async def _show(self, app: AsherApp) -> None:
659
730
  if app._robot is None:
660
731
  return
661
732
  try:
@@ -704,7 +775,7 @@ class SleepScheduleCommand(Command):
704
775
  continue
705
776
  idx, day_enabled, sleep_t, wake_t = parsed
706
777
 
707
- name = _DAY_NAMES[idx]
778
+ name = DAY_NAMES[idx]
708
779
  t = Text()
709
780
  t.append(f" {name} ", style=theme.MUTED)
710
781
  if day_enabled: