agent-toggle 0.12.1__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.1 → agent_toggle-0.13.0}/PKG-INFO +57 -36
  2. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/README.md +56 -35
  3. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/__init__.py +1 -1
  4. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/backends/plugin_cli.py +4 -3
  5. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/cli.py +18 -2
  6. agent_toggle-0.13.0/agent_toggle/completions.py +210 -0
  7. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/doctor.py +4 -2
  8. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/harnesses.py +13 -19
  9. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/help.py +11 -1
  10. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/locales/zh_tw_cli.py +4 -0
  11. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/mechanisms.py +283 -107
  12. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/ops.py +5 -4
  13. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/store.py +4 -3
  14. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/ui/config_menu.py +12 -6
  15. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle.egg-info/PKG-INFO +57 -36
  16. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle.egg-info/SOURCES.txt +1 -0
  17. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_alias.py +2 -2
  18. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_backends.py +23 -5
  19. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_cli_surface.py +9 -0
  20. agent_toggle-0.13.0/tests/test_completions.py +98 -0
  21. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_config.py +11 -0
  22. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_config_menu.py +32 -7
  23. agent_toggle-0.13.0/tests/test_crash.py +1058 -0
  24. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_harnesses.py +22 -0
  25. agent_toggle-0.13.0/tests/test_mechanisms.py +199 -0
  26. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_secret_audit.py +62 -0
  27. agent_toggle-0.12.1/tests/test_completions.py +0 -37
  28. agent_toggle-0.12.1/tests/test_crash.py +0 -382
  29. agent_toggle-0.12.1/tests/test_mechanisms.py +0 -79
  30. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/LICENSE +0 -0
  31. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/__main__.py +0 -0
  32. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/backends/__init__.py +0 -0
  33. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/backends/flag_json.py +0 -0
  34. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/backends/mcp_json.py +0 -0
  35. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/backends/mcp_toml.py +0 -0
  36. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/companions.py +0 -0
  37. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/config.py +0 -0
  38. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/cost.py +0 -0
  39. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/fs.py +0 -0
  40. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/i18n.py +0 -0
  41. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/locales/__init__.py +0 -0
  42. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/locales/zh_tw_common.py +0 -0
  43. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/locales/zh_tw_config.py +0 -0
  44. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/locales/zh_tw_picker.py +0 -0
  45. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/output.py +0 -0
  46. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/profiles.py +0 -0
  47. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/settings.py +0 -0
  48. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/shims/claude.md.tmpl +0 -0
  49. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/shims/generic.md.tmpl +0 -0
  50. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/spinner.py +0 -0
  51. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/toml_check.py +0 -0
  52. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/ui/__init__.py +0 -0
  53. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/ui/logo.py +0 -0
  54. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/ui/menu.py +0 -0
  55. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/ui/model.py +0 -0
  56. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/ui/picker.py +0 -0
  57. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/ui/theme.py +0 -0
  58. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/undo.py +0 -0
  59. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/update_check.py +0 -0
  60. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle/update_prompt.py +0 -0
  61. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle.egg-info/dependency_links.txt +0 -0
  62. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle.egg-info/entry_points.txt +0 -0
  63. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle.egg-info/requires.txt +0 -0
  64. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/agent_toggle.egg-info/top_level.txt +0 -0
  65. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/pyproject.toml +0 -0
  66. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/setup.cfg +0 -0
  67. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_all_type.py +0 -0
  68. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_cli.py +0 -0
  69. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_color.py +0 -0
  70. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_companions.py +0 -0
  71. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_conformance.py +0 -0
  72. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_containment.py +0 -0
  73. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_cost.py +0 -0
  74. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_doctor.py +0 -0
  75. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_encoding.py +0 -0
  76. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_fs.py +0 -0
  77. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_help.py +0 -0
  78. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_i18n.py +0 -0
  79. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_install.py +0 -0
  80. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_logo.py +0 -0
  81. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_menu.py +0 -0
  82. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_mods.py +0 -0
  83. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_picker.py +0 -0
  84. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_platform.py +0 -0
  85. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_profiles.py +0 -0
  86. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_project.py +0 -0
  87. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_safety.py +0 -0
  88. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_same_name.py +0 -0
  89. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_settings.py +0 -0
  90. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_store.py +0 -0
  91. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_theme.py +0 -0
  92. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_undo.py +0 -0
  93. {agent_toggle-0.12.1 → agent_toggle-0.13.0}/tests/test_update_check.py +0 -0
  94. {agent_toggle-0.12.1 → 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.1
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
181
182
  ```
182
183
 
183
- Source `agent-toggle.bash` from `~/.bashrc`, put `_agent-toggle` on your zsh
184
- `fpath`, or copy `agent-toggle.fish` to `~/.config/fish/completions/`.
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"'
187
+ ```
188
+
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
@@ -391,8 +400,6 @@ a band above the prompt, buttons) and change Claude Code's behaviour. A plugin w
391
400
  `hooks.json`, or with only ordinary hooks (shell commands such as `PreToolUse`), is a plain
392
401
  plugin: it ships skills, commands, agents, MCP servers or hooks and draws nothing.
393
402
 
394
- ![Claude Mods: toggling mods from the picker](https://raw.githubusercontent.com/weskao/agent-toggle/main/docs/images/claude-mods-demo.gif)
395
-
396
403
  - The check reads the install path `claude plugin list` reports and every cached version
397
404
  under `~/.claude/plugins/cache/<marketplace>/<name>/`, so a parked mod is still a mod.
398
405
  To list the installed mods by hand:
@@ -428,8 +435,7 @@ end of input, `q` or an empty line exits `0`
428
435
  | General | Check for updates, Color, Logo, Language, Default harness |
429
436
  | Harnesses | one On/Off per harness, with `found ~/.x` or `not on this machine` |
430
437
  | Picker | default sort, harness filter, type filter |
431
- | Notifications | Telegram bot token (masked, last 4 characters shown), Telegram chat ID |
432
- | 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 |
433
439
 
434
440
  Keys: `↑` `↓` move (wraps, skips headings), `←` `→` cycle a value, `Enter` / `Space` change or run
435
441
  the row, `r` reset the row, `R` reset everything (asks y/n), `q` / `Esc` / `Ctrl-C` quit. In an
@@ -568,7 +574,7 @@ rather than guessing.
568
574
 
569
575
  OpenCode may read skills from another harness's directory through
570
576
  `opencode.json` → `skills.paths` (absolute, `~`, `$HOME/...`
571
- 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);
572
578
  relative entries are skipped, since OpenCode resolves them against the session
573
579
  directory). OpenCode also always scans `~/.claude/skills` and `~/.agents/skills`,
574
580
  so claude's skills count as shared with it. Such a directory is **one** item, filed under
@@ -576,6 +582,11 @@ its owner (the harness whose home really holds it): it is
576
582
  parked once, tracked once, and every row reports `shared_with`, the other
577
583
  harnesses it also affects. `status` prints `shared dir with: ...`.
578
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
+
579
590
  If the live directory carries `.synced-from-*` markers, a sync job may
580
591
  re-create what you parked; `status` and `disable` warn about it. Park in the
581
592
  source harness instead. The warning is printed once per real directory, naming
@@ -875,10 +886,11 @@ a warning and a `recovered` log row (which `undo` does not reverse). `status` an
875
886
  `doctor` report it, with `agent-toggle enable ...` when the item ends up
876
887
  disabled. The one case left to you is a cross-filesystem move killed between its
877
888
  two renames: both copies are complete, and the report names the `diff -r` to
878
- check and the `rm -rf` that keeps either one. MCP and plugin items are saved
879
- per item (when the next one starts), without a `pending` record (a project `.mcp.json`
880
- edit has one): a kill inside that window
881
- 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.
882
894
  A killed run also leaves its `lock`; the next run's error says so.
883
895
 
884
896
  Each `log.jsonl` row is `{ts, harness, type, name, action, result, batch,
@@ -1029,21 +1041,24 @@ it on macOS, Linux and Windows.
1029
1041
 
1030
1042
  ## CI notifications
1031
1043
 
1032
- `.github/workflows/ci.yml` can send a Telegram message when the test matrix fails on a
1033
- push (a lint-only failure does not page). Alerts never fire for pull requests or green
1034
- 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.
1035
1049
 
1036
- 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).
1037
1055
 
1038
1056
  ```sh
1039
- agent-toggle config # settings menu: Notifications has the bot token (hidden) and chat id
1040
1057
  agent-toggle config test # send one test message
1041
1058
  agent-toggle config sync-ci # set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID on the repo
1042
1059
  ```
1043
1060
 
1044
- In the [settings menu](#settings--config-menu) an inline field commits on `Enter`, cancels on
1045
- `Esc`, and clears on `-` then `Enter`; the Tools group also has Send test message and Sync CI
1046
- secrets (the same as `config test` / `config sync-ci`). The bot token
1061
+ When the rows are shown, the bot token
1047
1062
  is kept only in the OS credential store (macOS Keychain, Linux Secret Service, Windows DPAPI)
1048
1063
  through telegram-kit, never in a file or on a command line, and is shown masked. With no
1049
1064
  credential store it refuses to store the token; set `TG_BOT_TOKEN` in the environment instead
@@ -1059,6 +1074,12 @@ gh secret set TELEGRAM_BOT_TOKEN
1059
1074
  gh secret set TELEGRAM_CHAT_ID
1060
1075
  ```
1061
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
+
1062
1083
  With either secret unset (a fork, or a clone you have not configured) the notify job
1063
1084
  prints a `::notice::` and exits 0, so it never adds a second red X.
1064
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
148
149
  ```
149
150
 
150
- Source `agent-toggle.bash` from `~/.bashrc`, put `_agent-toggle` on your zsh
151
- `fpath`, or copy `agent-toggle.fish` to `~/.config/fish/completions/`.
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"'
154
+ ```
155
+
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
@@ -358,8 +367,6 @@ a band above the prompt, buttons) and change Claude Code's behaviour. A plugin w
358
367
  `hooks.json`, or with only ordinary hooks (shell commands such as `PreToolUse`), is a plain
359
368
  plugin: it ships skills, commands, agents, MCP servers or hooks and draws nothing.
360
369
 
361
- ![Claude Mods: toggling mods from the picker](https://raw.githubusercontent.com/weskao/agent-toggle/main/docs/images/claude-mods-demo.gif)
362
-
363
370
  - The check reads the install path `claude plugin list` reports and every cached version
364
371
  under `~/.claude/plugins/cache/<marketplace>/<name>/`, so a parked mod is still a mod.
365
372
  To list the installed mods by hand:
@@ -395,8 +402,7 @@ end of input, `q` or an empty line exits `0`
395
402
  | General | Check for updates, Color, Logo, Language, Default harness |
396
403
  | Harnesses | one On/Off per harness, with `found ~/.x` or `not on this machine` |
397
404
  | Picker | default sort, harness filter, type filter |
398
- | Notifications | Telegram bot token (masked, last 4 characters shown), Telegram chat ID |
399
- | 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 |
400
406
 
401
407
  Keys: `↑` `↓` move (wraps, skips headings), `←` `→` cycle a value, `Enter` / `Space` change or run
402
408
  the row, `r` reset the row, `R` reset everything (asks y/n), `q` / `Esc` / `Ctrl-C` quit. In an
@@ -535,7 +541,7 @@ rather than guessing.
535
541
 
536
542
  OpenCode may read skills from another harness's directory through
537
543
  `opencode.json` → `skills.paths` (absolute, `~`, `$HOME/...`
538
- 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);
539
545
  relative entries are skipped, since OpenCode resolves them against the session
540
546
  directory). OpenCode also always scans `~/.claude/skills` and `~/.agents/skills`,
541
547
  so claude's skills count as shared with it. Such a directory is **one** item, filed under
@@ -543,6 +549,11 @@ its owner (the harness whose home really holds it): it is
543
549
  parked once, tracked once, and every row reports `shared_with`, the other
544
550
  harnesses it also affects. `status` prints `shared dir with: ...`.
545
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
+
546
557
  If the live directory carries `.synced-from-*` markers, a sync job may
547
558
  re-create what you parked; `status` and `disable` warn about it. Park in the
548
559
  source harness instead. The warning is printed once per real directory, naming
@@ -842,10 +853,11 @@ a warning and a `recovered` log row (which `undo` does not reverse). `status` an
842
853
  `doctor` report it, with `agent-toggle enable ...` when the item ends up
843
854
  disabled. The one case left to you is a cross-filesystem move killed between its
844
855
  two renames: both copies are complete, and the report names the `diff -r` to
845
- check and the `rm -rf` that keeps either one. MCP and plugin items are saved
846
- per item (when the next one starts), without a `pending` record (a project `.mcp.json`
847
- edit has one): a kill inside that window
848
- 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.
849
861
  A killed run also leaves its `lock`; the next run's error says so.
850
862
 
851
863
  Each `log.jsonl` row is `{ts, harness, type, name, action, result, batch,
@@ -996,21 +1008,24 @@ it on macOS, Linux and Windows.
996
1008
 
997
1009
  ## CI notifications
998
1010
 
999
- `.github/workflows/ci.yml` can send a Telegram message when the test matrix fails on a
1000
- push (a lint-only failure does not page). Alerts never fire for pull requests or green
1001
- 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.
1002
1016
 
1003
- 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).
1004
1022
 
1005
1023
  ```sh
1006
- agent-toggle config # settings menu: Notifications has the bot token (hidden) and chat id
1007
1024
  agent-toggle config test # send one test message
1008
1025
  agent-toggle config sync-ci # set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID on the repo
1009
1026
  ```
1010
1027
 
1011
- In the [settings menu](#settings--config-menu) an inline field commits on `Enter`, cancels on
1012
- `Esc`, and clears on `-` then `Enter`; the Tools group also has Send test message and Sync CI
1013
- secrets (the same as `config test` / `config sync-ci`). The bot token
1028
+ When the rows are shown, the bot token
1014
1029
  is kept only in the OS credential store (macOS Keychain, Linux Secret Service, Windows DPAPI)
1015
1030
  through telegram-kit, never in a file or on a command line, and is shown masked. With no
1016
1031
  credential store it refuses to store the token; set `TG_BOT_TOKEN` in the environment instead
@@ -1026,6 +1041,12 @@ gh secret set TELEGRAM_BOT_TOKEN
1026
1041
  gh secret set TELEGRAM_CHAT_ID
1027
1042
  ```
1028
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
+
1029
1050
  With either secret unset (a fork, or a clone you have not configured) the notify job
1030
1051
  prints a `::notice::` and exits 0, so it never adds a second red X.
1031
1052
 
@@ -1,3 +1,3 @@
1
1
  """agent-toggle: temporarily disable / re-enable AI-agent resources."""
2
2
 
3
- __version__ = "0.12.1"
3
+ __version__ = "0.13.0"
@@ -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: