asher-cli 1.0.2__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.
Files changed (102) hide show
  1. asher_cli-1.1.0/.claude/hooks/block-env.py +14 -0
  2. {asher_cli-1.0.2 → asher_cli-1.1.0}/.claude/settings.json +1 -1
  3. asher_cli-1.1.0/.claude/skills/mcp-bridge/SKILL.md +15 -0
  4. asher_cli-1.1.0/.github/FUNDING.yml +15 -0
  5. {asher_cli-1.0.2 → asher_cli-1.1.0}/.github/workflows/release.yml +15 -12
  6. {asher_cli-1.0.2 → asher_cli-1.1.0}/CHANGELOG.md +19 -6
  7. {asher_cli-1.0.2 → asher_cli-1.1.0}/CLAUDE.md +32 -53
  8. {asher_cli-1.0.2 → asher_cli-1.1.0}/PKG-INFO +51 -15
  9. {asher_cli-1.0.2 → asher_cli-1.1.0}/README.md +50 -14
  10. asher_cli-1.1.0/asher/__main__.py +116 -0
  11. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/activity_labels.py +22 -20
  12. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/app.py +9 -0
  13. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/auth.py +13 -11
  14. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/commands/__init__.py +49 -40
  15. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/completion.py +6 -4
  16. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/connection/__init__.py +4 -3
  17. asher_cli-1.1.0/asher/constants.py +24 -0
  18. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/export.py +17 -42
  19. asher_cli-1.1.0/asher/headless.py +626 -0
  20. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/helpers.py +5 -4
  21. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/history_view.py +19 -18
  22. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/monitoring/__init__.py +35 -34
  23. asher_cli-1.1.0/asher/theme.py +132 -0
  24. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/ui/__init__.py +53 -56
  25. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/ui/style.tcss +26 -26
  26. {asher_cli-1.0.2 → asher_cli-1.1.0}/cliff.toml +3 -0
  27. {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/ROADMAP.md +3 -1
  28. {asher_cli-1.0.2 → asher_cli-1.1.0}/pyproject.toml +5 -2
  29. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_activity_labels.py +6 -5
  30. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_auth.py +15 -4
  31. asher_cli-1.1.0/tests/test_headless.py +529 -0
  32. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_ui.py +8 -4
  33. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/testhelpers.py +5 -4
  34. {asher_cli-1.0.2 → asher_cli-1.1.0}/uv.lock +1 -1
  35. asher_cli-1.0.2/.claude/hooks/block-env.ps1 +0 -9
  36. asher_cli-1.0.2/.claude/skills/release/SKILL.md +0 -43
  37. asher_cli-1.0.2/asher/__main__.py +0 -58
  38. asher_cli-1.0.2/asher/constants.py +0 -22
  39. {asher_cli-1.0.2 → asher_cli-1.1.0}/.claude/skills/pylitterbot-ref/SKILL.md +0 -0
  40. {asher_cli-1.0.2 → asher_cli-1.1.0}/.claude/skills/textual/SKILL.md +0 -0
  41. {asher_cli-1.0.2 → asher_cli-1.1.0}/.env.example +0 -0
  42. {asher_cli-1.0.2 → asher_cli-1.1.0}/.githooks/pre-push +0 -0
  43. {asher_cli-1.0.2 → asher_cli-1.1.0}/.github/pull_request_template.md +0 -0
  44. {asher_cli-1.0.2 → asher_cli-1.1.0}/.github/workflows/bandit.yml +0 -0
  45. {asher_cli-1.0.2 → asher_cli-1.1.0}/.github/workflows/ci.yml +0 -0
  46. {asher_cli-1.0.2 → asher_cli-1.1.0}/.github/workflows/claude-code-review.yml +0 -0
  47. {asher_cli-1.0.2 → asher_cli-1.1.0}/.github/workflows/claude.yml +0 -0
  48. {asher_cli-1.0.2 → asher_cli-1.1.0}/.github/workflows/coverage.yml +0 -0
  49. {asher_cli-1.0.2 → asher_cli-1.1.0}/.gitignore +0 -0
  50. {asher_cli-1.0.2 → asher_cli-1.1.0}/.vscode/launch.json +0 -0
  51. {asher_cli-1.0.2 → asher_cli-1.1.0}/.vscode/settings.json +0 -0
  52. {asher_cli-1.0.2 → asher_cli-1.1.0}/.vscode/tasks.json +0 -0
  53. {asher_cli-1.0.2 → asher_cli-1.1.0}/CODEOWNERS +0 -0
  54. {asher_cli-1.0.2 → asher_cli-1.1.0}/LICENSE +0 -0
  55. {asher_cli-1.0.2 → asher_cli-1.1.0}/app.py +0 -0
  56. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/__init__.py +0 -0
  57. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/cats.py +0 -0
  58. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/commands/base.py +0 -0
  59. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/config.py +0 -0
  60. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/faults.py +0 -0
  61. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/login_flow.py +0 -0
  62. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/mcp_bridge.py +0 -0
  63. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/mcp_config.py +0 -0
  64. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/notifications.py +0 -0
  65. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/robot_adapters.py +0 -0
  66. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/robot_protocol.py +0 -0
  67. {asher_cli-1.0.2 → asher_cli-1.1.0}/asher/slash-commands/__init__.py +0 -0
  68. {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/roadmap-archive/README.md +0 -0
  69. {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/roadmap-archive/cat-panel-badges.md +0 -0
  70. {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/roadmap-archive/config-persistence.md +0 -0
  71. {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/roadmap-archive/desktop-notifications.md +0 -0
  72. {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/roadmap-archive/fault-monitoring.md +0 -0
  73. {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/roadmap-archive/headless-export.md +0 -0
  74. {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/roadmap-archive/history-export.md +0 -0
  75. {asher_cli-1.0.2 → asher_cli-1.1.0}/docs/roadmap-archive/tab-completion.md +0 -0
  76. {asher_cli-1.0.2 → asher_cli-1.1.0}/renovate.json +0 -0
  77. {asher_cli-1.0.2 → asher_cli-1.1.0}/requirements.txt +0 -0
  78. {asher_cli-1.0.2 → asher_cli-1.1.0}/test.py +0 -0
  79. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/__init__.py +0 -0
  80. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/conftest.py +0 -0
  81. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_app_pilot.py +0 -0
  82. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_auth_pilot.py +0 -0
  83. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_cats.py +0 -0
  84. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_commands_pilot.py +0 -0
  85. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_completion.py +0 -0
  86. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_config.py +0 -0
  87. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_connection.py +0 -0
  88. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_connection_mixin.py +0 -0
  89. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_export.py +0 -0
  90. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_faults.py +0 -0
  91. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_history_view.py +0 -0
  92. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_lr5_commands.py +0 -0
  93. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_mcp_bridge.py +0 -0
  94. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_mcp_command.py +0 -0
  95. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_mcp_config.py +0 -0
  96. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_missing_robot_commands.py +0 -0
  97. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_monitoring.py +0 -0
  98. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_new_commands_pilot.py +0 -0
  99. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_notifications.py +0 -0
  100. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_robot_adapters.py +0 -0
  101. {asher_cli-1.0.2 → asher_cli-1.1.0}/tests/test_version_command.py +0 -0
  102. {asher_cli-1.0.2 → asher_cli-1.1.0}/watchrun.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)
@@ -37,7 +37,7 @@
37
37
  "hooks": [
38
38
  {
39
39
  "type": "command",
40
- "command": "powershell -NonInteractive -ExecutionPolicy Bypass -File .claude/hooks/block-env.ps1"
40
+ "command": "uv run python .claude/hooks/block-env.py"
41
41
  }
42
42
  ]
43
43
  }
@@ -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']
@@ -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
- - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
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
- # Explicit -c cliff.toml avoids auto-discovering pyproject.toml.
63
- # Pin the just-released version so git-cliff groups its commits under
64
- # the right heading even before the tag is fetched, then emit only
65
- # that section (--latest) with the header stripped.
66
- git cliff -c cliff.toml --tag "v${VERSION}" --latest --strip header -o RELEASE_NOTES.md
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,25 @@ 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
+
10
29
  ## [1.0.2] - 2026-08-05
11
30
 
12
31
  ### 🐛 Bug Fixes
@@ -23,10 +42,6 @@ Generated by [git-cliff](https://git-cliff.org) from conventional commits.
23
42
 
24
43
  - *(history)* Copy-to-clipboard + discoverable footer hints
25
44
 
26
- ### 📚 Documentation
27
-
28
- - *(changelog)* Cut v1.0.1
29
-
30
45
  ## [1.0.0] - 2026-08-02
31
46
 
32
47
  ### 🚀 Features
@@ -38,7 +53,6 @@ Generated by [git-cliff](https://git-cliff.org) from conventional commits.
38
53
 
39
54
  ### 📚 Documentation
40
55
 
41
- - *(changelog)* Regenerate for next release, trim duplicate README list
42
56
  - *(roadmap)* Archive completed sections to docs/roadmap-archive/
43
57
 
44
58
  ### 🔧 Miscellaneous
@@ -57,7 +71,6 @@ Generated by [git-cliff](https://git-cliff.org) from conventional commits.
57
71
 
58
72
  ### 📚 Documentation
59
73
 
60
- - *(changelog)* Cut v0.2.0
61
74
  - *(commands)* Sync stale command lists in CLAUDE.md and slash-commands docstring
62
75
  - *(readme)* Sync command tables with code, add changelog
63
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
- - **Python 3.10+**
8
- - **Textual** — async TUI framework (`textual>=0.47`)
9
- - **pylitterbot** unofficial Whisker API wrapper (`pylitterbot>=3.0`)
10
- - **python-dotenv** — credential loading (`.env` fallback)
11
- - **keyring>=24** — OS credential store (Windows Credential Manager / macOS Keychain / Linux Secret Service)
12
-
13
- ## Tooling
14
-
15
- - **uv** — dependency management and task runner (`uv sync`, `uv run`)
16
- - **poethepoet** — task aliases via `uv run poe <task>`
17
- - **ruff** — linter and formatter
18
- - **mypy** — static type checking
19
- - **pytest + pytest-asyncio + pytest-cov** — tests
20
- - **textual-dev** — CSS hot reload devtools
21
- - **watchfiles** — Python auto-restart on file change
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 + headless export path: build_history_csv(), resolve_dest(), resolve_robot(), _run_headless_export(), ExportError — no Textual imports; both the TUI `export` command and `asher --export` call build_history_csv()
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,7 +66,8 @@ 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) + headless _run_headless_export (no Pilot, mocks _connect_headless)
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/
@@ -101,32 +92,22 @@ LITTER_ROBOT_PASSWORD=...
101
92
  Keyring service name: `asher-cli`, keys `email` and `password`.
102
93
  Helper functions in `asher/connection/__init__.py`: `_keyring_load()`, `_keyring_save()`, `_keyring_delete()`.
103
94
 
104
- ## MCP bridge (`/mcp`)
105
-
106
- 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:
107
-
108
- - `/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.
109
- - `/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.
110
- - `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.
111
- - `/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).
112
- - Requires the `mcp` extra: `uv sync --extra mcp` / `pip install asher-cli[mcp]`. Restart Claude Desktop after toggling for the change to take effect.
113
-
114
95
  ## Command convention
115
96
 
116
- **Normal commands** (no prefix)robot actions only:
117
- `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.
118
98
 
119
- **Slash commands** (`/` prefix) app management only:
120
- `/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.
121
100
 
122
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.
123
102
 
124
- > The authoritative list is the `_registry` in `asher/commands/__init__.py`; `/help` renders it at runtime. If you add a command, update the tables in `README.md` and the list in `asher/slash-commands/__init__.py`.
103
+ Do not add robot-control commands as slash commands, and do not add app-management commands as bare commands.
125
104
 
126
105
  **Special cases** (accepted both with and without `/`):
127
106
  `exit`, `quit`, `q` — exit the app
128
107
 
129
- Do not add robot-control commands as slash commands, and do not add app-management commands as bare commands.
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.
130
111
 
131
112
  ## Architecture
132
113
 
@@ -175,17 +156,15 @@ LoginScreen (ModalScreen) — available in auth.py but not the primary auth path
175
156
 
176
157
  ## Robot compatibility
177
158
 
178
- pylitterbot auto-detects robot type. Commands that differ per model are handled by `RobotAdapter` subclasses in `robot_adapters.py` — `make_adapter(robot)` returns the right one based on `type(robot).__name__`. Status-bar reads use `getattr(..., default)` for graceful degradation on older models. Tested API surface:
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.
179
160
 
180
- - `robot.name`, `robot.serial`, `robot.is_online`
181
- - `robot.status` (LitterBoxStatus enum)
182
- - `robot.waste_drawer_level` (0–100)
183
- - `robot.sleep_mode_enabled`, `robot.panel_lock_enabled`, `robot.night_light_mode_enabled`
184
- - `robot.last_seen` (datetime)
185
- - `robot.refresh()`, `robot.start_cleaning()`
186
- - `robot.set_sleep_mode(bool)`, `robot.set_panel_lockout(bool)`
187
- - `robot.set_night_light_brightness(int)` or `robot.set_night_light_mode(NightLightMode)`
188
- - `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.
189
168
 
190
169
  ## Code comments
191
170
 
@@ -213,6 +192,8 @@ Commands that need a confirmed cloud state before showing a result (e.g. sleep/w
213
192
 
214
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`.
215
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
+
216
197
  **Change poll interval:** `self.set_interval(300, ...)` in `on_mount`.
217
198
 
218
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")`.
@@ -221,18 +202,16 @@ Commands that need a confirmed cloud state before showing a result (e.g. sleep/w
221
202
 
222
203
  ## Dev workflow
223
204
 
224
- ```bash
225
- uv sync # install all deps (including dev group)
226
- uv run poe dev # run with CSS hot reload (textual --dev)
227
- uv run poe watch # run with Python auto-restart on file change (watchfiles)
228
- uv run poe test # run test suite
229
- uv run poe check # ruff + mypy + pytest (same as CI)
230
- uv run poe fix # auto-fix ruff issues
231
- uv run poe security # bandit scan (same config the Bandit workflow uses)
232
- ```
205
+ See `[tool.poe.tasks]` in `pyproject.toml` for the full task list (`uv run poe <task>`).
233
206
 
234
207
  Pre-push hook (`.githooks/pre-push`) runs: ruff check → ruff format --check → mypy. Tests are not in the hook — run them manually.
235
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
+
236
215
  ## Testing notes
237
216
 
238
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.2
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 export — `asher --export 7` writes activity history to CSV from cron / Task Scheduler / SSH without launching the TUI
78
+ - Headless modeevery 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 export (cron / Task Scheduler / SSH)
181
+ ### Headless commands (cron / Task Scheduler / SSH)
181
182
 
182
- `--export` writes the same CSV as the `export` command **without launching the TUI** — so you can script activity-history exports from cron, Windows Task Scheduler, or a server over SSH. No flags launches the interactive dashboard as before.
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 --export 7 export last 7 days to ~/Downloads
186
- asher --export 7 --output ~/hist.csv explicit output path
187
- asher --export month --robot "Asher 2" 30 days (Whisker ceiling) for a specific robot
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
- `--robot` accepts an index or a partial, case-insensitive name (defaults to your saved preferred robot, else the first). 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.
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` | export succeeded |
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 --export 7 --output /home/me/litter-history.csv >> /var/log/asher-export.log 2>&1
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 --export 7 --output C:\Users\me\litter-history.csv
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. regenerate CHANGELOG.md from conventional commits (idempotent), then commit it
231
- uv run poe changelog
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): update for next release"
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 auto-generated by git-cliff from the same cliff.toml)
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`**
@@ -24,7 +24,8 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
24
24
  - 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`
25
25
  - 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
26
  - Inline ghost-text completion for bare commands — type a prefix (`cle`) and the rest (`an`) appears greyed; `Tab` or `→` to accept → `clean`
27
- - Headless export — `asher --export 7` writes activity history to CSV from cron / Task Scheduler / SSH without launching the TUI
27
+ - Headless modeevery 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
28
+ - Catppuccin Mocha throughout — one palette in `asher/theme.py` drives the TUI stylesheet and every Rich style
28
29
  - Cat animation panel that reacts to robot state
29
30
  - Command history (↑/↓ arrows)
30
31
  - Real-time updates via WebSocket; 5-minute poll fallback
@@ -126,38 +127,61 @@ LITTER_ROBOT_PASSWORD=yourpassword
126
127
 
127
128
  **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.
128
129
 
129
- ### Headless export (cron / Task Scheduler / SSH)
130
+ ### Headless commands (cron / Task Scheduler / SSH)
130
131
 
131
- `--export` writes the same CSV as the `export` command **without launching the TUI** — so you can script activity-history exports from cron, Windows Task Scheduler, or a server over SSH. No flags launches the interactive dashboard as before.
132
+ 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.
132
133
 
133
134
  ```bash
134
- asher --export 7 export last 7 days to ~/Downloads
135
- asher --export 7 --output ~/hist.csv explicit output path
136
- asher --export month --robot "Asher 2" 30 days (Whisker ceiling) for a specific robot
135
+ asher status at-a-glance state
136
+ asher info model, serial, firmware, settings
137
+ asher clean start a clean cycle
138
+ asher night-light auto set night light mode
139
+ asher wait-time 7 set the clean-cycle wait time
140
+ asher history 20 recent activity, newest first
141
+ asher insight 7 cycle-usage statistics
142
+ asher export 7 --output ~/hist.csv activity history to CSV
137
143
  ```
138
144
 
139
- `--robot` accepts an index or a partial, case-insensitive name (defaults to your saved preferred robot, else the first). 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.
145
+ `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`.
146
+
147
+ Two flags apply to all of them:
148
+
149
+ - `--robot <index|name>` — an index or a partial, case-insensitive name (defaults to your saved preferred robot, else the first)
150
+ - `--json` — machine-readable output instead of aligned text, for piping into `jq`
151
+
152
+ ```bash
153
+ asher status --json | jq -r '.drawer'
154
+ asher history 50 --json | jq '[.events[] | select(.event | startswith("Cat"))] | length'
155
+ ```
156
+
157
+ 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.
140
158
 
141
159
  Exit codes for scripting:
142
160
 
143
161
  | Code | Meaning |
144
162
  |---|---|
145
- | `0` | export succeeded |
163
+ | `0` | command succeeded |
146
164
  | `1` | no credentials found (keyring or `.env`) |
147
165
  | `2` | connection or API failure |
148
166
  | `3` | failed to write the CSV (permissions, disk full) |
149
167
  | `4` | `--robot` matched no robot on the account |
168
+ | `5` | bad argument, or the robot/cloud rejected the command |
150
169
 
151
170
  ```bash
152
171
  # crontab — nightly export at 03:00
153
- 0 3 * * * /usr/bin/env asher --export 7 --output /home/me/litter-history.csv >> /var/log/asher-export.log 2>&1
172
+ 0 3 * * * /usr/bin/env asher export 7 --output /home/me/litter-history.csv >> /var/log/asher-export.log 2>&1
173
+
174
+ # alert when the drawer passes 85%
175
+ 0 * * * * [ "$(asher status --json | jq -r '.drawer' | tr -d %)" -gt 85 ] && notify-send "Litter drawer full"
154
176
  ```
155
177
 
156
178
  ```powershell
157
179
  # Windows Task Scheduler action
158
- asher.exe --export 7 --output C:\Users\me\litter-history.csv
180
+ asher.exe export 7 --output C:\Users\me\litter-history.csv
159
181
  ```
160
182
 
183
+ `asher --export [days]` remains as a deprecated alias for `asher export [days]`, so existing cron entries keep working.
184
+
161
185
  ## Configuration
162
186
 
163
187
  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:
@@ -176,10 +200,11 @@ Credentials and the preferred-robot serial stay in the OS keyring; `.env` vars s
176
200
  ## Releasing
177
201
 
178
202
  ```bash
179
- # 1. regenerate CHANGELOG.md from conventional commits (idempotent), then commit it
180
- uv run poe changelog
203
+ # 1. file the pending commits under the version you're about to cut, refine the
204
+ # prose if a squashed commit needs unpacking, then commit it
205
+ uv run poe changelog-release X.Y.Z
181
206
  git add CHANGELOG.md
182
- git commit -m "docs(changelog): update for next release"
207
+ git commit -m "docs(changelog): cut vX.Y.Z"
183
208
 
184
209
  # 2. bump version, commit, and tag in one step, then push with tags
185
210
  uv run bump-my-version bump patch # 0.2.0 → 0.2.1
@@ -189,7 +214,7 @@ uv run bump-my-version bump major # 0.2.0 → 1.0.0
189
214
  git push && git push --tags
190
215
 
191
216
  # 3. push the release branch to trigger PyPI publish (OIDC) + GitHub Release
192
- # (release notes auto-generated by git-cliff from the same cliff.toml)
217
+ # (release notes = the `## [X.Y.Z]` section of CHANGELOG.md, verbatim)
193
218
  git checkout -b release/X.Y.Z
194
219
  git push origin release/X.Y.Z
195
220
  ```
@@ -199,6 +224,17 @@ conventional commits — see [`cliff.toml`](cliff.toml). Commit messages must us
199
224
  `feat:`/`fix:`/`docs:`/etc. prefixes (optionally scoped, e.g. `feat(robot):`)
200
225
  to appear in the changelog.
201
226
 
227
+ Use `poe changelog-release X.Y.Z` — not plain `poe changelog` — when cutting a
228
+ release. Both regenerate the whole file, but only the former files the pending
229
+ commits under `## [X.Y.Z]` instead of `## [Unreleased]`, and the release
230
+ workflow extracts exactly that section for the GitHub Release body. It fails
231
+ loudly if the section is missing, so the notes can never come out stale.
232
+
233
+ Because the workflow reads the committed file rather than re-deriving from
234
+ commits, any hand-refinement you make to a section survives into the release
235
+ notes — but re-running either task afterwards regenerates from commits and
236
+ discards it. Refine last, right before committing.
237
+
202
238
  ## Troubleshooting
203
239
 
204
240
  **`asher` not found after `pip install asher-cli`**