asher-cli 1.0.1__tar.gz → 1.1.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.1.0/.claude/hooks/block-env.py +14 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/.claude/settings.json +1 -1
- asher_cli-1.1.0/.claude/skills/mcp-bridge/SKILL.md +15 -0
- asher_cli-1.1.0/.github/FUNDING.yml +15 -0
- asher_cli-1.1.0/.github/workflows/bandit.yml +47 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/.github/workflows/release.yml +15 -12
- {asher_cli-1.0.1 → asher_cli-1.1.0}/CHANGELOG.md +29 -2
- {asher_cli-1.0.1 → asher_cli-1.1.0}/CLAUDE.md +34 -52
- {asher_cli-1.0.1 → asher_cli-1.1.0}/PKG-INFO +61 -15
- {asher_cli-1.0.1 → asher_cli-1.1.0}/README.md +60 -14
- asher_cli-1.1.0/asher/__main__.py +116 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/activity_labels.py +22 -20
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/app.py +9 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/auth.py +13 -11
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/commands/__init__.py +116 -76
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/completion.py +6 -4
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/connection/__init__.py +4 -3
- asher_cli-1.1.0/asher/constants.py +24 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/export.py +17 -42
- asher_cli-1.1.0/asher/headless.py +626 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/helpers.py +5 -4
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/history_view.py +19 -18
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/mcp_bridge.py +3 -1
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/monitoring/__init__.py +39 -46
- asher_cli-1.1.0/asher/theme.py +132 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/ui/__init__.py +53 -56
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/ui/style.tcss +26 -26
- {asher_cli-1.0.1 → asher_cli-1.1.0}/cliff.toml +3 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/docs/ROADMAP.md +3 -1
- {asher_cli-1.0.1 → asher_cli-1.1.0}/pyproject.toml +22 -2
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_activity_labels.py +6 -5
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_auth.py +15 -4
- asher_cli-1.1.0/tests/test_headless.py +529 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_ui.py +8 -4
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/testhelpers.py +5 -4
- {asher_cli-1.0.1 → asher_cli-1.1.0}/uv.lock +170 -170
- {asher_cli-1.0.1 → asher_cli-1.1.0}/watchrun.py +11 -5
- asher_cli-1.0.1/.claude/hooks/block-env.ps1 +0 -9
- asher_cli-1.0.1/.claude/skills/release/SKILL.md +0 -43
- asher_cli-1.0.1/.github/workflows/bandit.yml +0 -52
- asher_cli-1.0.1/asher/__main__.py +0 -58
- asher_cli-1.0.1/asher/constants.py +0 -22
- {asher_cli-1.0.1 → asher_cli-1.1.0}/.claude/skills/pylitterbot-ref/SKILL.md +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/.claude/skills/textual/SKILL.md +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/.env.example +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/.githooks/pre-push +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/.github/pull_request_template.md +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/.github/workflows/ci.yml +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/.github/workflows/claude-code-review.yml +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/.github/workflows/claude.yml +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/.github/workflows/coverage.yml +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/.gitignore +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/.vscode/launch.json +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/.vscode/settings.json +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/.vscode/tasks.json +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/CODEOWNERS +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/LICENSE +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/app.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/__init__.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/cats.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/commands/base.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/config.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/faults.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/login_flow.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/mcp_config.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/notifications.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/robot_adapters.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/robot_protocol.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/asher/slash-commands/__init__.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/docs/roadmap-archive/README.md +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/docs/roadmap-archive/cat-panel-badges.md +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/docs/roadmap-archive/config-persistence.md +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/docs/roadmap-archive/desktop-notifications.md +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/docs/roadmap-archive/fault-monitoring.md +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/docs/roadmap-archive/headless-export.md +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/docs/roadmap-archive/history-export.md +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/docs/roadmap-archive/tab-completion.md +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/renovate.json +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/requirements.txt +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/test.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/__init__.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/conftest.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_app_pilot.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_auth_pilot.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_cats.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_commands_pilot.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_completion.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_config.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_connection.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_connection_mixin.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_export.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_faults.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_history_view.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_lr5_commands.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_mcp_bridge.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_mcp_command.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_mcp_config.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_missing_robot_commands.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_monitoring.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_new_commands_pilot.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_notifications.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_robot_adapters.py +0 -0
- {asher_cli-1.0.1 → asher_cli-1.1.0}/tests/test_version_command.py +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
payload = json.load(sys.stdin)
|
|
6
|
+
file_path = payload.get("file_path", "")
|
|
7
|
+
except (json.JSONDecodeError, ValueError):
|
|
8
|
+
sys.exit(0)
|
|
9
|
+
|
|
10
|
+
if file_path.endswith(".env"):
|
|
11
|
+
print("Blocked: .env contains real credentials — edit it manually if needed")
|
|
12
|
+
sys.exit(2)
|
|
13
|
+
|
|
14
|
+
sys.exit(0)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mcp-bridge
|
|
3
|
+
description: How asher-cli's /mcp command and the asher-mcp-launch credential bridge work. Load before changing asher/mcp_config.py, asher/mcp_bridge.py, or the /mcp slash command.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# MCP bridge (`/mcp`)
|
|
8
|
+
|
|
9
|
+
pylitterbot ships an optional MCP server (`pip install pylitterbot[mcp]`, run via `python -m pylitterbot.mcp`) that lets an MCP client like Claude Desktop monitor/control the robot directly. Its own docs configure it with plaintext credentials in the client's JSON config — asher-cli avoids that:
|
|
10
|
+
|
|
11
|
+
- `/mcp on|off|status` (in `asher/commands/__init__.py`, logic in `asher/mcp_config.py`) adds/removes an entry (named by `mcp_config._SERVER_NAME`) in every `claude_desktop_config.json` this OS's Claude Desktop might read (`mcp_config.config_paths()` — on Windows this includes both the standard installer path and any MSIX/Microsoft Store virtualized path). The entry's `command` is `sys.executable -m asher.mcp_bridge` — never the credentials themselves.
|
|
12
|
+
- `/mcp on` also auto-installs pylitterbot's `mcp` extra via `sys.executable -m pip install "pylitterbot[mcp]==<installed version>"` if the `mcp` package isn't importable yet.
|
|
13
|
+
- `asher/mcp_bridge.py` (console script `asher-mcp-launch`) is what Claude Desktop actually spawns. It reads email/password from the OS keyring at process start, sets them as `LITTER_ROBOT_USERNAME`/`LITTER_ROBOT_PASSWORD` (pylitterbot's expected names — note these differ from asher-cli's own `.env` var `LITTER_ROBOT_USER`) in that process's environment only, then execs `python -m pylitterbot.mcp`. No credentials ever touch the on-disk MCP config.
|
|
14
|
+
- `/mcp on` requires keyring credentials. If none are found but `.env` fallback credentials are set, it copies them into the keyring automatically (since the bridge process can't reliably discover a project-relative `.env` — Claude Desktop controls its working directory, not asher-cli).
|
|
15
|
+
- Requires the `mcp` extra: `uv sync --extra mcp` / `pip install asher-cli[mcp]`. Restart Claude Desktop after toggling for the change to take effect.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# These are supported funding model platforms
|
|
2
|
+
|
|
3
|
+
github: [karanshukla]
|
|
4
|
+
patreon: # Replace with a single Patreon username
|
|
5
|
+
open_collective: # Replace with a single Open Collective username
|
|
6
|
+
ko_fi: # Replace with a single Ko-fi username
|
|
7
|
+
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
8
|
+
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
9
|
+
liberapay: # Replace with a single Liberapay username
|
|
10
|
+
issuehunt: # Replace with a single IssueHunt username
|
|
11
|
+
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
12
|
+
polar: # Replace with a single Polar username
|
|
13
|
+
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
|
14
|
+
thanks_dev: # Replace with a single thanks.dev username
|
|
15
|
+
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Bandit is a security linter for Python, by PyCQA (Apache v2.0).
|
|
2
|
+
# https://pypi.org/project/bandit/
|
|
3
|
+
#
|
|
4
|
+
# Results land under the repository's Security → Code scanning tab.
|
|
5
|
+
#
|
|
6
|
+
# Scan configuration — which paths are excluded, which checks are skipped and
|
|
7
|
+
# why — lives in [tool.bandit] in pyproject.toml, so `uv run poe security`
|
|
8
|
+
# reproduces this job's findings locally.
|
|
9
|
+
|
|
10
|
+
name: Bandit
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
branches: ["main", "release/*"]
|
|
15
|
+
pull_request:
|
|
16
|
+
# The branches below must be a subset of the branches above
|
|
17
|
+
branches: ["main"]
|
|
18
|
+
schedule:
|
|
19
|
+
- cron: '32 0 * * 1'
|
|
20
|
+
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
|
|
24
|
+
jobs:
|
|
25
|
+
bandit:
|
|
26
|
+
permissions:
|
|
27
|
+
contents: read # for actions/checkout to fetch code
|
|
28
|
+
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
|
|
29
|
+
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
|
|
30
|
+
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
34
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
35
|
+
|
|
36
|
+
- name: Bandit scan
|
|
37
|
+
# --exit-zero: findings must not fail the job, or the upload step below
|
|
38
|
+
# is skipped and code scanning keeps showing the previous run's alerts.
|
|
39
|
+
run: >-
|
|
40
|
+
uvx --from 'bandit[toml,sarif]' bandit
|
|
41
|
+
-c pyproject.toml -r . -f sarif -o bandit.sarif --exit-zero
|
|
42
|
+
|
|
43
|
+
- name: Upload SARIF to code scanning
|
|
44
|
+
uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
|
45
|
+
with:
|
|
46
|
+
sarif_file: bandit.sarif
|
|
47
|
+
category: bandit
|
|
@@ -44,26 +44,29 @@ jobs:
|
|
|
44
44
|
needs: [build, publish]
|
|
45
45
|
runs-on: ubuntu-latest
|
|
46
46
|
steps:
|
|
47
|
-
# Full history (fetch-depth: 0) so git-cliff can walk commits/tags.
|
|
48
47
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
49
|
-
with:
|
|
50
|
-
fetch-depth: 0
|
|
51
48
|
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
52
49
|
with:
|
|
53
50
|
name: dist
|
|
54
51
|
path: dist/
|
|
55
|
-
-
|
|
56
|
-
- name: Install git-cliff
|
|
57
|
-
run: uv tool install git-cliff
|
|
58
|
-
- name: Generate release notes with git-cliff
|
|
52
|
+
- name: Extract release notes from CHANGELOG.md
|
|
59
53
|
env:
|
|
60
54
|
VERSION: ${{ needs.build.outputs.version }}
|
|
61
55
|
run: |
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
|
|
56
|
+
# CHANGELOG.md is the single source of release prose: it is generated
|
|
57
|
+
# by git-cliff (see `poe changelog-release`) but may be hand-refined
|
|
58
|
+
# before the release is cut, and re-deriving the notes from commits
|
|
59
|
+
# here would silently discard those edits.
|
|
60
|
+
awk -v hdr="## [${VERSION}]" '
|
|
61
|
+
index($0, hdr) == 1 { found = 1; next }
|
|
62
|
+
found && index($0, "## [") == 1 { exit }
|
|
63
|
+
found { print }
|
|
64
|
+
' CHANGELOG.md > RELEASE_NOTES.md
|
|
65
|
+
if [ ! -s RELEASE_NOTES.md ]; then
|
|
66
|
+
echo "::error::CHANGELOG.md has no '## [${VERSION}]' section." \
|
|
67
|
+
"Run 'uv run poe changelog-release ${VERSION}' and commit it before releasing."
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
67
70
|
- name: Create release
|
|
68
71
|
env:
|
|
69
72
|
GH_TOKEN: ${{ github.token }}
|
|
@@ -7,6 +7,35 @@ 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.1.0] - 2026-08-09
|
|
11
|
+
|
|
12
|
+
### 🚀 Features
|
|
13
|
+
|
|
14
|
+
- *(headless)* Every robot command now runs without the TUI as `asher <command>`, with `--json` output and `--robot` selection; `--export` stays as a deprecated alias
|
|
15
|
+
- *(theme)* Catppuccin Mocha throughout — one palette in `asher/theme.py` drives the TUI stylesheet and every Rich style, replacing ad-hoc hexes across eleven modules
|
|
16
|
+
|
|
17
|
+
### 📚 Documentation
|
|
18
|
+
|
|
19
|
+
- Point CLAUDE.md at the release process
|
|
20
|
+
|
|
21
|
+
### 👷 CI/CD
|
|
22
|
+
|
|
23
|
+
- *(release)* Build release notes from CHANGELOG.md
|
|
24
|
+
|
|
25
|
+
### 🔧 Miscellaneous
|
|
26
|
+
|
|
27
|
+
- Trim CLAUDE.md, split MCP bridge docs into a skill, fix env-guard hook
|
|
28
|
+
|
|
29
|
+
## [1.0.2] - 2026-08-05
|
|
30
|
+
|
|
31
|
+
### 🐛 Bug Fixes
|
|
32
|
+
|
|
33
|
+
- Silence bandit noise and resolve dependency advisories
|
|
34
|
+
|
|
35
|
+
### 🔧 Miscellaneous
|
|
36
|
+
|
|
37
|
+
- Enable Bandit B101 for asher/ and bump version to 1.0.2
|
|
38
|
+
|
|
10
39
|
## [1.0.1] - 2026-08-04
|
|
11
40
|
|
|
12
41
|
### 🚀 Features
|
|
@@ -24,7 +53,6 @@ Generated by [git-cliff](https://git-cliff.org) from conventional commits.
|
|
|
24
53
|
|
|
25
54
|
### 📚 Documentation
|
|
26
55
|
|
|
27
|
-
- *(changelog)* Regenerate for next release, trim duplicate README list
|
|
28
56
|
- *(roadmap)* Archive completed sections to docs/roadmap-archive/
|
|
29
57
|
|
|
30
58
|
### 🔧 Miscellaneous
|
|
@@ -43,7 +71,6 @@ Generated by [git-cliff](https://git-cliff.org) from conventional commits.
|
|
|
43
71
|
|
|
44
72
|
### 📚 Documentation
|
|
45
73
|
|
|
46
|
-
- *(changelog)* Cut v0.2.0
|
|
47
74
|
- *(commands)* Sync stale command lists in CLAUDE.md and slash-commands docstring
|
|
48
75
|
- *(readme)* Sync command tables with code, add changelog
|
|
49
76
|
|
|
@@ -4,21 +4,9 @@ Terminal dashboard for Litter Robot (LR3/LR4/LR5) via the Whisker cloud API.
|
|
|
4
4
|
|
|
5
5
|
## Stack
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
7
|
+
- **keyring** resolves to the OS-native credential store: Windows Credential Manager / macOS Keychain / Linux Secret Service.
|
|
8
|
+
|
|
9
|
+
See `pyproject.toml` for the full dependency and dev-tooling list (`[project].dependencies`, `[dependency-groups].dev`, `[tool.poe.tasks]`).
|
|
22
10
|
|
|
23
11
|
## Entry points
|
|
24
12
|
|
|
@@ -37,6 +25,7 @@ asher/
|
|
|
37
25
|
auth.py LoginScreen modal (ModalScreen[tuple[str,str]]) — available, not primary flow
|
|
38
26
|
helpers.py fmt_ago(), drawer_bar(), ts(), robot_model() (pure, testable)
|
|
39
27
|
constants.py STATUS_COLORS, ROBOT_MODELS
|
|
28
|
+
theme.py Catppuccin Mocha palette + semantic roles (BACKGROUND, MUTED, DANGER, …) + CSS_VARIABLES/apply() — the only place a hex literal belongs
|
|
40
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)
|
|
41
30
|
notifications.py desktop toast + audible alert façade over plyer (fire/beep, always-safe no-op on failure/headless)
|
|
42
31
|
cats.py CATS dict (ASCII art)
|
|
@@ -47,9 +36,10 @@ asher/
|
|
|
47
36
|
mcp_bridge.py asher-mcp-launch console script — keyring-backed pylitterbot MCP launcher
|
|
48
37
|
faults.py check_faults(robot) — model-scoped safety/component fault detection (status enum + per-model attr allowlist incl. LR4 USB power fault; hopper never a fault)
|
|
49
38
|
history_view.py HistoryScreen (ModalScreen) + format_history_rows()/format_history_text() — scrollable activity-history pager pushed by the `history` command; `c` copies the full history (plain text) to the clipboard via action_copy_all()
|
|
50
|
-
export.py shared activity-history CSV core +
|
|
39
|
+
export.py shared activity-history CSV core + exit-code contract: build_history_csv(), resolve_dest(), resolve_robot(), parse_days(), EXIT_*, ExportError — no Textual imports; the TUI `export` command and `asher export` both call build_history_csv()
|
|
40
|
+
headless.py headless command surface for `asher <command>` — Session/Result/CommandError, the COMMANDS registry, and run(); plain strings only, no Textual, routes model differences through RobotAdapter
|
|
51
41
|
completion.py pure helpers for command completion: slash popup (slash_matches, enter_completes, render_completion) + inline ghost text (CommandSuggester) — fed by _registry, no Textual imports except the Suggester base class
|
|
52
|
-
__main__.py main() entry point
|
|
42
|
+
__main__.py main() entry point — argparse subcommands (headless) vs no-args (TUI); `--export` kept as a deprecated alias
|
|
53
43
|
commands/
|
|
54
44
|
base.py Command ABC, SlashCommand, CommandRegistry
|
|
55
45
|
__init__.py CommandsMixin — all command classes + registry + dispatch
|
|
@@ -76,11 +66,13 @@ tests/
|
|
|
76
66
|
test_mcp_command.py /mcp slash command dispatch
|
|
77
67
|
test_faults.py check_faults() — safety statuses, attribute faults, graceful degradation
|
|
78
68
|
test_history_view.py format_history_rows()/format_history_text() + HistoryScreen structure, copy-all + Pilot push/dismiss
|
|
79
|
-
test_export.py build_history_csv/resolve_dest/resolve_robot/parse_days (pure) +
|
|
69
|
+
test_export.py build_history_csv/resolve_dest/resolve_robot/parse_days (pure) + the legacy `--export` flag path (no Pilot, mocks _connect_headless)
|
|
70
|
+
test_headless.py headless registry/rendering (pure) + every command handler against mock robots + run() exit codes + the argparse subcommand surface
|
|
80
71
|
test_completion.py slash_matches/enter_completes/render_completion (pure) + Pilot overlay visibility/navigation/accept
|
|
81
72
|
|
|
82
73
|
.github/workflows/
|
|
83
74
|
ci.yml ruff + mypy + pytest on every push/PR
|
|
75
|
+
bandit.yml bandit security scan → SARIF → code scanning (config: [tool.bandit] in pyproject.toml)
|
|
84
76
|
```
|
|
85
77
|
|
|
86
78
|
## Credentials
|
|
@@ -100,32 +92,22 @@ LITTER_ROBOT_PASSWORD=...
|
|
|
100
92
|
Keyring service name: `asher-cli`, keys `email` and `password`.
|
|
101
93
|
Helper functions in `asher/connection/__init__.py`: `_keyring_load()`, `_keyring_save()`, `_keyring_delete()`.
|
|
102
94
|
|
|
103
|
-
## MCP bridge (`/mcp`)
|
|
104
|
-
|
|
105
|
-
pylitterbot ships an optional MCP server (`pip install pylitterbot[mcp]`, run via `python -m pylitterbot.mcp`) that lets an MCP client like Claude Desktop monitor/control the robot directly. Its own docs configure it with plaintext credentials in the client's JSON config — asher-cli avoids that:
|
|
106
|
-
|
|
107
|
-
- `/mcp on|off|status` (in `asher/commands/__init__.py`, logic in `asher/mcp_config.py`) adds/removes an entry (named by `mcp_config._SERVER_NAME`) in every `claude_desktop_config.json` this OS's Claude Desktop might read (`mcp_config.config_paths()` — on Windows this includes both the standard installer path and any MSIX/Microsoft Store virtualized path). The entry's `command` is `sys.executable -m asher.mcp_bridge` — never the credentials themselves.
|
|
108
|
-
- `/mcp on` also auto-installs pylitterbot's `mcp` extra via `sys.executable -m pip install "pylitterbot[mcp]==<installed version>"` if the `mcp` package isn't importable yet.
|
|
109
|
-
- `asher/mcp_bridge.py` (console script `asher-mcp-launch`) is what Claude Desktop actually spawns. It reads email/password from the OS keyring at process start, sets them as `LITTER_ROBOT_USERNAME`/`LITTER_ROBOT_PASSWORD` (pylitterbot's expected names — note these differ from asher-cli's own `.env` var `LITTER_ROBOT_USER`) in that process's environment only, then execs `python -m pylitterbot.mcp`. No credentials ever touch the on-disk MCP config.
|
|
110
|
-
- `/mcp on` requires keyring credentials. If none are found but `.env` fallback credentials are set, it copies them into the keyring automatically (since the bridge process can't reliably discover a project-relative `.env` — Claude Desktop controls its working directory, not asher-cli).
|
|
111
|
-
- Requires the `mcp` extra: `uv sync --extra mcp` / `pip install asher-cli[mcp]`. Restart Claude Desktop after toggling for the change to take effect.
|
|
112
|
-
|
|
113
95
|
## Command convention
|
|
114
96
|
|
|
115
|
-
|
|
116
|
-
`clean`, `status`, `info`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto`, `night-light-brightness <level>`, `panel-brightness <low|medium|high>`, `wait-time <minutes>`, `power on|off`, `rename <name>`, `insight [days|month]`, `sleep-schedule`, `privacy on|off`, `volume <0-100>`, `camera-audio on|off`, `drawer-reset`, `history [count|all]`, `export [days|month]`, `clear`, `help`, `quit`
|
|
97
|
+
Command names, slash-command names, and their args are not listed here — see the `_registry` in `asher/commands/__init__.py`, which is authoritative; `/help` renders it at runtime. `/mcp`'s credential-bridging design is documented in the `mcp-bridge` skill.
|
|
117
98
|
|
|
118
|
-
**
|
|
119
|
-
`/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`, `/version`, `/mcp on|off|status`, `/exit`
|
|
99
|
+
**Normal commands** (no prefix) are robot actions only; **slash commands** (`/` prefix) are app management only.
|
|
120
100
|
|
|
121
101
|
`/refresh`, `/cat`, `/pet`, and `/notify` persist their settings to `~/.asher-cli/config.json` (via `asher.config.update()`), so they survive restarts. Credentials and the preferred-robot serial stay in the OS keyring; the config file holds only non-secret UI preferences.
|
|
122
102
|
|
|
123
|
-
|
|
103
|
+
Do not add robot-control commands as slash commands, and do not add app-management commands as bare commands.
|
|
124
104
|
|
|
125
105
|
**Special cases** (accepted both with and without `/`):
|
|
126
106
|
`exit`, `quit`, `q` — exit the app
|
|
127
107
|
|
|
128
|
-
|
|
108
|
+
**Headless commands** (`asher <command>`) are a parallel registry in `asher/headless.py`: same robot actions, no Textual, plain-string + JSON output. Slash commands have no headless equivalent — they configure the TUI, which isn't running. A robot command worth scripting should exist in both registries; the shared logic lives in `RobotAdapter`, not in either command class.
|
|
109
|
+
|
|
110
|
+
> If you add a command, update the tables in `README.md` and the list in `asher/slash-commands/__init__.py`. If it's a robot command, consider adding it to `COMMANDS` in `asher/headless.py` too.
|
|
129
111
|
|
|
130
112
|
## Architecture
|
|
131
113
|
|
|
@@ -174,17 +156,15 @@ LoginScreen (ModalScreen) — available in auth.py but not the primary auth path
|
|
|
174
156
|
|
|
175
157
|
## Robot compatibility
|
|
176
158
|
|
|
177
|
-
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.
|
|
159
|
+
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. See the `pylitterbot-ref` skill for the confirmed API surface.
|
|
178
160
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
- `
|
|
184
|
-
-
|
|
185
|
-
- `
|
|
186
|
-
- `robot.set_night_light_brightness(int)` or `robot.set_night_light_mode(NightLightMode)`
|
|
187
|
-
- `robot.get_activity_history(limit=int)` → list of `Activity` objects with `.timestamp` and `.action` (`LitterBoxStatus` enum)
|
|
161
|
+
## Colour
|
|
162
|
+
|
|
163
|
+
Every colour comes from `asher/theme.py` (Catppuccin Mocha). Reference the **semantic roles** (`theme.MUTED`, `theme.DANGER`, …), not the raw swatches (`theme.OVERLAY0`) and never a hex literal — a re-flavour then only repoints the roles.
|
|
164
|
+
|
|
165
|
+
- **Rich styles:** `style=theme.ACCENT`, or `style=f"bold {theme.ACCENT}"`. Prefer building `Text` objects with explicit styles over `Text.from_markup` with inline colours.
|
|
166
|
+
- **`ui/style.tcss`:** use the `$asher-*` variables; `AsherApp.get_css_variables()` supplies them.
|
|
167
|
+
- **Inline `CSS`/`DEFAULT_CSS` on a Screen or Widget:** wrap the block in `theme.apply(...)`, which bakes the `$asher-*` values in at class-definition time. A screen mounted on a host app that isn't `AsherApp` (as the Pilot tests do) would otherwise fail to parse.
|
|
188
168
|
|
|
189
169
|
## Code comments
|
|
190
170
|
|
|
@@ -196,6 +176,7 @@ Don't add comments above functions or inline unless the WHY is genuinely non-obv
|
|
|
196
176
|
- All command execution runs in `@work` async workers to keep the UI responsive
|
|
197
177
|
- Cat modes: `idle`, `happy`, `cleaning` (animated), `sleeping`, `error`, `full`
|
|
198
178
|
- `VERSION` is read from `importlib.metadata.version("asher-cli")` — falls back to `"dev"` when running from source
|
|
179
|
+
- **No `assert` in `asher/`** — Bandit enforces this (B101 is enabled; only `tests/` is exempt, via `exclude_dirs`). For a `requires_robot` command, narrow with `if app._robot is None: return` rather than an assert: `_dispatch_command` already rejects the disconnected case, and unlike `assert` the guard survives `python -O`
|
|
199
180
|
- 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.
|
|
200
181
|
- `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)
|
|
201
182
|
|
|
@@ -211,6 +192,8 @@ Commands that need a confirmed cloud state before showing a result (e.g. sleep/w
|
|
|
211
192
|
|
|
212
193
|
**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`.
|
|
213
194
|
|
|
195
|
+
**Add a headless command:** write `async def _my_command(session, args) -> Result` in `asher/headless.py` and add a `HeadlessCommand(...)` entry to `COMMANDS`. The argparse subparser is generated from the registry — nothing to add in `__main__.py`. Build the `Result` with `_rows()` (read commands) or `_outcome()` (actions) so text and JSON stay in step, and raise `CommandError` rather than printing.
|
|
196
|
+
|
|
214
197
|
**Change poll interval:** `self.set_interval(300, ...)` in `on_mount`.
|
|
215
198
|
|
|
216
199
|
**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")`.
|
|
@@ -219,17 +202,16 @@ Commands that need a confirmed cloud state before showing a result (e.g. sleep/w
|
|
|
219
202
|
|
|
220
203
|
## Dev workflow
|
|
221
204
|
|
|
222
|
-
|
|
223
|
-
uv sync # install all deps (including dev group)
|
|
224
|
-
uv run poe dev # run with CSS hot reload (textual --dev)
|
|
225
|
-
uv run poe watch # run with Python auto-restart on file change (watchfiles)
|
|
226
|
-
uv run poe test # run test suite
|
|
227
|
-
uv run poe check # ruff + mypy + pytest (same as CI)
|
|
228
|
-
uv run poe fix # auto-fix ruff issues
|
|
229
|
-
```
|
|
205
|
+
See `[tool.poe.tasks]` in `pyproject.toml` for the full task list (`uv run poe <task>`).
|
|
230
206
|
|
|
231
207
|
Pre-push hook (`.githooks/pre-push`) runs: ruff check → ruff format --check → mypy. Tests are not in the hook — run them manually.
|
|
232
208
|
|
|
209
|
+
## Releasing
|
|
210
|
+
|
|
211
|
+
Follow [README § Releasing](README.md#releasing) exactly, in order: `uv run poe changelog-release X.Y.Z` and commit the result → `uv run bump-my-version bump <part>` (this commits **and tags**) → `git push && git push --tags` → only then cut and push `release/X.Y.Z`.
|
|
212
|
+
|
|
213
|
+
Regenerate with `changelog-release X.Y.Z`, never plain `changelog` — the workflow lifts the `## [X.Y.Z]` section out of the committed `CHANGELOG.md` verbatim for the GitHub Release body, and a file still saying `## [Unreleased]` fails the release job. That extraction is also why hand-refinements to a section survive into the release notes, and why re-running either changelog task afterwards silently discards them: regeneration always re-derives from commits. Refine last.
|
|
214
|
+
|
|
233
215
|
## Testing notes
|
|
234
216
|
|
|
235
217
|
- Pilot-based integration tests use `app.run_test()` with `await pilot.pause()` before querying widgets
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asher-cli
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.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
|
|
@@ -75,7 +75,8 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
|
|
|
75
75
|
- 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`, `/version`, `/mcp on|off|status`, `/exit`
|
|
76
76
|
- Slash-command tab completion — type `/` and a Claude Code-style overlay lists matching commands; `↑`/`↓` to move, `Tab` or `Enter` to accept, `Esc` to dismiss
|
|
77
77
|
- Inline ghost-text completion for bare commands — type a prefix (`cle`) and the rest (`an`) appears greyed; `Tab` or `→` to accept → `clean`
|
|
78
|
-
- Headless
|
|
78
|
+
- Headless mode — every robot command also runs without the TUI (`asher status`, `asher clean`, `asher night-light auto`, `asher export 7`), with `--json` output and documented exit codes for cron / Task Scheduler / SSH
|
|
79
|
+
- Catppuccin Mocha throughout — one palette in `asher/theme.py` drives the TUI stylesheet and every Rich style
|
|
79
80
|
- Cat animation panel that reacts to robot state
|
|
80
81
|
- Command history (↑/↓ arrows)
|
|
81
82
|
- Real-time updates via WebSocket; 5-minute poll fallback
|
|
@@ -177,38 +178,61 @@ LITTER_ROBOT_PASSWORD=yourpassword
|
|
|
177
178
|
|
|
178
179
|
**Keyboard shortcuts:** `Ctrl+L` clears the log, `Ctrl+C` quits. While typing a `/` slash command, `↑`/`↓` move through completions, `Tab` or `Enter` accepts, `Esc` dismisses. While typing a bare command, a greyed ghost suggestion appears — `Tab` or `→` accepts it.
|
|
179
180
|
|
|
180
|
-
### Headless
|
|
181
|
+
### Headless commands (cron / Task Scheduler / SSH)
|
|
181
182
|
|
|
182
|
-
|
|
183
|
+
Every robot action the dashboard offers is also a subcommand that runs **without launching the TUI** — `asher` with no arguments still opens the interactive dashboard.
|
|
183
184
|
|
|
184
185
|
```bash
|
|
185
|
-
asher
|
|
186
|
-
asher
|
|
187
|
-
asher
|
|
186
|
+
asher status at-a-glance state
|
|
187
|
+
asher info model, serial, firmware, settings
|
|
188
|
+
asher clean start a clean cycle
|
|
189
|
+
asher night-light auto set night light mode
|
|
190
|
+
asher wait-time 7 set the clean-cycle wait time
|
|
191
|
+
asher history 20 recent activity, newest first
|
|
192
|
+
asher insight 7 cycle-usage statistics
|
|
193
|
+
asher export 7 --output ~/hist.csv activity history to CSV
|
|
188
194
|
```
|
|
189
195
|
|
|
190
|
-
|
|
196
|
+
`asher --help` lists every command; `asher <command> --help` shows its arguments. The full set: `status`, `info`, `robots`, `pets`, `history`, `insight`, `sleep-schedule`, `clean`, `lock`, `unlock`, `sleep`, `wake`, `night-light`, `night-light-brightness`, `panel-brightness`, `wait-time`, `power`, `rename`, `privacy`, `volume`, `camera-audio`, `drawer-reset`, `export`.
|
|
197
|
+
|
|
198
|
+
Two flags apply to all of them:
|
|
199
|
+
|
|
200
|
+
- `--robot <index|name>` — an index or a partial, case-insensitive name (defaults to your saved preferred robot, else the first)
|
|
201
|
+
- `--json` — machine-readable output instead of aligned text, for piping into `jq`
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
asher status --json | jq -r '.drawer'
|
|
205
|
+
asher history 50 --json | jq '[.events[] | select(.event | startswith("Cat"))] | length'
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Credentials use the same keyring → `.env` priority as the TUI, but with **no interactive login prompt** — a scheduled task can't type a password, so sign in once with `/login` first.
|
|
191
209
|
|
|
192
210
|
Exit codes for scripting:
|
|
193
211
|
|
|
194
212
|
| Code | Meaning |
|
|
195
213
|
|---|---|
|
|
196
|
-
| `0` |
|
|
214
|
+
| `0` | command succeeded |
|
|
197
215
|
| `1` | no credentials found (keyring or `.env`) |
|
|
198
216
|
| `2` | connection or API failure |
|
|
199
217
|
| `3` | failed to write the CSV (permissions, disk full) |
|
|
200
218
|
| `4` | `--robot` matched no robot on the account |
|
|
219
|
+
| `5` | bad argument, or the robot/cloud rejected the command |
|
|
201
220
|
|
|
202
221
|
```bash
|
|
203
222
|
# crontab — nightly export at 03:00
|
|
204
|
-
0 3 * * * /usr/bin/env asher
|
|
223
|
+
0 3 * * * /usr/bin/env asher export 7 --output /home/me/litter-history.csv >> /var/log/asher-export.log 2>&1
|
|
224
|
+
|
|
225
|
+
# alert when the drawer passes 85%
|
|
226
|
+
0 * * * * [ "$(asher status --json | jq -r '.drawer' | tr -d %)" -gt 85 ] && notify-send "Litter drawer full"
|
|
205
227
|
```
|
|
206
228
|
|
|
207
229
|
```powershell
|
|
208
230
|
# Windows Task Scheduler action
|
|
209
|
-
asher.exe
|
|
231
|
+
asher.exe export 7 --output C:\Users\me\litter-history.csv
|
|
210
232
|
```
|
|
211
233
|
|
|
234
|
+
`asher --export [days]` remains as a deprecated alias for `asher export [days]`, so existing cron entries keep working.
|
|
235
|
+
|
|
212
236
|
## Configuration
|
|
213
237
|
|
|
214
238
|
Runtime settings persist across restarts in `~/.asher-cli/config.json`, so you don't have to re-apply `/refresh 60`, `/cat colour #ff79c6`, or `/pet 1` every launch. The file is auto-created on first change and holds six non-secret UI preferences:
|
|
@@ -227,10 +251,11 @@ Credentials and the preferred-robot serial stay in the OS keyring; `.env` vars s
|
|
|
227
251
|
## Releasing
|
|
228
252
|
|
|
229
253
|
```bash
|
|
230
|
-
# 1.
|
|
231
|
-
|
|
254
|
+
# 1. file the pending commits under the version you're about to cut, refine the
|
|
255
|
+
# prose if a squashed commit needs unpacking, then commit it
|
|
256
|
+
uv run poe changelog-release X.Y.Z
|
|
232
257
|
git add CHANGELOG.md
|
|
233
|
-
git commit -m "docs(changelog):
|
|
258
|
+
git commit -m "docs(changelog): cut vX.Y.Z"
|
|
234
259
|
|
|
235
260
|
# 2. bump version, commit, and tag in one step, then push with tags
|
|
236
261
|
uv run bump-my-version bump patch # 0.2.0 → 0.2.1
|
|
@@ -240,7 +265,7 @@ uv run bump-my-version bump major # 0.2.0 → 1.0.0
|
|
|
240
265
|
git push && git push --tags
|
|
241
266
|
|
|
242
267
|
# 3. push the release branch to trigger PyPI publish (OIDC) + GitHub Release
|
|
243
|
-
# (release notes
|
|
268
|
+
# (release notes = the `## [X.Y.Z]` section of CHANGELOG.md, verbatim)
|
|
244
269
|
git checkout -b release/X.Y.Z
|
|
245
270
|
git push origin release/X.Y.Z
|
|
246
271
|
```
|
|
@@ -250,6 +275,17 @@ conventional commits — see [`cliff.toml`](cliff.toml). Commit messages must us
|
|
|
250
275
|
`feat:`/`fix:`/`docs:`/etc. prefixes (optionally scoped, e.g. `feat(robot):`)
|
|
251
276
|
to appear in the changelog.
|
|
252
277
|
|
|
278
|
+
Use `poe changelog-release X.Y.Z` — not plain `poe changelog` — when cutting a
|
|
279
|
+
release. Both regenerate the whole file, but only the former files the pending
|
|
280
|
+
commits under `## [X.Y.Z]` instead of `## [Unreleased]`, and the release
|
|
281
|
+
workflow extracts exactly that section for the GitHub Release body. It fails
|
|
282
|
+
loudly if the section is missing, so the notes can never come out stale.
|
|
283
|
+
|
|
284
|
+
Because the workflow reads the committed file rather than re-deriving from
|
|
285
|
+
commits, any hand-refinement you make to a section survives into the release
|
|
286
|
+
notes — but re-running either task afterwards regenerates from commits and
|
|
287
|
+
discards it. Refine last, right before committing.
|
|
288
|
+
|
|
253
289
|
## Troubleshooting
|
|
254
290
|
|
|
255
291
|
**`asher` not found after `pip install asher-cli`**
|
|
@@ -339,6 +375,16 @@ uv run poe types # mypy
|
|
|
339
375
|
uv run poe check # run all of the above + tests (same as CI)
|
|
340
376
|
```
|
|
341
377
|
|
|
378
|
+
### 6. Security scan
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
uv run poe security # bandit, same config the Bandit workflow uses
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Scan settings live in `[tool.bandit]` in `pyproject.toml`, so local runs and the
|
|
385
|
+
Bandit workflow report identical findings. Results from `main` and pull requests
|
|
386
|
+
are published to the repository's **Security → Code scanning** tab.
|
|
387
|
+
|
|
342
388
|
CI runs on Python 3.10 / 3.11 / 3.12 across Ubuntu, Windows, and macOS on every push.
|
|
343
389
|
|
|
344
390
|
## Changelog
|