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.
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.github/workflows/bandit.yml +2 -2
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.github/workflows/ci.yml +2 -2
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.github/workflows/claude-code-review.yml +1 -1
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.github/workflows/claude.yml +1 -1
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.github/workflows/coverage.yml +1 -1
- asher_cli-1.4.0/.github/workflows/dependency-automerge.yml +66 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.github/workflows/release.yml +1 -1
- {asher_cli-1.2.0 → asher_cli-1.4.0}/CHANGELOG.md +31 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/CLAUDE.md +2 -2
- {asher_cli-1.2.0 → asher_cli-1.4.0}/PKG-INFO +15 -8
- {asher_cli-1.2.0 → asher_cli-1.4.0}/README.md +12 -5
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/__main__.py +18 -1
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/activity_labels.py +7 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/commands/__init__.py +105 -34
- asher_cli-1.4.0/asher/constants.py +61 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/headless.py +78 -24
- asher_cli-1.4.0/asher/helpers.py +182 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/monitoring/__init__.py +5 -4
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/robot_adapters.py +149 -6
- {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/ROADMAP.md +51 -29
- {asher_cli-1.2.0 → asher_cli-1.4.0}/pyproject.toml +4 -4
- asher_cli-1.4.0/renovate.json +48 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_activity_labels.py +21 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_headless.py +169 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_lr5_commands.py +110 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_missing_robot_commands.py +139 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_monitoring.py +62 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_robot_adapters.py +192 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_version_command.py +12 -0
- asher_cli-1.4.0/tests/testhelpers.py +245 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/uv.lock +275 -268
- asher_cli-1.2.0/asher/constants.py +0 -24
- asher_cli-1.2.0/asher/helpers.py +0 -71
- asher_cli-1.2.0/renovate.json +0 -22
- asher_cli-1.2.0/tests/testhelpers.py +0 -113
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.claude/hooks/block-env.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.claude/settings.json +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.claude/skills/mcp-bridge/SKILL.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.claude/skills/pylitterbot-ref/SKILL.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.claude/skills/textual/SKILL.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.env.example +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.githooks/pre-push +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.github/FUNDING.yml +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.github/pull_request_template.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.gitignore +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.vscode/launch.json +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.vscode/settings.json +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/.vscode/tasks.json +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/CODEOWNERS +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/LICENSE +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/app.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/__init__.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/app.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/auth.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/autostart.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/cats.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/commands/base.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/completion.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/config.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/connection/__init__.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/daemon.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/desktoptheme.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/export.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/faults.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/history_view.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/launcher.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/login_flow.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/mcp_bridge.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/mcp_config.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/notifications.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/robot_protocol.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/slash-commands/__init__.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/theme.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/tray.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/ui/__init__.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/ui/style.tcss +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/updates.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/asher/watcher.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/cliff.toml +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/roadmap-archive/README.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/roadmap-archive/cat-panel-badges.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/roadmap-archive/config-persistence.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/roadmap-archive/desktop-notifications.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/roadmap-archive/fault-monitoring.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/roadmap-archive/headless-export.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/roadmap-archive/history-export.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/docs/roadmap-archive/tab-completion.md +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/requirements.txt +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/test.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/__init__.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/conftest.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_app_pilot.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_auth.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_auth_pilot.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_autostart.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_cats.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_commands_pilot.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_completion.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_config.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_connection.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_connection_mixin.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_daemon.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_desktoptheme.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_export.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_faults.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_history_view.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_launcher.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_mcp_bridge.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_mcp_command.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_mcp_config.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_new_commands_pilot.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_notifications.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_tray.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_ui.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_updates.py +0 -0
- {asher_cli-1.2.0 → asher_cli-1.4.0}/tests/test_watcher.py +0 -0
- {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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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.
|
|
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.
|
|
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.
|
|
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="
|
|
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
|
|
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
|
|
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="
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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",
|
|
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
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
app.
|
|
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 = "
|
|
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 =
|
|
778
|
+
name = DAY_NAMES[idx]
|
|
708
779
|
t = Text()
|
|
709
780
|
t.append(f" {name} ", style=theme.MUTED)
|
|
710
781
|
if day_enabled:
|