agent-toggle 0.12.0__tar.gz → 0.13.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 (94) hide show
  1. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/PKG-INFO +57 -34
  2. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/README.md +56 -33
  3. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/__init__.py +1 -1
  4. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/backends/mcp_json.py +1 -1
  5. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/backends/plugin_cli.py +4 -3
  6. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/cli.py +18 -2
  7. agent_toggle-0.13.0/agent_toggle/completions.py +210 -0
  8. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/doctor.py +5 -3
  9. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/fs.py +9 -0
  10. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/harnesses.py +13 -19
  11. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/help.py +11 -1
  12. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/locales/zh_tw_cli.py +4 -0
  13. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/mechanisms.py +283 -107
  14. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/ops.py +5 -4
  15. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/store.py +4 -3
  16. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/ui/config_menu.py +12 -6
  17. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle.egg-info/PKG-INFO +57 -34
  18. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle.egg-info/SOURCES.txt +1 -0
  19. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_alias.py +2 -2
  20. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_backends.py +29 -5
  21. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_cli_surface.py +9 -0
  22. agent_toggle-0.13.0/tests/test_completions.py +98 -0
  23. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_config.py +11 -0
  24. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_config_menu.py +32 -7
  25. agent_toggle-0.13.0/tests/test_crash.py +1058 -0
  26. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_harnesses.py +22 -0
  27. agent_toggle-0.13.0/tests/test_mechanisms.py +199 -0
  28. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_secret_audit.py +62 -0
  29. agent_toggle-0.12.0/tests/test_completions.py +0 -37
  30. agent_toggle-0.12.0/tests/test_crash.py +0 -382
  31. agent_toggle-0.12.0/tests/test_mechanisms.py +0 -79
  32. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/LICENSE +0 -0
  33. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/__main__.py +0 -0
  34. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/backends/__init__.py +0 -0
  35. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/backends/flag_json.py +0 -0
  36. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/backends/mcp_toml.py +0 -0
  37. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/companions.py +0 -0
  38. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/config.py +0 -0
  39. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/cost.py +0 -0
  40. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/i18n.py +0 -0
  41. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/locales/__init__.py +0 -0
  42. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/locales/zh_tw_common.py +0 -0
  43. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/locales/zh_tw_config.py +0 -0
  44. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/locales/zh_tw_picker.py +0 -0
  45. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/output.py +0 -0
  46. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/profiles.py +0 -0
  47. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/settings.py +0 -0
  48. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/shims/claude.md.tmpl +0 -0
  49. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/shims/generic.md.tmpl +0 -0
  50. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/spinner.py +0 -0
  51. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/toml_check.py +0 -0
  52. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/ui/__init__.py +0 -0
  53. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/ui/logo.py +0 -0
  54. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/ui/menu.py +0 -0
  55. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/ui/model.py +0 -0
  56. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/ui/picker.py +0 -0
  57. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/ui/theme.py +0 -0
  58. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/undo.py +0 -0
  59. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/update_check.py +0 -0
  60. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle/update_prompt.py +0 -0
  61. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle.egg-info/dependency_links.txt +0 -0
  62. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle.egg-info/entry_points.txt +0 -0
  63. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle.egg-info/requires.txt +0 -0
  64. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/agent_toggle.egg-info/top_level.txt +0 -0
  65. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/pyproject.toml +0 -0
  66. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/setup.cfg +0 -0
  67. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_all_type.py +0 -0
  68. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_cli.py +0 -0
  69. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_color.py +0 -0
  70. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_companions.py +0 -0
  71. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_conformance.py +0 -0
  72. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_containment.py +0 -0
  73. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_cost.py +0 -0
  74. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_doctor.py +0 -0
  75. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_encoding.py +0 -0
  76. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_fs.py +0 -0
  77. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_help.py +0 -0
  78. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_i18n.py +0 -0
  79. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_install.py +0 -0
  80. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_logo.py +0 -0
  81. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_menu.py +0 -0
  82. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_mods.py +0 -0
  83. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_picker.py +0 -0
  84. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_platform.py +0 -0
  85. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_profiles.py +0 -0
  86. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_project.py +0 -0
  87. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_safety.py +0 -0
  88. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_same_name.py +0 -0
  89. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_settings.py +0 -0
  90. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_store.py +0 -0
  91. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_theme.py +0 -0
  92. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_undo.py +0 -0
  93. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_update_check.py +0 -0
  94. {agent_toggle-0.12.0 → agent_toggle-0.13.0}/tests/test_update_prompt.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-toggle
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  Summary: Temporarily disable / re-enable AI-agent skills, agents, commands, rules, plugins and MCP servers across harnesses (Claude Code, Codex, Grok, OpenCode, OpenClaw, Copilot, Vibe, Devin, Antigravity)
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/weskao/agent-toggle
@@ -59,10 +59,11 @@ language, update check and Telegram notifications. It also runs tools such as `d
59
59
 
60
60
  ![agent-toggle config: the settings menu](https://raw.githubusercontent.com/weskao/agent-toggle/main/docs/images/config-menu.jpeg)
61
61
 
62
- **`/agent-toggle` inside Claude Code**: the same picker as a pane, for turning skills, MCP
63
- servers, plugins and mods on or off without leaving the session ([Claude Code pane mod](#claude-code-pane-mod)).
62
+ **`/agent-toggle` inside Claude Code**: the same picker as a pane. Search, then turn skills, MCP
63
+ servers, plugins and mods on or off without leaving the session; each toggle applies at once and
64
+ queues the matching reload ([Claude Code pane mod](#claude-code-pane-mod)).
64
65
 
65
- ![/agent-toggle: the picker as a Claude Code pane](https://raw.githubusercontent.com/weskao/agent-toggle/main/docs/images/claude-pane-mod.jpeg)
66
+ ![/agent-toggle: searching and toggling resources in the Claude Code pane](https://raw.githubusercontent.com/weskao/agent-toggle/main/docs/images/claude-mods-demo.gif)
66
67
 
67
68
  ## Why
68
69
 
@@ -146,17 +147,12 @@ tool itself stays in one place.
146
147
  **Optional: Telegram alerts for CI** (for maintainers of a fork or clone). The
147
148
  Telegram support comes from [telegram-kit](https://pypi.org/project/telegram-kit/)
148
149
  (itself stdlib-only), which every install includes, so a reinstall or upgrade keeps
149
- your settings. Run the setup once; see [CI notifications](#ci-notifications):
150
-
151
- ```sh
152
- agent-toggle config # settings menu: bot token + chat id live under Notifications
153
- agent-toggle config sync-ci # set the two GitHub repository secrets
154
- ```
150
+ your settings. The settings menu hides its Telegram rows for now; set up the alerts as in
151
+ [CI notifications](#ci-notifications).
155
152
 
156
153
  The old `agent-toggle[telegram]` extra still resolves and is now a no-op. If
157
154
  telegram-kit is missing from a broken install, `config test` and `config sync-ci`
158
- exit `4`, bare `config` still opens, and the Telegram rows read
159
- `telegram-kit missing: reinstall agent-toggle`.
155
+ exit `4`.
160
156
 
161
157
  **Optional: the Claude Code pane mod** (the picker as a pane inside Claude Code, claude harness
162
158
  only). It is not part of the PyPI package; with `agent-toggle` already on `PATH` from one of the
@@ -172,16 +168,27 @@ See [Claude Code pane mod](#claude-code-pane-mod).
172
168
 
173
169
  ## Shell completion
174
170
 
175
- Each release publishes static completion files for bash, zsh and fish as the
176
- `completions` artifact of its Release workflow run. They are generated from the
177
- CLI parser; to build them from a checkout:
171
+ `agent-toggle completion <shell>` prints a static completion script, built from the CLI
172
+ parser, for bash, zsh, fish or PowerShell. Save it once where your shell loads it, and
173
+ run the same line again after an upgrade so new commands complete:
178
174
 
179
175
  ```sh
180
- python3 tools/gen_completions.py completions # agent-toggle.bash, _agent-toggle (zsh), agent-toggle.fish
176
+ # zsh: needs fpath=(~/.zfunc $fpath) before compinit in ~/.zshrc
177
+ mkdir -p ~/.zfunc && agent-toggle completion zsh > ~/.zfunc/_agent-toggle
178
+ # bash: loaded on demand by the bash-completion package
179
+ mkdir -p ~/.local/share/bash-completion/completions && agent-toggle completion bash > ~/.local/share/bash-completion/completions/agent-toggle
180
+ # fish
181
+ agent-toggle completion fish > ~/.config/fish/completions/agent-toggle.fish
182
+ ```
183
+
184
+ ```powershell
185
+ # PowerShell: save the script, then load it from your profile
186
+ agent-toggle completion powershell > "$HOME\agent-toggle.ps1"; Add-Content $PROFILE '. "$HOME\agent-toggle.ps1"'
181
187
  ```
182
188
 
183
- Source `agent-toggle.bash` from `~/.bashrc`, put `_agent-toggle` on your zsh
184
- `fpath`, or copy `agent-toggle.fish` to `~/.config/fish/completions/`.
189
+ Without the bash-completion package, add `source <file>` to `~/.bashrc` instead.
190
+ `python3 tools/gen_completions.py completions` writes all four files from a checkout; the
191
+ Release workflow uploads them as its `completions` artifact.
185
192
 
186
193
  ## Usage
187
194
 
@@ -258,7 +265,9 @@ unshared).
258
265
  `--dry-run` (`disable` / `enable` / `enable --all` / `undo` / `profile apply` /
259
266
  `install-shims` / `ui`) computes the plan --
260
267
  moves, companions, backups, MCP edits, warnings -- and writes nothing: no state,
261
- log, lock or backup, and no chmod; it never shells out to `claude`. `disable` /
268
+ log, lock or backup, and no chmod; it never shells out to `claude`, except one
269
+ read-only `claude plugin list --json` (up to 30 s) when a killed run left a plugin
270
+ op pending and has to be settled. `disable` /
262
271
  `enable` result rows have action `would-disable` / `would-enable`; every plan
263
272
  row has status `planned`. Read-only commands (`list`, `status`, `cost`) also
264
273
  change nothing on disk -- `status` warns about a `state.json` or
@@ -426,8 +435,7 @@ end of input, `q` or an empty line exits `0`
426
435
  | General | Check for updates, Color, Logo, Language, Default harness |
427
436
  | Harnesses | one On/Off per harness, with `found ~/.x` or `not on this machine` |
428
437
  | Picker | default sort, harness filter, type filter |
429
- | Notifications | Telegram bot token (masked, last 4 characters shown), Telegram chat ID |
430
- | Tools | Health check (`doctor`; then asks y/n per fix it can run), Install shims, Undo last change (asks y/n), Send test message, Sync CI secrets (asks y/n); their output shows in an overlay |
438
+ | Tools | Health check (`doctor`; then asks y/n per fix it can run), Install shims, Undo last change (asks y/n); their output shows in an overlay |
431
439
 
432
440
  Keys: `↑` `↓` move (wraps, skips headings), `←` `→` cycle a value, `Enter` / `Space` change or run
433
441
  the row, `r` reset the row, `R` reset everything (asks y/n), `q` / `Esc` / `Ctrl-C` quit. In an
@@ -566,7 +574,7 @@ rather than guessing.
566
574
 
567
575
  OpenCode may read skills from another harness's directory through
568
576
  `opencode.json` → `skills.paths` (absolute, `~`, `$HOME/...`
569
- and `${HOME}/...` entries; read from `opencode.json`, else `opencode.jsonc`;
577
+ and `${HOME}/...` entries; read from `opencode.json` and `opencode.jsonc` (OpenCode loads both and merges them);
570
578
  relative entries are skipped, since OpenCode resolves them against the session
571
579
  directory). OpenCode also always scans `~/.claude/skills` and `~/.agents/skills`,
572
580
  so claude's skills count as shared with it. Such a directory is **one** item, filed under
@@ -574,6 +582,11 @@ its owner (the harness whose home really holds it): it is
574
582
  parked once, tracked once, and every row reports `shared_with`, the other
575
583
  harnesses it also affects. `status` prints `shared dir with: ...`.
576
584
 
585
+ Disabling an OpenCode skill while a same-named copy sits in another scanned dir warns
586
+ that OpenCode still loads that copy (stderr, and `warnings` in `--json`). Two copies of
587
+ one name under one owner share one record, so a second `disable` is refused with
588
+ `another copy is already parked at <path> -- enable it first`.
589
+
577
590
  If the live directory carries `.synced-from-*` markers, a sync job may
578
591
  re-create what you parked; `status` and `disable` warn about it. Park in the
579
592
  source harness instead. The warning is printed once per real directory, naming
@@ -873,10 +886,11 @@ a warning and a `recovered` log row (which `undo` does not reverse). `status` an
873
886
  `doctor` report it, with `agent-toggle enable ...` when the item ends up
874
887
  disabled. The one case left to you is a cross-filesystem move killed between its
875
888
  two renames: both copies are complete, and the report names the `diff -r` to
876
- check and the `rm -rf` that keeps either one. MCP and plugin items are saved
877
- per item (when the next one starts), without a `pending` record (a project `.mcp.json`
878
- edit has one): a kill inside that window
879
- leaves the change unrecorded (an MCP server's backup stays in `mcp-backups/`).
889
+ check and the `rm -rf` that keeps either one. MCP servers (user-scope json,
890
+ claude-json, toml, project `.mcp.json`) and claude plugins have a `pending` record
891
+ too (a plugin is settled from a live `claude plugin list --json`); only a
892
+ `claude.ai` connector toggle is saved per item (when the next one starts), and a
893
+ kill inside that window leaves its change unrecorded.
880
894
  A killed run also leaves its `lock`; the next run's error says so.
881
895
 
882
896
  Each `log.jsonl` row is `{ts, harness, type, name, action, result, batch,
@@ -1027,21 +1041,24 @@ it on macOS, Linux and Windows.
1027
1041
 
1028
1042
  ## CI notifications
1029
1043
 
1030
- `.github/workflows/ci.yml` can send a Telegram message when the test matrix fails on a
1031
- push (a lint-only failure does not page). Alerts never fire for pull requests or green
1032
- runs. The message names the repository, branch, 7-character commit and a link to the run.
1044
+ CI and the Release workflow can send a Telegram message when they fail: `ci.yml` when the
1045
+ test matrix fails on a push (a lint-only failure does not page), `release.yml` when the
1046
+ build, PyPI publish or post-publish smoke test fails. Alerts never fire for pull requests or
1047
+ green runs. Both call `.github/workflows/notify-telegram.yml`, and the message names the
1048
+ failure, repository, tag or branch, 7-character commit and a link to the run.
1033
1049
 
1034
- Set it up once, with the `telegram` extra installed (see [Install](#install)):
1050
+ The [settings menu](#settings--config-menu) hides its Telegram rows (bot token, chat ID,
1051
+ Send test message, Sync CI secrets) for now; `SHOW_TELEGRAM` in `agent_toggle/ui/config_menu.py`
1052
+ brings them back. `config test` and `config sync-ci` still work: they read a bot token and
1053
+ chat ID saved earlier, else `TG_BOT_TOKEN` / `TG_CHAT_ID` from the environment. Without
1054
+ either, set the secrets by hand (below).
1035
1055
 
1036
1056
  ```sh
1037
- agent-toggle config # settings menu: Notifications has the bot token (hidden) and chat id
1038
1057
  agent-toggle config test # send one test message
1039
1058
  agent-toggle config sync-ci # set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID on the repo
1040
1059
  ```
1041
1060
 
1042
- In the [settings menu](#settings--config-menu) an inline field commits on `Enter`, cancels on
1043
- `Esc`, and clears on `-` then `Enter`; the Tools group also has Send test message and Sync CI
1044
- secrets (the same as `config test` / `config sync-ci`). The bot token
1061
+ When the rows are shown, the bot token
1045
1062
  is kept only in the OS credential store (macOS Keychain, Linux Secret Service, Windows DPAPI)
1046
1063
  through telegram-kit, never in a file or on a command line, and is shown masked. With no
1047
1064
  credential store it refuses to store the token; set `TG_BOT_TOKEN` in the environment instead
@@ -1057,6 +1074,12 @@ gh secret set TELEGRAM_BOT_TOKEN
1057
1074
  gh secret set TELEGRAM_CHAT_ID
1058
1075
  ```
1059
1076
 
1077
+ To check the repository secrets themselves, send a test message through the same workflow:
1078
+
1079
+ ```sh
1080
+ gh workflow run notify-telegram.yml # optional: -f title="..."
1081
+ ```
1082
+
1060
1083
  With either secret unset (a fork, or a clone you have not configured) the notify job
1061
1084
  prints a `::notice::` and exits 0, so it never adds a second red X.
1062
1085
 
@@ -26,10 +26,11 @@ language, update check and Telegram notifications. It also runs tools such as `d
26
26
 
27
27
  ![agent-toggle config: the settings menu](https://raw.githubusercontent.com/weskao/agent-toggle/main/docs/images/config-menu.jpeg)
28
28
 
29
- **`/agent-toggle` inside Claude Code**: the same picker as a pane, for turning skills, MCP
30
- servers, plugins and mods on or off without leaving the session ([Claude Code pane mod](#claude-code-pane-mod)).
29
+ **`/agent-toggle` inside Claude Code**: the same picker as a pane. Search, then turn skills, MCP
30
+ servers, plugins and mods on or off without leaving the session; each toggle applies at once and
31
+ queues the matching reload ([Claude Code pane mod](#claude-code-pane-mod)).
31
32
 
32
- ![/agent-toggle: the picker as a Claude Code pane](https://raw.githubusercontent.com/weskao/agent-toggle/main/docs/images/claude-pane-mod.jpeg)
33
+ ![/agent-toggle: searching and toggling resources in the Claude Code pane](https://raw.githubusercontent.com/weskao/agent-toggle/main/docs/images/claude-mods-demo.gif)
33
34
 
34
35
  ## Why
35
36
 
@@ -113,17 +114,12 @@ tool itself stays in one place.
113
114
  **Optional: Telegram alerts for CI** (for maintainers of a fork or clone). The
114
115
  Telegram support comes from [telegram-kit](https://pypi.org/project/telegram-kit/)
115
116
  (itself stdlib-only), which every install includes, so a reinstall or upgrade keeps
116
- your settings. Run the setup once; see [CI notifications](#ci-notifications):
117
-
118
- ```sh
119
- agent-toggle config # settings menu: bot token + chat id live under Notifications
120
- agent-toggle config sync-ci # set the two GitHub repository secrets
121
- ```
117
+ your settings. The settings menu hides its Telegram rows for now; set up the alerts as in
118
+ [CI notifications](#ci-notifications).
122
119
 
123
120
  The old `agent-toggle[telegram]` extra still resolves and is now a no-op. If
124
121
  telegram-kit is missing from a broken install, `config test` and `config sync-ci`
125
- exit `4`, bare `config` still opens, and the Telegram rows read
126
- `telegram-kit missing: reinstall agent-toggle`.
122
+ exit `4`.
127
123
 
128
124
  **Optional: the Claude Code pane mod** (the picker as a pane inside Claude Code, claude harness
129
125
  only). It is not part of the PyPI package; with `agent-toggle` already on `PATH` from one of the
@@ -139,16 +135,27 @@ See [Claude Code pane mod](#claude-code-pane-mod).
139
135
 
140
136
  ## Shell completion
141
137
 
142
- Each release publishes static completion files for bash, zsh and fish as the
143
- `completions` artifact of its Release workflow run. They are generated from the
144
- CLI parser; to build them from a checkout:
138
+ `agent-toggle completion <shell>` prints a static completion script, built from the CLI
139
+ parser, for bash, zsh, fish or PowerShell. Save it once where your shell loads it, and
140
+ run the same line again after an upgrade so new commands complete:
145
141
 
146
142
  ```sh
147
- python3 tools/gen_completions.py completions # agent-toggle.bash, _agent-toggle (zsh), agent-toggle.fish
143
+ # zsh: needs fpath=(~/.zfunc $fpath) before compinit in ~/.zshrc
144
+ mkdir -p ~/.zfunc && agent-toggle completion zsh > ~/.zfunc/_agent-toggle
145
+ # bash: loaded on demand by the bash-completion package
146
+ mkdir -p ~/.local/share/bash-completion/completions && agent-toggle completion bash > ~/.local/share/bash-completion/completions/agent-toggle
147
+ # fish
148
+ agent-toggle completion fish > ~/.config/fish/completions/agent-toggle.fish
149
+ ```
150
+
151
+ ```powershell
152
+ # PowerShell: save the script, then load it from your profile
153
+ agent-toggle completion powershell > "$HOME\agent-toggle.ps1"; Add-Content $PROFILE '. "$HOME\agent-toggle.ps1"'
148
154
  ```
149
155
 
150
- Source `agent-toggle.bash` from `~/.bashrc`, put `_agent-toggle` on your zsh
151
- `fpath`, or copy `agent-toggle.fish` to `~/.config/fish/completions/`.
156
+ Without the bash-completion package, add `source <file>` to `~/.bashrc` instead.
157
+ `python3 tools/gen_completions.py completions` writes all four files from a checkout; the
158
+ Release workflow uploads them as its `completions` artifact.
152
159
 
153
160
  ## Usage
154
161
 
@@ -225,7 +232,9 @@ unshared).
225
232
  `--dry-run` (`disable` / `enable` / `enable --all` / `undo` / `profile apply` /
226
233
  `install-shims` / `ui`) computes the plan --
227
234
  moves, companions, backups, MCP edits, warnings -- and writes nothing: no state,
228
- log, lock or backup, and no chmod; it never shells out to `claude`. `disable` /
235
+ log, lock or backup, and no chmod; it never shells out to `claude`, except one
236
+ read-only `claude plugin list --json` (up to 30 s) when a killed run left a plugin
237
+ op pending and has to be settled. `disable` /
229
238
  `enable` result rows have action `would-disable` / `would-enable`; every plan
230
239
  row has status `planned`. Read-only commands (`list`, `status`, `cost`) also
231
240
  change nothing on disk -- `status` warns about a `state.json` or
@@ -393,8 +402,7 @@ end of input, `q` or an empty line exits `0`
393
402
  | General | Check for updates, Color, Logo, Language, Default harness |
394
403
  | Harnesses | one On/Off per harness, with `found ~/.x` or `not on this machine` |
395
404
  | Picker | default sort, harness filter, type filter |
396
- | Notifications | Telegram bot token (masked, last 4 characters shown), Telegram chat ID |
397
- | Tools | Health check (`doctor`; then asks y/n per fix it can run), Install shims, Undo last change (asks y/n), Send test message, Sync CI secrets (asks y/n); their output shows in an overlay |
405
+ | Tools | Health check (`doctor`; then asks y/n per fix it can run), Install shims, Undo last change (asks y/n); their output shows in an overlay |
398
406
 
399
407
  Keys: `↑` `↓` move (wraps, skips headings), `←` `→` cycle a value, `Enter` / `Space` change or run
400
408
  the row, `r` reset the row, `R` reset everything (asks y/n), `q` / `Esc` / `Ctrl-C` quit. In an
@@ -533,7 +541,7 @@ rather than guessing.
533
541
 
534
542
  OpenCode may read skills from another harness's directory through
535
543
  `opencode.json` → `skills.paths` (absolute, `~`, `$HOME/...`
536
- and `${HOME}/...` entries; read from `opencode.json`, else `opencode.jsonc`;
544
+ and `${HOME}/...` entries; read from `opencode.json` and `opencode.jsonc` (OpenCode loads both and merges them);
537
545
  relative entries are skipped, since OpenCode resolves them against the session
538
546
  directory). OpenCode also always scans `~/.claude/skills` and `~/.agents/skills`,
539
547
  so claude's skills count as shared with it. Such a directory is **one** item, filed under
@@ -541,6 +549,11 @@ its owner (the harness whose home really holds it): it is
541
549
  parked once, tracked once, and every row reports `shared_with`, the other
542
550
  harnesses it also affects. `status` prints `shared dir with: ...`.
543
551
 
552
+ Disabling an OpenCode skill while a same-named copy sits in another scanned dir warns
553
+ that OpenCode still loads that copy (stderr, and `warnings` in `--json`). Two copies of
554
+ one name under one owner share one record, so a second `disable` is refused with
555
+ `another copy is already parked at <path> -- enable it first`.
556
+
544
557
  If the live directory carries `.synced-from-*` markers, a sync job may
545
558
  re-create what you parked; `status` and `disable` warn about it. Park in the
546
559
  source harness instead. The warning is printed once per real directory, naming
@@ -840,10 +853,11 @@ a warning and a `recovered` log row (which `undo` does not reverse). `status` an
840
853
  `doctor` report it, with `agent-toggle enable ...` when the item ends up
841
854
  disabled. The one case left to you is a cross-filesystem move killed between its
842
855
  two renames: both copies are complete, and the report names the `diff -r` to
843
- check and the `rm -rf` that keeps either one. MCP and plugin items are saved
844
- per item (when the next one starts), without a `pending` record (a project `.mcp.json`
845
- edit has one): a kill inside that window
846
- leaves the change unrecorded (an MCP server's backup stays in `mcp-backups/`).
856
+ check and the `rm -rf` that keeps either one. MCP servers (user-scope json,
857
+ claude-json, toml, project `.mcp.json`) and claude plugins have a `pending` record
858
+ too (a plugin is settled from a live `claude plugin list --json`); only a
859
+ `claude.ai` connector toggle is saved per item (when the next one starts), and a
860
+ kill inside that window leaves its change unrecorded.
847
861
  A killed run also leaves its `lock`; the next run's error says so.
848
862
 
849
863
  Each `log.jsonl` row is `{ts, harness, type, name, action, result, batch,
@@ -994,21 +1008,24 @@ it on macOS, Linux and Windows.
994
1008
 
995
1009
  ## CI notifications
996
1010
 
997
- `.github/workflows/ci.yml` can send a Telegram message when the test matrix fails on a
998
- push (a lint-only failure does not page). Alerts never fire for pull requests or green
999
- runs. The message names the repository, branch, 7-character commit and a link to the run.
1011
+ CI and the Release workflow can send a Telegram message when they fail: `ci.yml` when the
1012
+ test matrix fails on a push (a lint-only failure does not page), `release.yml` when the
1013
+ build, PyPI publish or post-publish smoke test fails. Alerts never fire for pull requests or
1014
+ green runs. Both call `.github/workflows/notify-telegram.yml`, and the message names the
1015
+ failure, repository, tag or branch, 7-character commit and a link to the run.
1000
1016
 
1001
- Set it up once, with the `telegram` extra installed (see [Install](#install)):
1017
+ The [settings menu](#settings--config-menu) hides its Telegram rows (bot token, chat ID,
1018
+ Send test message, Sync CI secrets) for now; `SHOW_TELEGRAM` in `agent_toggle/ui/config_menu.py`
1019
+ brings them back. `config test` and `config sync-ci` still work: they read a bot token and
1020
+ chat ID saved earlier, else `TG_BOT_TOKEN` / `TG_CHAT_ID` from the environment. Without
1021
+ either, set the secrets by hand (below).
1002
1022
 
1003
1023
  ```sh
1004
- agent-toggle config # settings menu: Notifications has the bot token (hidden) and chat id
1005
1024
  agent-toggle config test # send one test message
1006
1025
  agent-toggle config sync-ci # set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID on the repo
1007
1026
  ```
1008
1027
 
1009
- In the [settings menu](#settings--config-menu) an inline field commits on `Enter`, cancels on
1010
- `Esc`, and clears on `-` then `Enter`; the Tools group also has Send test message and Sync CI
1011
- secrets (the same as `config test` / `config sync-ci`). The bot token
1028
+ When the rows are shown, the bot token
1012
1029
  is kept only in the OS credential store (macOS Keychain, Linux Secret Service, Windows DPAPI)
1013
1030
  through telegram-kit, never in a file or on a command line, and is shown masked. With no
1014
1031
  credential store it refuses to store the token; set `TG_BOT_TOKEN` in the environment instead
@@ -1024,6 +1041,12 @@ gh secret set TELEGRAM_BOT_TOKEN
1024
1041
  gh secret set TELEGRAM_CHAT_ID
1025
1042
  ```
1026
1043
 
1044
+ To check the repository secrets themselves, send a test message through the same workflow:
1045
+
1046
+ ```sh
1047
+ gh workflow run notify-telegram.yml # optional: -f title="..."
1048
+ ```
1049
+
1027
1050
  With either secret unset (a fork, or a clone you have not configured) the notify job
1028
1051
  prints a `::notice::` and exits 0, so it never adds a second red X.
1029
1052
 
@@ -1,3 +1,3 @@
1
1
  """agent-toggle: temporarily disable / re-enable AI-agent resources."""
2
2
 
3
- __version__ = "0.12.0"
3
+ __version__ = "0.13.0"
@@ -134,7 +134,7 @@ def read_project_mcp(file: Path) -> tuple[str, dict]:
134
134
  try:
135
135
  if file.stat().st_size > MAX_PROJECT_MCP:
136
136
  raise ProjectMcpError(f"{file} is larger than {MAX_PROJECT_MCP >> 20} MiB")
137
- text = file.read_bytes().decode("utf-8") # bytes: CRLF must survive
137
+ text = fs.read_utf8(file) # bytes: CRLF must survive
138
138
  except FileNotFoundError:
139
139
  raise ProjectMcpError(f"{file} not found") from None
140
140
  except (OSError, UnicodeDecodeError) as e:
@@ -59,8 +59,8 @@ def run_cli(binary: str | None, args: list[str], cwd: str | None = None,
59
59
  return False, "CLI not found on PATH"
60
60
  limit = _timeout(timeout)
61
61
  try:
62
- p = runner([binary, *args], capture_output=True, text=True,
63
- timeout=limit, cwd=cwd)
62
+ p = runner([binary, *args], capture_output=True, text=True, encoding="utf-8",
63
+ errors="replace", timeout=limit, cwd=cwd) # Windows: not cp1252
64
64
  except subprocess.TimeoutExpired:
65
65
  return False, f"CLI timed out after {limit:g}s (raise it with {TIMEOUT_ENV}=<seconds>)"
66
66
  except OSError as e: # cwd gone, binary unexecutable
@@ -79,7 +79,8 @@ def list_plugins(warn: Callable[[str], None]) -> list[dict]:
79
79
  except ValueError:
80
80
  data = None
81
81
  if not isinstance(data, list):
82
- warn("could not read `claude plugin list --json`; plugin rows come from state only")
82
+ why = f" ({text.splitlines()[-1][:120]})" if not ok and text else "" # e.g. a timeout
83
+ warn(f"could not read `claude plugin list --json`{why}; plugin rows come from state only")
83
84
  return []
84
85
  return [p for p in data if isinstance(p, dict) and isinstance(p.get("id"), str)]
85
86
 
@@ -51,6 +51,7 @@ from pathlib import Path
51
51
 
52
52
  from . import (
53
53
  __version__,
54
+ completions,
54
55
  config,
55
56
  cost,
56
57
  doctor,
@@ -558,7 +559,7 @@ def _stale_ignore_lines(h, args: argparse.Namespace, out: Result) -> None:
558
559
 
559
560
 
560
561
  COMMANDS = ("ui", "pick", "status", "list", "cost", "migrate", "disable", "enable",
561
- "install-shims", "profile", "undo", "doctor", "config", "update")
562
+ "install-shims", "profile", "undo", "doctor", "config", "update", "completion")
562
563
  _GLOBAL_FLAGS = ("--json", "-v", "--verbose")
563
564
 
564
565
 
@@ -701,6 +702,9 @@ def build_parser() -> argparse.ArgumentParser:
701
702
  help="test: send one message | sync-ci: set the GitHub repo secrets")
702
703
  cf.add_argument("--repo", metavar="OWNER/REPO", help="sync-ci: repository (default: this one)")
703
704
  cf.add_argument("--dry-run", action="store_true", help="sync-ci: show the plan; set nothing")
705
+ cm = sub.add_parser("completion", parents=[common],
706
+ help="print the shell completion script")
707
+ cm.add_argument("shell", choices=list(completions.SHELLS))
704
708
  up2 = sub.add_parser("undo", parents=[common], help="reverse the last logged batch")
705
709
  up2.add_argument("--dry-run", action="store_true", help="show the plan; change nothing")
706
710
  for name, verb in (("disable", "park"), ("enable", "restore")):
@@ -730,7 +734,8 @@ def main(argv: list[str] | None = None) -> int:
730
734
  i18n.set_language(language)
731
735
  color = scan_color(argv, color) # --color > env > setting > auto
732
736
  i = _command_index(argv)
733
- started = None if argv[i:i + 1] == ["update"] else update_prompt.start() # `update` is the check
737
+ # `update` is the check; `completion` may run on every shell start (eval "$(...)")
738
+ started = None if argv[i:i + 1] in (["update"], ["completion"]) else update_prompt.start()
734
739
  try:
735
740
  return _main(argv, color)
736
741
  except KeyboardInterrupt:
@@ -802,6 +807,17 @@ def _main(argv: list[str], color: str) -> int:
802
807
  update_prompt.cmd_update(out)
803
808
  elif cmd == "config":
804
809
  config.cmd_config(args, out)
810
+ elif cmd == "completion":
811
+ if out.json_mode:
812
+ die("completion prints a shell script; --json is not supported", 2)
813
+ text = completions.script(args.shell, build_parser())
814
+ buf = getattr(sys.stdout, "buffer", None)
815
+ if buf is None: # a StringIO: no newline translation to dodge
816
+ print(text, end="")
817
+ else: # bytes: Windows text stdout would write \r\n,
818
+ sys.stdout.flush() # which bash rejects
819
+ buf.write(text.encode("utf-8"))
820
+ buf.flush()
805
821
  elif cmd == "undo":
806
822
  undo.cmd_undo(args.dry_run, args.harness, out)
807
823
  elif cmd == "enable" and args.all: