asher-cli 1.1.0__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. {asher_cli-1.1.0 → asher_cli-1.3.0}/.github/workflows/bandit.yml +2 -2
  2. {asher_cli-1.1.0 → asher_cli-1.3.0}/.github/workflows/ci.yml +10 -4
  3. {asher_cli-1.1.0 → asher_cli-1.3.0}/.github/workflows/claude-code-review.yml +1 -1
  4. {asher_cli-1.1.0 → asher_cli-1.3.0}/.github/workflows/claude.yml +1 -1
  5. {asher_cli-1.1.0 → asher_cli-1.3.0}/.github/workflows/coverage.yml +7 -2
  6. {asher_cli-1.1.0 → asher_cli-1.3.0}/.github/workflows/release.yml +2 -2
  7. {asher_cli-1.1.0 → asher_cli-1.3.0}/CHANGELOG.md +55 -2
  8. {asher_cli-1.1.0 → asher_cli-1.3.0}/CLAUDE.md +45 -8
  9. {asher_cli-1.1.0 → asher_cli-1.3.0}/PKG-INFO +101 -18
  10. {asher_cli-1.1.0 → asher_cli-1.3.0}/README.md +92 -14
  11. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/__main__.py +81 -1
  12. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/activity_labels.py +7 -0
  13. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/app.py +1 -0
  14. asher_cli-1.3.0/asher/autostart.py +370 -0
  15. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/commands/__init__.py +163 -46
  16. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/completion.py +3 -1
  17. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/config.py +10 -0
  18. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/connection/__init__.py +44 -12
  19. asher_cli-1.3.0/asher/constants.py +61 -0
  20. asher_cli-1.3.0/asher/daemon.py +429 -0
  21. asher_cli-1.3.0/asher/desktoptheme.py +192 -0
  22. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/headless.py +78 -24
  23. asher_cli-1.3.0/asher/helpers.py +182 -0
  24. asher_cli-1.3.0/asher/launcher.py +133 -0
  25. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/monitoring/__init__.py +19 -4
  26. asher_cli-1.3.0/asher/notifications.py +120 -0
  27. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/robot_adapters.py +149 -6
  28. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/slash-commands/__init__.py +2 -0
  29. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/theme.py +6 -0
  30. asher_cli-1.3.0/asher/tray.py +264 -0
  31. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/ui/__init__.py +18 -3
  32. asher_cli-1.3.0/asher/updates.py +181 -0
  33. asher_cli-1.3.0/asher/watcher.py +393 -0
  34. {asher_cli-1.1.0 → asher_cli-1.3.0}/docs/ROADMAP.md +51 -29
  35. {asher_cli-1.1.0 → asher_cli-1.3.0}/pyproject.toml +31 -4
  36. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_activity_labels.py +21 -0
  37. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_app_pilot.py +3 -0
  38. asher_cli-1.3.0/tests/test_autostart.py +341 -0
  39. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_commands_pilot.py +3 -0
  40. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_config.py +4 -4
  41. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_connection.py +77 -0
  42. asher_cli-1.3.0/tests/test_daemon.py +445 -0
  43. asher_cli-1.3.0/tests/test_desktoptheme.py +228 -0
  44. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_headless.py +169 -0
  45. asher_cli-1.3.0/tests/test_launcher.py +203 -0
  46. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_lr5_commands.py +110 -0
  47. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_mcp_command.py +14 -0
  48. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_missing_robot_commands.py +139 -0
  49. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_monitoring.py +69 -0
  50. asher_cli-1.3.0/tests/test_notifications.py +176 -0
  51. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_robot_adapters.py +192 -0
  52. asher_cli-1.3.0/tests/test_tray.py +305 -0
  53. asher_cli-1.3.0/tests/test_updates.py +272 -0
  54. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_version_command.py +12 -0
  55. asher_cli-1.3.0/tests/test_watcher.py +379 -0
  56. asher_cli-1.3.0/tests/testhelpers.py +245 -0
  57. {asher_cli-1.1.0 → asher_cli-1.3.0}/uv.lock +1137 -692
  58. asher_cli-1.1.0/asher/constants.py +0 -24
  59. asher_cli-1.1.0/asher/helpers.py +0 -46
  60. asher_cli-1.1.0/asher/notifications.py +0 -48
  61. asher_cli-1.1.0/tests/test_notifications.py +0 -82
  62. asher_cli-1.1.0/tests/testhelpers.py +0 -93
  63. {asher_cli-1.1.0 → asher_cli-1.3.0}/.claude/hooks/block-env.py +0 -0
  64. {asher_cli-1.1.0 → asher_cli-1.3.0}/.claude/settings.json +0 -0
  65. {asher_cli-1.1.0 → asher_cli-1.3.0}/.claude/skills/mcp-bridge/SKILL.md +0 -0
  66. {asher_cli-1.1.0 → asher_cli-1.3.0}/.claude/skills/pylitterbot-ref/SKILL.md +0 -0
  67. {asher_cli-1.1.0 → asher_cli-1.3.0}/.claude/skills/textual/SKILL.md +0 -0
  68. {asher_cli-1.1.0 → asher_cli-1.3.0}/.env.example +0 -0
  69. {asher_cli-1.1.0 → asher_cli-1.3.0}/.githooks/pre-push +0 -0
  70. {asher_cli-1.1.0 → asher_cli-1.3.0}/.github/FUNDING.yml +0 -0
  71. {asher_cli-1.1.0 → asher_cli-1.3.0}/.github/pull_request_template.md +0 -0
  72. {asher_cli-1.1.0 → asher_cli-1.3.0}/.gitignore +0 -0
  73. {asher_cli-1.1.0 → asher_cli-1.3.0}/.vscode/launch.json +0 -0
  74. {asher_cli-1.1.0 → asher_cli-1.3.0}/.vscode/settings.json +0 -0
  75. {asher_cli-1.1.0 → asher_cli-1.3.0}/.vscode/tasks.json +0 -0
  76. {asher_cli-1.1.0 → asher_cli-1.3.0}/CODEOWNERS +0 -0
  77. {asher_cli-1.1.0 → asher_cli-1.3.0}/LICENSE +0 -0
  78. {asher_cli-1.1.0 → asher_cli-1.3.0}/app.py +0 -0
  79. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/__init__.py +0 -0
  80. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/auth.py +0 -0
  81. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/cats.py +0 -0
  82. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/commands/base.py +0 -0
  83. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/export.py +0 -0
  84. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/faults.py +0 -0
  85. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/history_view.py +0 -0
  86. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/login_flow.py +0 -0
  87. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/mcp_bridge.py +0 -0
  88. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/mcp_config.py +0 -0
  89. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/robot_protocol.py +0 -0
  90. {asher_cli-1.1.0 → asher_cli-1.3.0}/asher/ui/style.tcss +0 -0
  91. {asher_cli-1.1.0 → asher_cli-1.3.0}/cliff.toml +0 -0
  92. {asher_cli-1.1.0 → asher_cli-1.3.0}/docs/roadmap-archive/README.md +0 -0
  93. {asher_cli-1.1.0 → asher_cli-1.3.0}/docs/roadmap-archive/cat-panel-badges.md +0 -0
  94. {asher_cli-1.1.0 → asher_cli-1.3.0}/docs/roadmap-archive/config-persistence.md +0 -0
  95. {asher_cli-1.1.0 → asher_cli-1.3.0}/docs/roadmap-archive/desktop-notifications.md +0 -0
  96. {asher_cli-1.1.0 → asher_cli-1.3.0}/docs/roadmap-archive/fault-monitoring.md +0 -0
  97. {asher_cli-1.1.0 → asher_cli-1.3.0}/docs/roadmap-archive/headless-export.md +0 -0
  98. {asher_cli-1.1.0 → asher_cli-1.3.0}/docs/roadmap-archive/history-export.md +0 -0
  99. {asher_cli-1.1.0 → asher_cli-1.3.0}/docs/roadmap-archive/tab-completion.md +0 -0
  100. {asher_cli-1.1.0 → asher_cli-1.3.0}/renovate.json +0 -0
  101. {asher_cli-1.1.0 → asher_cli-1.3.0}/requirements.txt +0 -0
  102. {asher_cli-1.1.0 → asher_cli-1.3.0}/test.py +0 -0
  103. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/__init__.py +0 -0
  104. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/conftest.py +0 -0
  105. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_auth.py +0 -0
  106. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_auth_pilot.py +0 -0
  107. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_cats.py +0 -0
  108. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_completion.py +0 -0
  109. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_connection_mixin.py +0 -0
  110. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_export.py +0 -0
  111. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_faults.py +0 -0
  112. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_history_view.py +0 -0
  113. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_mcp_bridge.py +0 -0
  114. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_mcp_config.py +0 -0
  115. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_new_commands_pilot.py +0 -0
  116. {asher_cli-1.1.0 → asher_cli-1.3.0}/tests/test_ui.py +0 -0
  117. {asher_cli-1.1.0 → asher_cli-1.3.0}/watchrun.py +0 -0
@@ -31,7 +31,7 @@ jobs:
31
31
  runs-on: ubuntu-latest
32
32
  steps:
33
33
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
34
- - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
34
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
35
35
 
36
36
  - name: Bandit scan
37
37
  # --exit-zero: findings must not fail the job, or the upload step below
@@ -41,7 +41,7 @@ jobs:
41
41
  -c pyproject.toml -r . -f sarif -o bandit.sarif --exit-zero
42
42
 
43
43
  - name: Upload SARIF to code scanning
44
- uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
44
+ uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
45
45
  with:
46
46
  sarif_file: bandit.sarif
47
47
  category: bandit
@@ -8,13 +8,19 @@ on:
8
8
  permissions:
9
9
  contents: read
10
10
 
11
+ # `uv run` re-syncs to the default groups on every invocation, which would drag
12
+ # the `tray` group (and so a PyGObject/pycairo source build) back in after the
13
+ # explicit sync below deliberately left it out. Sync once, then never re-sync.
14
+ env:
15
+ UV_NO_SYNC: "1"
16
+
11
17
  jobs:
12
18
  lint:
13
19
  runs-on: ubuntu-latest
14
20
  steps:
15
21
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
16
- - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
17
- - run: uv sync --dev
22
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
23
+ - run: uv sync --no-default-groups --group dev
18
24
  - run: uv run ruff check .
19
25
  - run: uv run ruff format --check .
20
26
  - run: uv run mypy asher/ --ignore-missing-imports
@@ -29,6 +35,6 @@ jobs:
29
35
  runs-on: ${{ matrix.os }}
30
36
  steps:
31
37
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
32
- - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
33
- - run: uv sync --dev
38
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
39
+ - run: uv sync --no-default-groups --group dev
34
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@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
36
+ uses: anthropics/claude-code-action@9d7150bc8a3dae8149739a88019d192b579ad90c # 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@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
35
+ uses: anthropics/claude-code-action@9d7150bc8a3dae8149739a88019d192b579ad90c # v1
36
36
  with:
37
37
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38
38
 
@@ -8,13 +8,18 @@ on:
8
8
  permissions:
9
9
  contents: read
10
10
 
11
+ # See ci.yml: `uv run` would otherwise re-sync the `tray` group back in and try
12
+ # to build PyGObject from source.
13
+ env:
14
+ UV_NO_SYNC: "1"
15
+
11
16
  jobs:
12
17
  coverage:
13
18
  runs-on: ubuntu-latest
14
19
  steps:
15
20
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
16
- - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
17
- - run: uv sync --dev
21
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
22
+ - run: uv sync --no-default-groups --group dev
18
23
  - name: Run tests with coverage
19
24
  run: uv run pytest tests/ --cov=asher --cov-report=lcov --cov-report=term-missing
20
25
  - name: Upload to Coveralls
@@ -20,7 +20,7 @@ jobs:
20
20
  - name: Extract version from branch name
21
21
  id: version
22
22
  run: echo "version=${GITHUB_REF_NAME#release/}" >> "$GITHUB_OUTPUT"
23
- - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
23
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
24
24
  - run: uv build
25
25
  - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
26
26
  with:
@@ -37,7 +37,7 @@ jobs:
37
37
  with:
38
38
  name: dist
39
39
  path: dist/
40
- - uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # release/v1
40
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
41
41
 
42
42
  github-release:
43
43
  name: Create GitHub Release
@@ -7,12 +7,65 @@ 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.3.0] - 2026-08-23
11
+
12
+ ### 🚀 Features
13
+
14
+ - *(commands)* Close the remaining robot-command gaps in the roadmap
15
+ - *(monitoring)* Shorten cat-panel status labels that overflow the panel width
16
+
17
+ ### 🐛 Bug Fixes
18
+
19
+ - *(monitoring)* Render readable status text in cat panel, not raw cloud code
20
+
21
+ ### 🧪 Tests
22
+
23
+ - *(version)* Keep the /version pilot tests off the network
24
+
25
+ ## [1.2.0] - 2026-08-16
26
+
27
+ ### 🚀 Features
28
+
29
+ - Drop "Clean now" from the tray menu
30
+ - Refuse to start a watcher that has no credentials
31
+ - Open the dashboard from the tray
32
+ - Tone the tray icon to the desktop panel, and stop repeating the drawer
33
+ - [**breaking**] Take credentials only from the keyring outside dev mode
34
+ - Start the watcher at login, and notify (never install) on new releases
35
+ - Background watcher and system-tray icon for notifications
36
+
37
+ ### 🐛 Bug Fixes
38
+
39
+ - Keep the tray installed across a plain `uv run`
40
+ - Read the selected completion row against its own background
41
+ - *(deps)* Update dependency pylitterbot to v2025.6.4
42
+
43
+ ### 📚 Documentation
44
+
45
+ - Trim the README, move the packaging detail to CLAUDE.md
46
+ - Cover the tray's new behaviour and keyring-only credentials
47
+
48
+ ### 🧪 Tests
49
+
50
+ - Stop four tests reading the developer's own machine
51
+ - Cover the notifier fallback on every platform
52
+ - Stop the notification tests toasting the desktop
53
+ - Compare the Windows Run command against its normalised path
54
+ - Pin platform branches in the daemon tests
55
+
56
+ ### 👷 CI/CD
57
+
58
+ - Stop `uv run` re-syncing the tray group back in
59
+
10
60
  ## [1.1.0] - 2026-08-09
11
61
 
12
62
  ### 🚀 Features
13
63
 
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
64
+ - Headless command surface + Catppuccin Mocha palette
65
+
66
+ ### 🐛 Bug Fixes
67
+
68
+ - *(release)* Generate notes against the release tag, not the previous one
16
69
 
17
70
  ### 📚 Documentation
18
71
 
@@ -23,11 +23,18 @@ 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
- notifications.py desktop toast + audible alert façade over plyer (fire/beep, always-safe no-op on failure/headless)
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
31
+ watcher.py background notification loop with no TUI — pure WatchState (robot snapshots → Alert list) + supervising watch() that reconnects with backoff, plus WatcherRunner (asyncio on a worker thread, for the tray)
32
+ daemon.py detached watcher process control — `start` pre-flights `connection.credentials_available()` (presence only, no network, so starting offline still works) because the watcher claims the pid file *before* authenticating and would otherwise report a pid for a process that stops a moment later — pid file/log in ~/.asher-cli, start/stop/status/run, cross-platform detach + liveness (os.kill would *terminate* on Windows)
33
+ tray.py optional pystray/Pillow system-tray icon over WatcherRunner; every path degrades to a headless watcher. Icon is a panel-toned silhouette + status dot — colour rides the badge, never the whole glyph
34
+ launcher.py open_app() — start the TUI in a new terminal from the tray (a detached tray has none): new console on Windows, Terminal.app via AppleScript on macOS, first installed emulator on Linux (desktop's own preferred)
35
+ desktoptheme.py panel_is_dark() — is the tray/menu-bar background dark? kdeglobals luma / gsettings / AppleInterfaceStyle / the Personalize registry keys, behind a TTL cache; every probe degrades to a fallback, never raises
36
+ autostart.py login items — AutostartBackend ABC + launchd/systemd-user/registry subclasses + backend() factory; all per-user, no admin rights, disable() removes exactly what enable() wrote
37
+ updates.py PyPI release check — read-only over HTTPS, once a day, reports only. Never installs (see its docstring for why that stays manual)
31
38
  cats.py CATS dict (ASCII art)
32
39
  login_flow.py LoginFlow state machine — inline email/password prompt in command bar
33
40
  robot_protocol.py RobotProtocol structural Protocol for pylitterbot robot objects
@@ -62,6 +69,13 @@ tests/
62
69
  test_mcp_config.py Claude Desktop config read/write
63
70
  test_config.py runtime settings persistence — load/save/update over defaults
64
71
  test_notifications.py plyer toast + beep façade — always-safe no-op paths
72
+ test_watcher.py WatchState alert transitions + snapshot/deliver (pure) + watch() against a patched open_session + WatcherRunner threading
73
+ test_daemon.py pid-file bookkeeping, start/stop/status/dispatch with Popen and os.kill mocked
74
+ test_tray.py availability probing, menu/title text, icon rendering (tone inversion + status badge), and the headless fallbacks
75
+ test_launcher.py every platform's open path driven directly (Popen always mocked), terminal preference/fallback order
76
+ test_desktoptheme.py all four panel-tone probes driven directly (so each is covered on every runner) with the readers/registry mocked, plus the TTL cache
77
+ test_autostart.py all three login-item backends driven directly (so each is covered on every runner) with launchctl/systemctl/winreg mocked
78
+ test_updates.py version comparison, the once-a-day cache, HTTPS-only fetching, and a guard that the module spawns no process
65
79
  test_mcp_bridge.py mcp_bridge launcher credential/subprocess handling
66
80
  test_mcp_command.py /mcp slash command dispatch
67
81
  test_faults.py check_faults() — safety statuses, attribute faults, graceful degradation
@@ -79,16 +93,27 @@ tests/
79
93
 
80
94
  Priority order on startup:
81
95
 
82
- 1. **OS keyring** — set automatically after first interactive login
83
- 2. **`.env` file** — fallback for existing users / CI
96
+ 1. **OS keyring** — cached OAuth token, then email/password; set automatically after first interactive login
97
+ 2. **`.env` file** — **development only**, gated on `ASHER_CLI_DEV_MODE=true`
84
98
  3. **Inline login flow** — shown when no credentials found anywhere (email → password prompt in command bar)
85
99
 
86
- `.env` variable names (for fallback):
100
+ In a real install the keyring is the **only** source of credentials. `.env` is a
101
+ working-copy convenience: without dev mode a stray `LITTER_ROBOT_USER` in a shell
102
+ profile or a checked-out `.env` would silently outrank the keyring and
103
+ authenticate as the wrong account.
104
+
105
+ `.env` variable names (dev mode only):
87
106
  ```
107
+ ASHER_CLI_DEV_MODE=true
88
108
  LITTER_ROBOT_USER=...
89
109
  LITTER_ROBOT_PASSWORD=...
90
110
  ```
91
111
 
112
+ Every environment read goes through `_env_credentials()` in
113
+ `asher/connection/__init__.py` — the one place the gate is applied. Don't call
114
+ `os.getenv("LITTER_ROBOT_*")` anywhere else. `helpers.dev_mode()` is the shared
115
+ predicate (it also selects the `dev` version string).
116
+
92
117
  Keyring service name: `asher-cli`, keys `email` and `password`.
93
118
  Helper functions in `asher/connection/__init__.py`: `_keyring_load()`, `_keyring_save()`, `_keyring_delete()`.
94
119
 
@@ -98,14 +123,14 @@ Command names, slash-command names, and their args are not listed here — see t
98
123
 
99
124
  **Normal commands** (no prefix) are robot actions only; **slash commands** (`/` prefix) are app management only.
100
125
 
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.
126
+ `/refresh`, `/cat`, `/pet`, and `/notify` persist their settings to `~/.asher-cli/config.json` (via `asher.config.update()`), so they survive restarts. That file is also the only channel to a running watcher, which re-reads it per alert rather than caching at startup. Credentials and the preferred-robot serial stay in the OS keyring; the config file holds only non-secret UI preferences.
102
127
 
103
128
  Do not add robot-control commands as slash commands, and do not add app-management commands as bare commands.
104
129
 
105
130
  **Special cases** (accepted both with and without `/`):
106
131
  `exit`, `quit`, `q` — exit the app
107
132
 
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.
133
+ **Headless commands** (`asher <command>`) are a parallel registry in `asher/headless.py`: same robot actions, no Textual, plain-string + JSON output. `asher watch` and `asher update` are deliberately *not* in that registry — `watch` manages a long-lived process rather than doing one thing and exiting, and `update` talks to PyPI rather than a robot, so neither should be forced through `open_session()` and made to authenticate. Both declare their subparser directly in `__main__.py`. 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
134
 
110
135
  > 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.
111
136
 
@@ -144,6 +169,10 @@ LoginScreen (ModalScreen) — available in auth.py but not the primary auth path
144
169
  | `_refresh_faults(robot)` | run `check_faults()`, render `#fault-banner`, log transitions; sets cat mode to `error` while faults active |
145
170
  | `_cycling_chip()` / `_start_cycle_timer()` / `_stop_cycle_timer()` / `_tick_cycle()` | `⟳ Cycling M:SS` chip + lazy 1s elapsed timer |
146
171
  | `_poll_status_interval()` | `@work` — poll fallback every 300s (5 min); WebSocket is primary |
172
+ | `watcher.watch()` | supervise a cloud session and notify on every change worth interrupting for — the TUI's `_notify_fault` without a TUI |
173
+ | `daemon.start/stop/status()` | detached watcher process control; `running_pid()` also gates the TUI's own toasts so alerts never double up |
174
+ | `daemon.run_foreground()` | the watcher process itself — claims the pid file (`_pid_file_held`) so a login-started watcher is still visible to `status`/`stop`, and refuses to be a second one |
175
+ | `autostart.enable/disable()` | register/remove the platform login item; `describe()` feeds the `watch status` line |
147
176
  | `_tick_cat()` | advances multi-frame cat animation every 0.4s |
148
177
  | `_dispatch_command(command, args)` | `@work` — calls `command.run(app, args)` from the registry |
149
178
  | `on_input_submitted()` | routes input to login flow or `_dispatch_command` via `CommandRegistry`; Enter on a partial `/cmd` completes it instead of submitting |
@@ -200,6 +229,14 @@ Commands that need a confirmed cloud state before showing a result (e.g. sleep/w
200
229
 
201
230
  **File naming convention:** no underscores in filenames (except Python-required `__init__.py` and `__main__.py`).
202
231
 
232
+ ## Tray packaging
233
+
234
+ The `tray` extra is what a published install uses (`pip install "asher-cli[tray]"`). It is **mirrored** as a `tray` dependency-group listed in `[tool.uv] default-groups`, because `uv run` re-syncs the environment to the default *groups* on every invocation and prunes everything else — extras included. Without the group, any plain `uv run` silently removed `pystray` and left the watcher drawing an icon no desktop was hosting. Keep the two lists in step when changing either.
235
+
236
+ CI opts out with `uv sync --no-default-groups --group dev` (`ci.yml`, `coverage.yml`): PyGObject builds from source and the runners have no GObject-introspection headers. `[tool.uv] default-extras` is not an option — uv 0.11 doesn't support that key.
237
+
238
+ PyGObject is what selects pystray's `_appindicator` backend. Without it pystray falls back to a legacy XEmbed icon that KDE Plasma and GNOME no longer host: the watcher runs, draws an icon, and nothing shows it, with no error anywhere. Check with `pystray.Icon.__module__`.
239
+
203
240
  ## Dev workflow
204
241
 
205
242
  See `[tool.poe.tasks]` in `pyproject.toml` for the full task list (`uv run poe <task>`).
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: asher-cli
3
- Version: 1.1.0
3
+ Version: 1.3.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,13 +40,18 @@ 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.2
43
+ Requires-Dist: pylitterbot==2025.6.4
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
- Requires-Dist: pylitterbot[mcp]==2025.6.2; extra == 'mcp'
49
+ Requires-Dist: mcp<2; extra == 'mcp'
50
+ Requires-Dist: pylitterbot[mcp]==2025.6.4; extra == 'mcp'
51
+ Provides-Extra: tray
52
+ Requires-Dist: pillow>=10.0; extra == 'tray'
53
+ Requires-Dist: pygobject>=3.42; (sys_platform == 'linux') and extra == 'tray'
54
+ Requires-Dist: pystray>=0.19; extra == 'tray'
50
55
  Description-Content-Type: text/markdown
51
56
 
52
57
  # Asher CLI
@@ -70,12 +75,14 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
70
75
  - Real-time cycling indicator with elapsed time (`⟳ Cycling M:SS`)
71
76
  - Fault & safety monitoring — model-scoped in-panel alerts for cat detected, pinch, motor/position/gas faults (LR5: bonnet/laser/drawer); press `d` to dismiss
72
77
  - Cat panel with mode label + status badges (status chip, lock, night light, sleep, wait time) under the art
73
- - 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
74
- - Commands: `clean`, `status`, `info`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto`, `night-light-brightness`, `panel-brightness <low|medium|high>` (LR4/LR5), `wait-time`, `power on|off`, `rename`, `insight`, `sleep-schedule`, plus LR5 extras (`privacy`, `volume`, `camera-audio`, `drawer-reset`), `history [count|all]`, `export [days|month]`, `help`, `quit`
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`
78
+ - 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
79
+ - 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
+ - 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`
76
81
  - 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
82
  - Inline ghost-text completion for bare commands — type a prefix (`cle`) and the rest (`an`) appears greyed; `Tab` or `→` to accept → `clean`
78
83
  - 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
84
+ - Background watcher — `asher watch start` detaches a notifier process that keeps toasting faults, a filling drawer, and offline/online changes **after you close the terminal**; `asher watch enable` starts it at login (launchd / systemd / registry); optional system-tray icon shows live status with Open Asher / Notifications / Quit
85
+ - Update notices — checks PyPI once a day and tells you when a newer release exists, with the right upgrade command for how you installed it. It never installs anything itself
79
86
  - Catppuccin Mocha throughout — one palette in `asher/theme.py` drives the TUI stylesheet and every Rich style
80
87
  - Cat animation panel that reacts to robot state
81
88
  - Command history (↑/↓ arrows)
@@ -121,12 +128,7 @@ On first run, type `/login` at the command prompt. Your credentials are saved to
121
128
 
122
129
  To sign out: `/logout`
123
130
 
124
- `.env` fallback (for CI or existing users):
125
-
126
- ```env
127
- LITTER_ROBOT_USER=your@email.com
128
- LITTER_ROBOT_PASSWORD=yourpassword
129
- ```
131
+ The keyring is the only place an installed copy reads credentials from. Working from a clone, you can use `.env` instead by opting into dev mode (see [Development](#development)).
130
132
 
131
133
  ## Commands
132
134
 
@@ -140,18 +142,21 @@ LITTER_ROBOT_PASSWORD=yourpassword
140
142
  | `lock` / `unlock` | Toggle panel lockout |
141
143
  | `sleep` / `wake` | Toggle sleep mode |
142
144
  | `night-light on\|off\|auto` | Set night light mode |
145
+ | `night-light color <hex>` | Set the LR5 night light colour (`#RRGGBB`, or the 3-digit shorthand) |
143
146
  | `night-light-brightness <level>` | Set brightness (LR5: 0-100; LR4: 25/50/100) |
144
147
  | `panel-brightness <low\|medium\|high>` | Set control-panel brightness (LR4/LR5 only; shows current if omitted) |
145
148
  | `wait-time <minutes>` | Set clean-cycle wait time (shows valid values / current if omitted) |
146
149
  | `power on\|off` | Hard-power the unit on or off |
147
150
  | `rename <new name>` | Rename the unit in the Whisker cloud |
148
151
  | `insight [days\|month]` | Show cycle-usage statistics (default: 30 days) |
149
- | `sleep-schedule` | Show the per-day sleep schedule (read-only) |
152
+ | `sleep-schedule` | Show the per-day sleep schedule |
153
+ | `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) |
154
+ | `sleep-schedule disable` | Turn every scheduled sleep window off |
150
155
  | `privacy on\|off` | Toggle LR5 privacy mode |
151
156
  | `volume <0-100>` | Set LR5 sound volume |
152
157
  | `camera-audio on\|off` | Toggle LR5 camera audio |
153
158
  | `drawer-reset` | Reset the LR5 waste drawer level indicator |
154
- | `history [count\|all]` | Show recent activity in a scrollable pager (default: 50 events) |
159
+ | `history [count\|all] [--type <kind>]` | Show recent activity in a scrollable pager (default: 50 events); `--type cat\|clean\|litter\|offline\|…` filters on the LR5 |
155
160
  | `export [days\|month]` | Export activity history to CSV in `~/Downloads` (default: 30 days) |
156
161
  | `clear` | Clear the log |
157
162
  | `help` | Show command list |
@@ -170,8 +175,10 @@ LITTER_ROBOT_PASSWORD=yourpassword
170
175
  | `/cat on\|off` | Show or hide the cat animation panel |
171
176
  | `/cat colour <hex>` | Change the cat art colour (e.g. `/cat colour #ff79c6`); `color` also accepted; `/cat reset` to revert |
172
177
  | `/refresh [seconds\|off]` | Change the auto-poll interval or disable it (`/refresh 60`, `/refresh off`) |
173
- | `/config` | Show current runtime settings (robot, refresh rate, cat panel, active pet, notifications) |
178
+ | `/config` | Show current runtime settings (robot, refresh rate, cat panel, active pet, notifications, watcher) |
174
179
  | `/notify on\|off\|sound on\|off\|test` | Toggle desktop toast notifications for fault events (cat detected, pinch, motor fault, drawer full); `test` fires a sample toast |
180
+ | `/watch start\|stop\|status` | Start, stop, or check the background watcher that keeps notifications running after you close the dashboard |
181
+ | `/watch enable\|disable` | Register (or remove) the watcher as a login item so it starts automatically |
175
182
  | `/version` | Show version info (asher-cli, Python, pylitterbot, textual) |
176
183
  | `/mcp on\|off\|status` | Toggle the Litter-Robot MCP server entry in Claude Desktop |
177
184
  | `/exit` | Exit Asher CLI |
@@ -189,6 +196,8 @@ asher clean start a clean cycle
189
196
  asher night-light auto set night light mode
190
197
  asher wait-time 7 set the clean-cycle wait time
191
198
  asher history 20 recent activity, newest first
199
+ asher history 20 --type cat only cat visits (LR5)
200
+ asher sleep-schedule set all 22:00 07:00 sleep 22:00 → 07:00 every day
192
201
  asher insight 7 cycle-usage statistics
193
202
  asher export 7 --output ~/hist.csv activity history to CSV
194
203
  ```
@@ -231,11 +240,80 @@ Exit codes for scripting:
231
240
  asher.exe export 7 --output C:\Users\me\litter-history.csv
232
241
  ```
233
242
 
243
+ ### Update notices
244
+
245
+ ```bash
246
+ asher update # is there a newer release?
247
+ asher update --json
248
+ ```
249
+
250
+ Asher checks PyPI's JSON API once a day (and on `/version`) and tells you when a newer release is published, along with the right command for however you installed it — `pipx upgrade`, `uv tool upgrade`, or `pip install -U`. Exit code is `0` when current, `10` when an update exists, so a script can branch on it.
251
+
252
+ **It never installs anything.** That's deliberate, not an omission:
253
+
254
+ - The process can't reliably tell how it was installed, so a self-upgrade would be guessing at the command that owns its own files.
255
+ - Replacing files under a live interpreter can load a mix of old and new modules, and the background watcher is exactly the long-lived process that would be running while its own code got swapped.
256
+ - Most importantly, auto-installing turns one compromised release into code running everywhere with nobody choosing the moment. Checking is safe; applying should be your call.
257
+
258
+ The check is read-only over HTTPS, fetches no code, and `update_check: false` in `~/.asher-cli/config.json` turns it off entirely.
259
+
234
260
  `asher --export [days]` remains as a deprecated alias for `asher export [days]`, so existing cron entries keep working.
235
261
 
262
+ ### Background notifications (`asher watch`)
263
+
264
+ The dashboard only notifies while it's on screen — close the terminal and the toasts stop. `asher watch` runs the same fault monitoring as a detached process, so notifications keep arriving with no terminal open at all.
265
+
266
+ ```bash
267
+ asher watch start # detach a watcher; the terminal is free to close
268
+ asher watch status # is one running? plus the last few log lines
269
+ asher watch stop # shut it down
270
+ asher watch enable # also start it at every login
271
+ asher watch disable # stop starting it at login
272
+ asher watch run # same thing in the foreground, for debugging
273
+ ```
274
+
275
+ You can also drive it from inside the dashboard with `/watch start|stop|status` — handy right before you quit. While a watcher is running the dashboard suppresses its own toasts, so you never get an alert twice.
276
+
277
+ It notifies on:
278
+
279
+ - **faults appearing and clearing** — cat detected, pinch, motor/position/gas faults, drawer full (same model-scoped detection as the dashboard's fault banner)
280
+ - **the waste drawer crossing 85%** — an early warning before the robot stops on its own (tune with `watch_drawer_threshold`)
281
+ - **the robot going offline or coming back**
282
+
283
+ Notifications respect your `/notify` settings, and re-read them on every alert — toggling notifications in the dashboard reaches a watcher that's already running. Faults already showing when the watcher starts are announced immediately; connectivity is only reported when it *changes*, so startup never claims a robot "came back" that was never away.
284
+
285
+ The watcher is built to be left alone: it reconnects with backoff through Wi-Fi drops, laptop sleeps, expired tokens, and a cloud that's still unreachable at login. Only genuinely missing credentials stop it — sign in with `/login` once first. Its log lives at `~/.asher-cli/watch.log` (`~` is your home directory; `%USERPROFILE%` on Windows).
286
+
287
+ **System-tray icon** (optional):
288
+
289
+ ```bash
290
+ pip install "asher-cli[tray]"
291
+ ```
292
+
293
+ `asher watch start` then shows a tray icon: a cat silhouette toned to your desktop panel, badged green healthy / red faulted / grey offline, with the full status in its tooltip. The menu offers **Open Asher** (also the left-click action, opens the dashboard in a new terminal), a **Notifications** toggle, and **Quit**. It carries no robot actions on purpose: a misclick next to the clock shouldn't be able to start a cycle. No extras, no desktop session, or no AppIndicator host all fall back to a headless watcher rather than failing. Pass `--no-tray` (or set `watch_tray: false`) to skip the icon.
294
+
295
+ On Linux the extra pulls in PyGObject, which builds from source and needs your distro's GObject-introspection and Cairo dev packages (`gobject-introspection-devel` + `cairo-gobject-devel` on Fedora, `libgirepository1.0-dev` + `libcairo2-dev` on Debian/Ubuntu).
296
+
297
+ **Start at login:**
298
+
299
+ ```bash
300
+ asher watch enable # register a login item, and start watching now
301
+ asher watch disable # remove it
302
+ ```
303
+
304
+ `enable` uses whatever already runs things at login on your machine, so there's no extra background service to trust:
305
+
306
+ | Platform | Login item | Where |
307
+ |---|---|---|
308
+ | macOS | launchd user agent | `~/Library/LaunchAgents/com.asher-cli.watcher.plist` |
309
+ | Linux | systemd user unit | `~/.config/systemd/user/asher-watch.service` |
310
+ | Windows | registry Run value | `HKCU\...\CurrentVersion\Run\AsherWatch` |
311
+
312
+ All three are per-user and need no administrator rights, and `disable` removes exactly what `enable` created. A crashed watcher is restarted; one you stopped with `asher watch stop` stays stopped. `asher watch status` shows whether autostart is registered. If your system has no user-level systemd (some minimal distros, WSL), `enable` says so and points you at putting `asher watch start` in your shell profile instead.
313
+
236
314
  ## Configuration
237
315
 
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:
316
+ 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 these non-secret UI preferences:
239
317
 
240
318
  | Setting | Slash command | Default |
241
319
  |---|---|---|
@@ -245,6 +323,9 @@ Runtime settings persist across restarts in `~/.asher-cli/config.json`, so you d
245
323
  | `active_pet_index` | `/pet <index\|name>` | `0` |
246
324
  | `notifications` | `/notify on\|off` | `true` |
247
325
  | `notification_sound` | `/notify sound on\|off` | `false` |
326
+ | `watch_tray` | *(file only)* | `true` — set `false` to make `asher watch` always run without a tray icon |
327
+ | `watch_drawer_threshold` | *(file only)* | `85` — drawer % that triggers an early background warning |
328
+ | `update_check` | *(file only)* | `true` — set `false` to stop the daily PyPI version check |
248
329
 
249
330
  Credentials and the preferred-robot serial stay in the OS keyring; `.env` vars stay as env vars. You generally don't need to edit the file by hand — just use the slash commands — but it's plain JSON and safe to inspect or delete (deleting it restores defaults).
250
331
 
@@ -335,11 +416,13 @@ cp .env.example .env
335
416
  ```
336
417
 
337
418
  ```env
419
+ ASHER_CLI_DEV_MODE=true # shows version as "dev", and opts the two variables below in
338
420
  LITTER_ROBOT_USER=your@email.com
339
421
  LITTER_ROBOT_PASSWORD=yourpassword
340
- ASHER_CLI_DEV_MODE=true # sets version to "dev" instead of the installed package version
341
422
  ```
342
423
 
424
+ `ASHER_CLI_DEV_MODE` is what makes the credentials below readable at all — outside dev mode the app takes credentials only from the OS keyring.
425
+
343
426
  ### 3. Run with hot reload
344
427
 
345
428
  **CSS hot reload** — Textual's devtools watch inline `CSS` strings and `.tcss` files and reload them in-place without restarting: