agent-toggle 0.9.0__tar.gz → 0.11.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 (91) hide show
  1. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/PKG-INFO +31 -10
  2. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/README.md +30 -9
  3. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/__init__.py +1 -1
  4. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/cli.py +8 -2
  5. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/fs.py +4 -0
  6. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/help.py +9 -1
  7. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/locales/zh_tw_cli.py +3 -0
  8. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/shims/claude.md.tmpl +2 -1
  9. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/shims/generic.md.tmpl +1 -1
  10. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/store.py +9 -0
  11. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/update_check.py +4 -2
  12. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/update_prompt.py +47 -8
  13. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/PKG-INFO +31 -10
  14. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_cli.py +10 -4
  15. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_cli_surface.py +1 -1
  16. agent_toggle-0.11.0/tests/test_update_prompt.py +78 -0
  17. agent_toggle-0.9.0/tests/test_update_prompt.py +0 -41
  18. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/LICENSE +0 -0
  19. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/__main__.py +0 -0
  20. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/backends/__init__.py +0 -0
  21. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/backends/flag_json.py +0 -0
  22. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/backends/mcp_json.py +0 -0
  23. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/backends/mcp_toml.py +0 -0
  24. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/backends/plugin_cli.py +0 -0
  25. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/companions.py +0 -0
  26. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/config.py +0 -0
  27. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/cost.py +0 -0
  28. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/doctor.py +0 -0
  29. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/harnesses.py +0 -0
  30. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/i18n.py +0 -0
  31. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/locales/__init__.py +0 -0
  32. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/locales/zh_tw_common.py +0 -0
  33. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/locales/zh_tw_config.py +0 -0
  34. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/locales/zh_tw_picker.py +0 -0
  35. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/mechanisms.py +0 -0
  36. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/ops.py +0 -0
  37. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/output.py +0 -0
  38. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/profiles.py +0 -0
  39. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/settings.py +0 -0
  40. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/spinner.py +0 -0
  41. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/toml_check.py +0 -0
  42. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/ui/__init__.py +0 -0
  43. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/ui/config_menu.py +0 -0
  44. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/ui/logo.py +0 -0
  45. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/ui/menu.py +0 -0
  46. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/ui/model.py +0 -0
  47. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/ui/picker.py +0 -0
  48. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/ui/theme.py +0 -0
  49. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/undo.py +0 -0
  50. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/SOURCES.txt +0 -0
  51. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/dependency_links.txt +0 -0
  52. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/entry_points.txt +0 -0
  53. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/requires.txt +0 -0
  54. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/top_level.txt +0 -0
  55. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/pyproject.toml +0 -0
  56. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/setup.cfg +0 -0
  57. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_alias.py +0 -0
  58. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_all_type.py +0 -0
  59. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_backends.py +0 -0
  60. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_color.py +0 -0
  61. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_companions.py +0 -0
  62. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_completions.py +0 -0
  63. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_config.py +0 -0
  64. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_config_menu.py +0 -0
  65. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_conformance.py +0 -0
  66. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_containment.py +0 -0
  67. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_cost.py +0 -0
  68. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_crash.py +0 -0
  69. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_doctor.py +0 -0
  70. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_encoding.py +0 -0
  71. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_fs.py +0 -0
  72. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_harnesses.py +0 -0
  73. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_help.py +0 -0
  74. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_i18n.py +0 -0
  75. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_install.py +0 -0
  76. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_logo.py +0 -0
  77. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_mechanisms.py +0 -0
  78. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_menu.py +0 -0
  79. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_mods.py +0 -0
  80. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_picker.py +0 -0
  81. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_platform.py +0 -0
  82. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_profiles.py +0 -0
  83. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_project.py +0 -0
  84. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_safety.py +0 -0
  85. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_same_name.py +0 -0
  86. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_secret_audit.py +0 -0
  87. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_settings.py +0 -0
  88. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_store.py +0 -0
  89. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_theme.py +0 -0
  90. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_undo.py +0 -0
  91. {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_update_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-toggle
3
- Version: 0.9.0
3
+ Version: 0.11.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
@@ -32,6 +32,8 @@ Dynamic: license-file
32
32
 
33
33
  # agent-toggle
34
34
 
35
+ PyPI: <https://pypi.org/project/agent-toggle/>
36
+
35
37
  Temporarily disable and restore AI-agent resources — skills, agents, commands,
36
38
  rules, plugins (mods included), MCP servers — across Claude Code, Codex, Grok CLI, OpenCode,
37
39
  OpenClaw, Copilot, Vibe, Devin and Antigravity — and show what each one costs at
@@ -193,6 +195,7 @@ agent-toggle <command> [args] # or: python3 agent_toggle.py <command> [
193
195
  | `status` | health check: harnesses found, types each supports, parked counts, gitignore, untracked parked items, stale live twins, shared dirs, and state entries whose parked item is gone (a `stale` row with the fix command; read-only, still exit `0`) |
194
196
  | `list [type]` | what is currently disabled (`--project <dir>` filters to one project) |
195
197
  | `cost [--type T]` | estimated startup tokens per item, biggest first (read-only; `--harness H` filters; `--project <dir>` prices a repo's `.claude/` and `.mcp.json` instead of user scope) |
198
+ | `update` | check PyPI now (ignores the cache and a skipped version) and print the commands that update the CLI, the Claude Code plugin (when installed) and the shims; runs none of them. `/agent-toggle update` in Claude Code does the same; opening the pane toasts a newer release once |
196
199
  | `install-shims` | write the skill shim into every installed harness that is on in the settings (an off one is skipped unless named with `--harness`); refuses to overwrite a file it did not write (`--dry-run` shows the plan); reports stale legacy `<dir>-disabled/` lines in a harness-home `.gitignore` (`--gitignore` removes them, `--no-gitignore` skips the check); on a terminal, then asks y/n for each problem `doctor` can fix (see [Fixing](#fixing)) |
197
200
  | `disable <type> <name>...` | park one or more items (`--dry-run` shows the plan; `--project <dir>` for a repo's own `.claude/` and `.mcp.json`) |
198
201
  | `enable <type> <name>...` | put them back (`--dry-run` shows the plan; `--project <dir>` likewise) |
@@ -367,7 +370,8 @@ off under `--dry-run`. Esc closes the prompt.
367
370
  Nothing happens while the picker is open. Changes are staged, the screen is
368
371
  torn down, and only then do the real operations run, so their output (which
369
372
  companion files moved, which were kept because they are shared) is readable
370
- instead of fighting curses for the terminal.
373
+ instead of fighting curses for the terminal. Claude Code sessions running the
374
+ [pane mod](#claude-code-pane-mod) pick up the Claude changes within 5 s and reload them.
371
375
 
372
376
  Built on stdlib `curses`, so there is nothing to install on macOS and Linux
373
377
  (on Windows, `pip install "agent-toggle[windows]"` pulls `windows-curses`).
@@ -469,10 +473,15 @@ before the command and is offered after it, on every exit path; it overlaps the
469
473
 
470
474
  - **On a terminal** (stdin and stderr are TTYs, no `--json`): a panel on stderr, "agent-toggle X
471
475
  is available (you have Y)", with `Update now`, `Skip` and `Skip until next version`, a release
472
- notes link, and `↑` `↓` `Enter` `q`. `Update now` runs `uv tool upgrade agent-toggle`; if that
473
- fails you get a yellow warning with the command to run yourself.
474
- - **Off a terminal**: two stderr lines, `agent-toggle X is available (you have Y)` and
475
- ` uv tool upgrade agent-toggle`.
476
+ notes link, and `↑` `↓` `Enter` `q`. `Update now` runs, in order and stopping at the first
477
+ failure: `uv tool upgrade agent-toggle` (`pipx upgrade agent-toggle` for a pipx install), then,
478
+ when the Claude Code plugin is installed, `claude plugin marketplace update agent-toggle` and
479
+ `claude plugin update agent-toggle@agent-toggle`, then `agent-toggle install-shims`. A failure
480
+ gives a yellow warning listing the commands to run yourself.
481
+ - **Windows**: a running `agent-toggle.exe` cannot be replaced, so there is no `Update now`; the
482
+ commands are printed instead (as below), to run once agent-toggle has exited.
483
+ - **Off a terminal** (or `--json`): a stderr line, `agent-toggle X is available (you have Y)`,
484
+ then those commands, one per line.
476
485
  - It never changes the exit code or stdout, so `--json` output stays one document.
477
486
 
478
487
  It is one `GET https://pypi.org/pypi/agent-toggle/json` on a background thread with a 0.8 s
@@ -598,7 +607,8 @@ visible rather than silent.
598
607
  hand-rolled serializer would mangle comments — a text slice is lossless and
599
608
  much less code.
600
609
 
601
- MCP changes need a **new session** to take effect.
610
+ MCP changes need a **new session** to take effect, except in a Claude Code session running the
611
+ agent-toggle mod, which runs `/mcp disable|enable <name>` for you (see the mod section).
602
612
 
603
613
  ### Claude MCP scopes
604
614
 
@@ -846,6 +856,7 @@ bookkeeping.
846
856
  | `state.json` | current disabled list (schema v3; atomic write, mode `0600`), saved before each flag write or dir move with the op in flight under an optional `pending` key, and after each MCP / plugin item |
847
857
  | `lock` | held by `disable` / `enable` / `enable --all` / `undo` / `profile apply` / `migrate` / `ui` for the whole batch; a second run waits 5 s then exits `3` (stale after 10 min *and* its PID is gone; a live batch refreshes it per item; the error says when its PID is gone, i.e. a killed run left it) |
848
858
  | `log.jsonl` | one line per operation (mode `0600`), see below |
859
+ | `reload.json` | the Claude rows the last `ui` Enter changed, rewritten each time; open Claude Code sessions running the mod poll it and reload them |
849
860
  | `mcp-backups/` | `<harness>__<server>.json`, or `<sha8>__<harness>__<server>.json` for a project `.mcp.json` (mode `0600` -- may hold auth headers) |
850
861
  | `companions/` | parked exclusive helper files |
851
862
  | `parked/user/<owner>/<dir>/` | user-scope items, e.g. `parked/user/claude/skills/<name>` (`<owner>` = the harness whose home holds the dir; a dir shared by several harnesses parks once, under its owner); an absolute OpenCode `skills.paths` dir parks under `parked/user/opencode/ext-<sha8>/` (`<sha8>` of its resolved path). Nothing is ever parked inside a harness home |
@@ -948,7 +959,8 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
948
959
  - It looks like the `ui` picker: a title with the live-token total, a search line, rows under
949
960
  type headings with counts, green `●` live / yellow `○` parked, tokens and a bar per row (a
950
961
  parked row shows `(N)`, what restoring it would load), the cursor row in reverse video, and a
951
- key-chip footer. The list scrolls with the cursor.
962
+ key-chip footer. The list scrolls with the cursor; the status line shows its position (`7/23`),
963
+ and the first and last list lines count the rows out of view (`↑ 4`, `↓ 12`).
952
964
  - The mouse wheel over the list moves the cursor, as in the terminal picker; no click needed.
953
965
  - Click the list once so it takes keys (Esc hands the keyboard back to the prompt). Then `↑` `↓`
954
966
  `PgUp` `PgDn` `Home` `End` (also `Ctrl-P` / `Ctrl-N`) move; `Space` or `Enter` toggles the row,
@@ -958,8 +970,8 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
958
970
  ...), like `disable all <name>`; `Ctrl-U` clears the search. A click moves the cursor.
959
971
  - A toggle applies at once (no staging, no Enter to apply); the toast names the target and the state
960
972
  with an emoji and a bracketed tag (no colour codes, so nothing garbles; on the Linux console and the
961
- legacy Windows console, where emoji draw as boxes, the tag alone): `✅ [ENABLED] skill: foo`, `⛔ [DISABLED] mod: bar (takes effect in a new
962
- session)`, `⛔ [DISABLED] all: foo` after `n`, or `❌ [FAILED] disable skill: foo - <error>`.
973
+ legacy Windows console, where emoji draw as boxes, the tag alone): `✅ [ENABLED] skill: foo`, `⛔ [DISABLED] mod: bar (/reload-plugins)`,
974
+ `⛔ [DISABLED] rule: baz (takes effect in a new session)`, `⛔ [DISABLED] all: foo` after `n`, or `❌ [FAILED] disable skill: foo - <error>`.
963
975
  The row flips the moment you press (and flips back
964
976
  if the CLI fails); the list reloads in the background. While a toggle runs, further keys are ignored.
965
977
  - Opening the pane shows the same braille spinner and `Loading resources…` as `agent-toggle ui`
@@ -970,6 +982,15 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
970
982
  `undo`, `list`, `status`, `cost` or `doctor` runs the CLI instead of opening the pane and prints
971
983
  one line per result row: `/agent-toggle disable skill foo bar` (it adds `--json`, and
972
984
  `--harness claude` unless you pass `--harness`). A toggle reloads an open pane.
985
+ - Inside Claude Code the mod then applies the change to the running session by queueing the slash
986
+ command for you, once per kind: skills and commands `/reload-skills`, plugins (mods included)
987
+ `/reload-plugins`, an MCP server `/mcp disable|enable <name>`. The toast names it, e.g.
988
+ `⛔ [DISABLED] skill: foo (/reload-skills)`. Agents and rules have no reload and still say
989
+ `takes effect in a new session`. If Claude refuses a queued command, a toast says so.
990
+ - `agent-toggle ui` in another terminal applies on Enter and writes the Claude rows it changed to
991
+ `~/.agent-toggle/reload.json`. Every open session running the mod checks that file's mtime every
992
+ 5 s (one `stat`) and queues the same reloads, toasting `agent-toggle ui: /reload-skills`. A file
993
+ older than the session is ignored.
973
994
  - Needs `agent-toggle` on `PATH`.
974
995
  - On Windows `agent-toggle` must resolve to a real executable on `PATH` (a pip or uv install
975
996
  provides `agent-toggle.exe`).
@@ -1,5 +1,7 @@
1
1
  # agent-toggle
2
2
 
3
+ PyPI: <https://pypi.org/project/agent-toggle/>
4
+
3
5
  Temporarily disable and restore AI-agent resources — skills, agents, commands,
4
6
  rules, plugins (mods included), MCP servers — across Claude Code, Codex, Grok CLI, OpenCode,
5
7
  OpenClaw, Copilot, Vibe, Devin and Antigravity — and show what each one costs at
@@ -161,6 +163,7 @@ agent-toggle <command> [args] # or: python3 agent_toggle.py <command> [
161
163
  | `status` | health check: harnesses found, types each supports, parked counts, gitignore, untracked parked items, stale live twins, shared dirs, and state entries whose parked item is gone (a `stale` row with the fix command; read-only, still exit `0`) |
162
164
  | `list [type]` | what is currently disabled (`--project <dir>` filters to one project) |
163
165
  | `cost [--type T]` | estimated startup tokens per item, biggest first (read-only; `--harness H` filters; `--project <dir>` prices a repo's `.claude/` and `.mcp.json` instead of user scope) |
166
+ | `update` | check PyPI now (ignores the cache and a skipped version) and print the commands that update the CLI, the Claude Code plugin (when installed) and the shims; runs none of them. `/agent-toggle update` in Claude Code does the same; opening the pane toasts a newer release once |
164
167
  | `install-shims` | write the skill shim into every installed harness that is on in the settings (an off one is skipped unless named with `--harness`); refuses to overwrite a file it did not write (`--dry-run` shows the plan); reports stale legacy `<dir>-disabled/` lines in a harness-home `.gitignore` (`--gitignore` removes them, `--no-gitignore` skips the check); on a terminal, then asks y/n for each problem `doctor` can fix (see [Fixing](#fixing)) |
165
168
  | `disable <type> <name>...` | park one or more items (`--dry-run` shows the plan; `--project <dir>` for a repo's own `.claude/` and `.mcp.json`) |
166
169
  | `enable <type> <name>...` | put them back (`--dry-run` shows the plan; `--project <dir>` likewise) |
@@ -335,7 +338,8 @@ off under `--dry-run`. Esc closes the prompt.
335
338
  Nothing happens while the picker is open. Changes are staged, the screen is
336
339
  torn down, and only then do the real operations run, so their output (which
337
340
  companion files moved, which were kept because they are shared) is readable
338
- instead of fighting curses for the terminal.
341
+ instead of fighting curses for the terminal. Claude Code sessions running the
342
+ [pane mod](#claude-code-pane-mod) pick up the Claude changes within 5 s and reload them.
339
343
 
340
344
  Built on stdlib `curses`, so there is nothing to install on macOS and Linux
341
345
  (on Windows, `pip install "agent-toggle[windows]"` pulls `windows-curses`).
@@ -437,10 +441,15 @@ before the command and is offered after it, on every exit path; it overlaps the
437
441
 
438
442
  - **On a terminal** (stdin and stderr are TTYs, no `--json`): a panel on stderr, "agent-toggle X
439
443
  is available (you have Y)", with `Update now`, `Skip` and `Skip until next version`, a release
440
- notes link, and `↑` `↓` `Enter` `q`. `Update now` runs `uv tool upgrade agent-toggle`; if that
441
- fails you get a yellow warning with the command to run yourself.
442
- - **Off a terminal**: two stderr lines, `agent-toggle X is available (you have Y)` and
443
- ` uv tool upgrade agent-toggle`.
444
+ notes link, and `↑` `↓` `Enter` `q`. `Update now` runs, in order and stopping at the first
445
+ failure: `uv tool upgrade agent-toggle` (`pipx upgrade agent-toggle` for a pipx install), then,
446
+ when the Claude Code plugin is installed, `claude plugin marketplace update agent-toggle` and
447
+ `claude plugin update agent-toggle@agent-toggle`, then `agent-toggle install-shims`. A failure
448
+ gives a yellow warning listing the commands to run yourself.
449
+ - **Windows**: a running `agent-toggle.exe` cannot be replaced, so there is no `Update now`; the
450
+ commands are printed instead (as below), to run once agent-toggle has exited.
451
+ - **Off a terminal** (or `--json`): a stderr line, `agent-toggle X is available (you have Y)`,
452
+ then those commands, one per line.
444
453
  - It never changes the exit code or stdout, so `--json` output stays one document.
445
454
 
446
455
  It is one `GET https://pypi.org/pypi/agent-toggle/json` on a background thread with a 0.8 s
@@ -566,7 +575,8 @@ visible rather than silent.
566
575
  hand-rolled serializer would mangle comments — a text slice is lossless and
567
576
  much less code.
568
577
 
569
- MCP changes need a **new session** to take effect.
578
+ MCP changes need a **new session** to take effect, except in a Claude Code session running the
579
+ agent-toggle mod, which runs `/mcp disable|enable <name>` for you (see the mod section).
570
580
 
571
581
  ### Claude MCP scopes
572
582
 
@@ -814,6 +824,7 @@ bookkeeping.
814
824
  | `state.json` | current disabled list (schema v3; atomic write, mode `0600`), saved before each flag write or dir move with the op in flight under an optional `pending` key, and after each MCP / plugin item |
815
825
  | `lock` | held by `disable` / `enable` / `enable --all` / `undo` / `profile apply` / `migrate` / `ui` for the whole batch; a second run waits 5 s then exits `3` (stale after 10 min *and* its PID is gone; a live batch refreshes it per item; the error says when its PID is gone, i.e. a killed run left it) |
816
826
  | `log.jsonl` | one line per operation (mode `0600`), see below |
827
+ | `reload.json` | the Claude rows the last `ui` Enter changed, rewritten each time; open Claude Code sessions running the mod poll it and reload them |
817
828
  | `mcp-backups/` | `<harness>__<server>.json`, or `<sha8>__<harness>__<server>.json` for a project `.mcp.json` (mode `0600` -- may hold auth headers) |
818
829
  | `companions/` | parked exclusive helper files |
819
830
  | `parked/user/<owner>/<dir>/` | user-scope items, e.g. `parked/user/claude/skills/<name>` (`<owner>` = the harness whose home holds the dir; a dir shared by several harnesses parks once, under its owner); an absolute OpenCode `skills.paths` dir parks under `parked/user/opencode/ext-<sha8>/` (`<sha8>` of its resolved path). Nothing is ever parked inside a harness home |
@@ -916,7 +927,8 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
916
927
  - It looks like the `ui` picker: a title with the live-token total, a search line, rows under
917
928
  type headings with counts, green `●` live / yellow `○` parked, tokens and a bar per row (a
918
929
  parked row shows `(N)`, what restoring it would load), the cursor row in reverse video, and a
919
- key-chip footer. The list scrolls with the cursor.
930
+ key-chip footer. The list scrolls with the cursor; the status line shows its position (`7/23`),
931
+ and the first and last list lines count the rows out of view (`↑ 4`, `↓ 12`).
920
932
  - The mouse wheel over the list moves the cursor, as in the terminal picker; no click needed.
921
933
  - Click the list once so it takes keys (Esc hands the keyboard back to the prompt). Then `↑` `↓`
922
934
  `PgUp` `PgDn` `Home` `End` (also `Ctrl-P` / `Ctrl-N`) move; `Space` or `Enter` toggles the row,
@@ -926,8 +938,8 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
926
938
  ...), like `disable all <name>`; `Ctrl-U` clears the search. A click moves the cursor.
927
939
  - A toggle applies at once (no staging, no Enter to apply); the toast names the target and the state
928
940
  with an emoji and a bracketed tag (no colour codes, so nothing garbles; on the Linux console and the
929
- legacy Windows console, where emoji draw as boxes, the tag alone): `✅ [ENABLED] skill: foo`, `⛔ [DISABLED] mod: bar (takes effect in a new
930
- session)`, `⛔ [DISABLED] all: foo` after `n`, or `❌ [FAILED] disable skill: foo - <error>`.
941
+ legacy Windows console, where emoji draw as boxes, the tag alone): `✅ [ENABLED] skill: foo`, `⛔ [DISABLED] mod: bar (/reload-plugins)`,
942
+ `⛔ [DISABLED] rule: baz (takes effect in a new session)`, `⛔ [DISABLED] all: foo` after `n`, or `❌ [FAILED] disable skill: foo - <error>`.
931
943
  The row flips the moment you press (and flips back
932
944
  if the CLI fails); the list reloads in the background. While a toggle runs, further keys are ignored.
933
945
  - Opening the pane shows the same braille spinner and `Loading resources…` as `agent-toggle ui`
@@ -938,6 +950,15 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
938
950
  `undo`, `list`, `status`, `cost` or `doctor` runs the CLI instead of opening the pane and prints
939
951
  one line per result row: `/agent-toggle disable skill foo bar` (it adds `--json`, and
940
952
  `--harness claude` unless you pass `--harness`). A toggle reloads an open pane.
953
+ - Inside Claude Code the mod then applies the change to the running session by queueing the slash
954
+ command for you, once per kind: skills and commands `/reload-skills`, plugins (mods included)
955
+ `/reload-plugins`, an MCP server `/mcp disable|enable <name>`. The toast names it, e.g.
956
+ `⛔ [DISABLED] skill: foo (/reload-skills)`. Agents and rules have no reload and still say
957
+ `takes effect in a new session`. If Claude refuses a queued command, a toast says so.
958
+ - `agent-toggle ui` in another terminal applies on Enter and writes the Claude rows it changed to
959
+ `~/.agent-toggle/reload.json`. Every open session running the mod checks that file's mtime every
960
+ 5 s (one `stat`) and queues the same reloads, toasting `agent-toggle ui: /reload-skills`. A file
961
+ older than the session is ignored.
941
962
  - Needs `agent-toggle` on `PATH`.
942
963
  - On Windows `agent-toggle` must resolve to a real executable on `PATH` (a pip or uv install
943
964
  provides `agent-toggle.exe`).
@@ -1,3 +1,3 @@
1
1
  """agent-toggle: temporarily disable / re-enable AI-agent resources."""
2
2
 
3
- __version__ = "0.9.0"
3
+ __version__ = "0.11.0"
@@ -325,6 +325,7 @@ def cmd_ui(state: dict, out: Result, dry_run: bool = False, project: str | None
325
325
  if dry_run:
326
326
  out.say("\ndry run -- nothing changed")
327
327
  return
328
+ store.signal_reload(out.rows)
328
329
  if any(r.type in ("mcp", "plugin") for r in changes):
329
330
  out.say("\nMCP/plugin changed -- open a NEW session for it to take effect.")
330
331
 
@@ -557,7 +558,7 @@ def _stale_ignore_lines(h, args: argparse.Namespace, out: Result) -> None:
557
558
 
558
559
 
559
560
  COMMANDS = ("ui", "pick", "status", "list", "cost", "migrate", "disable", "enable",
560
- "install-shims", "profile", "undo", "doctor", "config")
561
+ "install-shims", "profile", "undo", "doctor", "config", "update")
561
562
  _GLOBAL_FLAGS = ("--json", "-v", "--verbose")
562
563
 
563
564
 
@@ -692,6 +693,8 @@ def build_parser() -> argparse.ArgumentParser:
692
693
  pp.add_argument("--project", metavar="dir", help=PROJECT_HELP)
693
694
  sub.add_parser("doctor", parents=[common],
694
695
  help="check harness layouts and state against disk; offers y/n fixes")
696
+ sub.add_parser("update", parents=[common],
697
+ help="check for a newer release; show the commands that update everything")
695
698
  cf = sub.add_parser("config", parents=[common],
696
699
  help="Telegram settings for the CI failure alerts (needs the telegram extra)")
697
700
  cf.add_argument("action", nargs="?", choices=("test", "sync-ci"),
@@ -726,7 +729,8 @@ def main(argv: list[str] | None = None) -> int:
726
729
  language, color = "en", "auto"
727
730
  i18n.set_language(language)
728
731
  color = scan_color(argv, color) # --color > env > setting > auto
729
- started = update_prompt.start()
732
+ i = _command_index(argv)
733
+ started = None if argv[i:i + 1] == ["update"] else update_prompt.start() # `update` is the check
730
734
  try:
731
735
  return _main(argv, color)
732
736
  except KeyboardInterrupt:
@@ -794,6 +798,8 @@ def _main(argv: list[str], color: str) -> int:
794
798
  fixes = doctor.cmd_doctor(args.harness, out)
795
799
  if fixes and _interactive(out):
796
800
  doctor.offer_fixes(fixes, out)
801
+ elif cmd == "update":
802
+ update_prompt.cmd_update(out)
797
803
  elif cmd == "config":
798
804
  config.cmd_config(args, out)
799
805
  elif cmd == "undo":
@@ -44,6 +44,10 @@ def log_file() -> Path:
44
44
  return state_dir() / "log.jsonl"
45
45
 
46
46
 
47
+ def reload_file() -> Path:
48
+ return state_dir() / "reload.json"
49
+
50
+
47
51
  def backup_dir() -> Path:
48
52
  return state_dir() / "mcp-backups"
49
53
 
@@ -135,6 +135,14 @@ def commands(default_harness: str = "claude") -> dict[str, Cmd]:
135
135
  ("--dry-run", t("help.opt.config_dry_run",
136
136
  "sync-ci: show the plan; set nothing"))],
137
137
  examples=["config", "config test", "config sync-ci --dry-run"]),
138
+ "update": Cmd(
139
+ "update",
140
+ t("help.update.summary", "check for a newer release; show how to update"),
141
+ t("help.update.desc", "Ask PyPI now (ignoring the cache and a skipped version) and, "
142
+ "when a newer release exists, print the commands that update "
143
+ "the CLI, the Claude Code plugin (if installed) and the shims. "
144
+ "Runs none of them."),
145
+ examples=["update", "update --json"]),
138
146
  "install-shims": Cmd(
139
147
  "install-shims [--dry-run] [--gitignore | --no-gitignore]",
140
148
  t("help.install_shims.summary", "write the skill shim into every installed harness"),
@@ -166,7 +174,7 @@ def commands(default_harness: str = "claude") -> dict[str, Cmd]:
166
174
  def groups() -> list[tuple[str, list[str]]]:
167
175
  return [(t("help.group.toggle", "Toggle"), ["ui", "disable", "enable", "undo", "profile"]),
168
176
  (t("help.group.inspect", "Inspect"), ["status", "list", "cost", "doctor"]),
169
- (t("help.group.setup", "Setup"), ["config", "install-shims", "migrate"])]
177
+ (t("help.group.setup", "Setup"), ["config", "install-shims", "update", "migrate"])]
170
178
 
171
179
 
172
180
  def _table(rows: list[tuple[str, str]], color: bool, role: str = "choice") -> list[str]:
@@ -27,6 +27,9 @@ CATALOG: dict[str, str] = {
27
27
  "help.cost.summary": "每個項目的預估啟動 token 數,由大到小",
28
28
  "help.cost.desc": "估算每個項目啟動時載入多少 token(字元數/4,±25%),"
29
29
  "以及暫存項目已省下多少。",
30
+ "help.update.summary": "檢查新版本;顯示更新方式",
31
+ "help.update.desc": "立即向 PyPI 查詢(忽略快取與已略過的版本);有新版本時,印出更新 CLI、"
32
+ "Claude Code 外掛(若已安裝)與 shim 的指令。不會執行任何指令。",
30
33
  "help.doctor.summary": "檢查 harness 目錄結構與狀態",
31
34
  "help.doctor.desc": "比對狀態檔與磁碟;有問題時結束碼為 1。在終端機上,可自動修復的問題"
32
35
  "(chmod、過期項目、孤立檔案、中斷的操作)會逐一詢問 y/n;否則不做任何變更。",
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: agent-toggle
3
3
  description: Temporarily disable or restore AI-agent resources — skills, agents, commands, rules, plugins, MCP servers — across Claude Code, Codex, Grok CLI, OpenCode, OpenClaw, Copilot and Vibe. Nothing is deleted; everything is parked and reversible. Use when the user wants to turn something off, trim startup context cost, re-enable something previously disabled, or asks "how do I disable X" / "turn off this MCP" / "add it back". Also when they ask to open, close or toggle the agent-toggle pane ("open agent-toggle" / "close the agent-toggle pane"), and to save, switch, rename or delete a profile ("save my setup as work" / "switch to my work profile").
4
+ argument-hint: "[status | list | cost | disable|enable <type> <name>... | undo | profile | doctor | update | open | close]"
4
5
  ---
5
6
  <!-- agent-toggle shim: written by `agent-toggle install-shims`; re-running it overwrites this file -->
6
7
 
@@ -16,7 +17,7 @@ agent-toggle <command> --json
16
17
  ```
17
18
  If `agent-toggle` is not on PATH, ask the user to install it (`uv tool install agent-toggle`); do not guess where a checkout lives.
18
19
 
19
- Commands: `status` (run first), `doctor` (read-only health check), `list [type]`,
20
+ Commands: `status` (run first), `doctor` (read-only health check), `update` (checks for a newer release and lists the commands that update it; run none yourself, relay them), `list [type]`,
20
21
  `cost [--type T]` (startup token estimates, biggest first), `disable <type> <name>...` (type `all`: every type with that name),
21
22
  `enable <type> <name>...`, `enable --all`, `undo` (reverse the last batch),
22
23
  `profile save|apply|diff|delete <name>`, `profile rename <old> <new>`, `profile list` (see Profiles), `migrate`; add `--harness claude|codex|grok|opencode|openclaw|copilot|vibe|devin|agy`
@@ -20,7 +20,7 @@ You are running in `__HARNESS__`: pass `--harness __HARNESS__` for its own items
20
20
  (the default is `claude`). Other harnesses: `--harness claude|codex|grok|opencode|openclaw|copilot|vibe|devin|agy`.
21
21
 
22
22
  Commands: `status` (run first; shows which types this harness supports),
23
- `doctor` (read-only health check), `list [type]`, `cost [--type T]` (startup
23
+ `doctor` (read-only health check), `update` (checks for a newer release and lists the commands that update it; run none yourself, relay them), `list [type]`, `cost [--type T]` (startup
24
24
  token estimates, biggest first), `disable <type> <name>...` (type `all`: every type with that name), `enable <type> <name>...`,
25
25
  `enable --all`, `undo` (reverse the last batch), `profile save|apply|diff|delete <name>`,
26
26
  `profile rename <old> <new>`, `profile list` (see Profiles); add `--dry-run` to preview. `<type>` = `skill` / `agent` /
@@ -259,6 +259,15 @@ def log(action: str, type_: str, name: str, result: str, detail: str = "", *,
259
259
  fh.write(json.dumps(rec, ensure_ascii=False) + "\n")
260
260
 
261
261
 
262
+ def signal_reload(rows: list[dict]) -> None:
263
+ """`ui` runs in a terminal of its own, so it cannot reload a Claude Code session: it leaves the claude rows it
264
+ changed here, and every session running the agent-toggle mod polls the file and reloads them (mod/hooks/register.tsx)."""
265
+ done = [r for r in rows if r["harness"] == "claude" and r["status"] == "ok" and r["action"] in ("disable", "enable")]
266
+ if done:
267
+ fs.private_dir(fs.state_dir())
268
+ fs.atomic_write(fs.reload_file(), json.dumps({"batch": BATCH, "results": done}, ensure_ascii=False))
269
+
270
+
262
271
  def migrate(state: dict) -> int:
263
272
  """Import ~/.claude-toggle state so existing rollbacks keep working."""
264
273
  legacy_state_dir, backup_dir = fs.legacy_state_dir(), fs.backup_dir()
@@ -143,8 +143,10 @@ def check(
143
143
  timeout: float = DEFAULT_TIMEOUT,
144
144
  now: float | None = None,
145
145
  fetch: Callable[[str, float], str] | None = None,
146
+ ignore_skip: bool = False,
146
147
  ) -> UpdateAvailable | None:
147
- """Return an update when the latest release is newer than *current*, else None."""
148
+ """Return an update when the latest release is newer than *current*, else None.
149
+ *ignore_skip* reports a "skip this version" release too (an explicit ask)."""
148
150
  current_parts = _numeric_tuple(current)
149
151
  if current_parts is None:
150
152
  return None
@@ -166,7 +168,7 @@ def check(
166
168
  # offline costs one timeout per TTL, not one per command.
167
169
  latest = fetched or latest
168
170
  _save_cache(cache_path, stamp, latest)
169
- if not isinstance(latest, str) or latest == _valid_version(cached.get("skipped")):
171
+ if not isinstance(latest, str) or (not ignore_skip and latest == _valid_version(cached.get("skipped"))):
170
172
  return None
171
173
  latest_parts = _numeric_tuple(latest)
172
174
  if latest_parts is None or latest_parts <= current_parts:
@@ -18,12 +18,16 @@ from collections.abc import Iterable, Iterator
18
18
  from pathlib import Path
19
19
 
20
20
  from . import __version__, fs, settings, update_check
21
+ from .backends import plugin_cli
21
22
  from .i18n import t
22
23
  from .output import use_color
23
24
  from .ui import theme
24
25
 
25
26
  DIST = "agent-toggle"
26
- UPGRADE = ["uv", "tool", "upgrade", DIST]
27
+ PLUGIN = "agent-toggle@agent-toggle"
28
+ # shortcut: pipx is spotted by "pipx" in the venv path (its default homes); a custom PIPX_HOME gets uv
29
+ UPGRADE = (["pipx", "upgrade", DIST] if "pipx" in Path(sys.prefix).parts
30
+ else ["uv", "tool", "upgrade", DIST])
27
31
  RELEASE_NOTES = "https://github.com/weskao/agent-toggle/releases/tag/v%s"
28
32
  ANSWERS = (update_check.UPDATE_NOW, update_check.SKIP, update_check.SKIP_VERSION)
29
33
 
@@ -36,6 +40,16 @@ def _clean(text: str) -> str:
36
40
  return _CONTROL.sub("", text)
37
41
 
38
42
 
43
+ def upgrade_steps() -> list[list[str]]:
44
+ """Upgrade the CLI, then the Claude Code plugin when it is installed, then refresh the shims."""
45
+ steps = [UPGRADE]
46
+ # bare "claude": what a person pastes; offer()'s run() resolves it, as claude is often off PATH
47
+ if plugin_cli.claude_bin() and any(p["id"] == PLUGIN for p in plugin_cli.list_plugins(lambda _msg: None)):
48
+ steps += [["claude", "plugin", "marketplace", "update", DIST],
49
+ ["claude", "plugin", "update", PLUGIN]]
50
+ return [*steps, [DIST, "install-shims"]]
51
+
52
+
39
53
  def cache_path() -> Path:
40
54
  return fs.state_dir() / "update-check.json"
41
55
 
@@ -64,9 +78,10 @@ def _available(found: update_check.UpdateAvailable) -> str:
64
78
 
65
79
 
66
80
  def hint(found: update_check.UpdateAvailable) -> str:
67
- """The `ask` off a terminal (CI, pipes, --json): two plain lines, never a block."""
81
+ """The `ask` off a terminal (CI, pipes, --json): the headline, then one command per line."""
68
82
  print(_available(found), file=sys.stderr)
69
- print(" " + " ".join(UPGRADE), file=sys.stderr)
83
+ for step in upgrade_steps():
84
+ print(" " + " ".join(step), file=sys.stderr)
70
85
  return update_check.SKIP
71
86
 
72
87
 
@@ -182,20 +197,44 @@ def offer(started: update_check.Started | None, *, json_mode: bool = False,
182
197
  stderr only and never raises, so neither stdout nor the exit code can change."""
183
198
  if started is None:
184
199
  return None
185
- if is_interactive() and not json_mode:
200
+ # Windows cannot replace the agent-toggle.exe that is running: print the commands, run none
201
+ if is_interactive() and not json_mode and not fs.WIN:
186
202
  def asker(found):
187
203
  return ask(found, color_mode)
188
204
  else:
189
205
  asker = hint
190
- def run(cmd, **kw): # uv's output must never reach stdout
191
- return subprocess.run(cmd, stdout=sys.stderr, **kw)
192
- answer = update_check.offer(started, asker, cache_path=cache_path(), upgrade=UPGRADE, run=run)
206
+ def run(cmd, **kw): # their output must never reach stdout; no step may prompt
207
+ if cmd[0] == "claude":
208
+ cmd = [plugin_cli.claude_bin() or "claude", *cmd[1:]]
209
+ return subprocess.run(cmd, stdout=sys.stderr, stdin=subprocess.DEVNULL, **kw)
210
+ steps: list[list[str]] = []
211
+ def upgrade(_found):
212
+ steps.extend(upgrade_steps())
213
+ return steps
214
+ answer = update_check.offer(started, asker, cache_path=cache_path(), upgrade=upgrade, run=run)
193
215
  if answer == update_check.UPGRADE_FAILED:
194
216
  try:
195
217
  mark = theme.ansi("warning", "⚠" if theme.get_glyphs(sys.stderr).unicode else "!",
196
218
  use_color(sys.stderr, color_mode))
197
219
  print(mark + " " + t("update.failed", "upgrade did not finish — run it yourself: %s",
198
- " ".join(UPGRADE)), file=sys.stderr)
220
+ "\n ".join(map(" ".join, steps or [UPGRADE]))), file=sys.stderr)
199
221
  except Exception: # noqa: BLE001, S110 - a hint must never fail the command
200
222
  pass
201
223
  return answer
224
+
225
+
226
+ def cmd_update(out) -> None:
227
+ """`update`: check PyPI now (cache and "skipped" ignored), show what would be updated and
228
+ the commands for it. Runs none of them: the picker's Update now does that."""
229
+ found = update_check.check(DIST, __version__, cache_path=cache_path(), ttl_seconds=0,
230
+ timeout=5.0, ignore_skip=True)
231
+ if found is None:
232
+ out.row(None, "cli", DIST, "update", "ok", f"no newer release found (you have {__version__})",
233
+ current=__version__, latest=None, commands=[])
234
+ return
235
+ steps = [" ".join(s) for s in upgrade_steps()]
236
+ out.row(None, "cli", DIST, "update", "ok", f"{found.current} -> {found.latest}", show=False,
237
+ current=found.current, latest=found.latest, commands=steps)
238
+ out.say(_available(found) + ". Run, in order:")
239
+ for step in steps:
240
+ out.say(" " + step)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-toggle
3
- Version: 0.9.0
3
+ Version: 0.11.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
@@ -32,6 +32,8 @@ Dynamic: license-file
32
32
 
33
33
  # agent-toggle
34
34
 
35
+ PyPI: <https://pypi.org/project/agent-toggle/>
36
+
35
37
  Temporarily disable and restore AI-agent resources — skills, agents, commands,
36
38
  rules, plugins (mods included), MCP servers — across Claude Code, Codex, Grok CLI, OpenCode,
37
39
  OpenClaw, Copilot, Vibe, Devin and Antigravity — and show what each one costs at
@@ -193,6 +195,7 @@ agent-toggle <command> [args] # or: python3 agent_toggle.py <command> [
193
195
  | `status` | health check: harnesses found, types each supports, parked counts, gitignore, untracked parked items, stale live twins, shared dirs, and state entries whose parked item is gone (a `stale` row with the fix command; read-only, still exit `0`) |
194
196
  | `list [type]` | what is currently disabled (`--project <dir>` filters to one project) |
195
197
  | `cost [--type T]` | estimated startup tokens per item, biggest first (read-only; `--harness H` filters; `--project <dir>` prices a repo's `.claude/` and `.mcp.json` instead of user scope) |
198
+ | `update` | check PyPI now (ignores the cache and a skipped version) and print the commands that update the CLI, the Claude Code plugin (when installed) and the shims; runs none of them. `/agent-toggle update` in Claude Code does the same; opening the pane toasts a newer release once |
196
199
  | `install-shims` | write the skill shim into every installed harness that is on in the settings (an off one is skipped unless named with `--harness`); refuses to overwrite a file it did not write (`--dry-run` shows the plan); reports stale legacy `<dir>-disabled/` lines in a harness-home `.gitignore` (`--gitignore` removes them, `--no-gitignore` skips the check); on a terminal, then asks y/n for each problem `doctor` can fix (see [Fixing](#fixing)) |
197
200
  | `disable <type> <name>...` | park one or more items (`--dry-run` shows the plan; `--project <dir>` for a repo's own `.claude/` and `.mcp.json`) |
198
201
  | `enable <type> <name>...` | put them back (`--dry-run` shows the plan; `--project <dir>` likewise) |
@@ -367,7 +370,8 @@ off under `--dry-run`. Esc closes the prompt.
367
370
  Nothing happens while the picker is open. Changes are staged, the screen is
368
371
  torn down, and only then do the real operations run, so their output (which
369
372
  companion files moved, which were kept because they are shared) is readable
370
- instead of fighting curses for the terminal.
373
+ instead of fighting curses for the terminal. Claude Code sessions running the
374
+ [pane mod](#claude-code-pane-mod) pick up the Claude changes within 5 s and reload them.
371
375
 
372
376
  Built on stdlib `curses`, so there is nothing to install on macOS and Linux
373
377
  (on Windows, `pip install "agent-toggle[windows]"` pulls `windows-curses`).
@@ -469,10 +473,15 @@ before the command and is offered after it, on every exit path; it overlaps the
469
473
 
470
474
  - **On a terminal** (stdin and stderr are TTYs, no `--json`): a panel on stderr, "agent-toggle X
471
475
  is available (you have Y)", with `Update now`, `Skip` and `Skip until next version`, a release
472
- notes link, and `↑` `↓` `Enter` `q`. `Update now` runs `uv tool upgrade agent-toggle`; if that
473
- fails you get a yellow warning with the command to run yourself.
474
- - **Off a terminal**: two stderr lines, `agent-toggle X is available (you have Y)` and
475
- ` uv tool upgrade agent-toggle`.
476
+ notes link, and `↑` `↓` `Enter` `q`. `Update now` runs, in order and stopping at the first
477
+ failure: `uv tool upgrade agent-toggle` (`pipx upgrade agent-toggle` for a pipx install), then,
478
+ when the Claude Code plugin is installed, `claude plugin marketplace update agent-toggle` and
479
+ `claude plugin update agent-toggle@agent-toggle`, then `agent-toggle install-shims`. A failure
480
+ gives a yellow warning listing the commands to run yourself.
481
+ - **Windows**: a running `agent-toggle.exe` cannot be replaced, so there is no `Update now`; the
482
+ commands are printed instead (as below), to run once agent-toggle has exited.
483
+ - **Off a terminal** (or `--json`): a stderr line, `agent-toggle X is available (you have Y)`,
484
+ then those commands, one per line.
476
485
  - It never changes the exit code or stdout, so `--json` output stays one document.
477
486
 
478
487
  It is one `GET https://pypi.org/pypi/agent-toggle/json` on a background thread with a 0.8 s
@@ -598,7 +607,8 @@ visible rather than silent.
598
607
  hand-rolled serializer would mangle comments — a text slice is lossless and
599
608
  much less code.
600
609
 
601
- MCP changes need a **new session** to take effect.
610
+ MCP changes need a **new session** to take effect, except in a Claude Code session running the
611
+ agent-toggle mod, which runs `/mcp disable|enable <name>` for you (see the mod section).
602
612
 
603
613
  ### Claude MCP scopes
604
614
 
@@ -846,6 +856,7 @@ bookkeeping.
846
856
  | `state.json` | current disabled list (schema v3; atomic write, mode `0600`), saved before each flag write or dir move with the op in flight under an optional `pending` key, and after each MCP / plugin item |
847
857
  | `lock` | held by `disable` / `enable` / `enable --all` / `undo` / `profile apply` / `migrate` / `ui` for the whole batch; a second run waits 5 s then exits `3` (stale after 10 min *and* its PID is gone; a live batch refreshes it per item; the error says when its PID is gone, i.e. a killed run left it) |
848
858
  | `log.jsonl` | one line per operation (mode `0600`), see below |
859
+ | `reload.json` | the Claude rows the last `ui` Enter changed, rewritten each time; open Claude Code sessions running the mod poll it and reload them |
849
860
  | `mcp-backups/` | `<harness>__<server>.json`, or `<sha8>__<harness>__<server>.json` for a project `.mcp.json` (mode `0600` -- may hold auth headers) |
850
861
  | `companions/` | parked exclusive helper files |
851
862
  | `parked/user/<owner>/<dir>/` | user-scope items, e.g. `parked/user/claude/skills/<name>` (`<owner>` = the harness whose home holds the dir; a dir shared by several harnesses parks once, under its owner); an absolute OpenCode `skills.paths` dir parks under `parked/user/opencode/ext-<sha8>/` (`<sha8>` of its resolved path). Nothing is ever parked inside a harness home |
@@ -948,7 +959,8 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
948
959
  - It looks like the `ui` picker: a title with the live-token total, a search line, rows under
949
960
  type headings with counts, green `●` live / yellow `○` parked, tokens and a bar per row (a
950
961
  parked row shows `(N)`, what restoring it would load), the cursor row in reverse video, and a
951
- key-chip footer. The list scrolls with the cursor.
962
+ key-chip footer. The list scrolls with the cursor; the status line shows its position (`7/23`),
963
+ and the first and last list lines count the rows out of view (`↑ 4`, `↓ 12`).
952
964
  - The mouse wheel over the list moves the cursor, as in the terminal picker; no click needed.
953
965
  - Click the list once so it takes keys (Esc hands the keyboard back to the prompt). Then `↑` `↓`
954
966
  `PgUp` `PgDn` `Home` `End` (also `Ctrl-P` / `Ctrl-N`) move; `Space` or `Enter` toggles the row,
@@ -958,8 +970,8 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
958
970
  ...), like `disable all <name>`; `Ctrl-U` clears the search. A click moves the cursor.
959
971
  - A toggle applies at once (no staging, no Enter to apply); the toast names the target and the state
960
972
  with an emoji and a bracketed tag (no colour codes, so nothing garbles; on the Linux console and the
961
- legacy Windows console, where emoji draw as boxes, the tag alone): `✅ [ENABLED] skill: foo`, `⛔ [DISABLED] mod: bar (takes effect in a new
962
- session)`, `⛔ [DISABLED] all: foo` after `n`, or `❌ [FAILED] disable skill: foo - <error>`.
973
+ legacy Windows console, where emoji draw as boxes, the tag alone): `✅ [ENABLED] skill: foo`, `⛔ [DISABLED] mod: bar (/reload-plugins)`,
974
+ `⛔ [DISABLED] rule: baz (takes effect in a new session)`, `⛔ [DISABLED] all: foo` after `n`, or `❌ [FAILED] disable skill: foo - <error>`.
963
975
  The row flips the moment you press (and flips back
964
976
  if the CLI fails); the list reloads in the background. While a toggle runs, further keys are ignored.
965
977
  - Opening the pane shows the same braille spinner and `Loading resources…` as `agent-toggle ui`
@@ -970,6 +982,15 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
970
982
  `undo`, `list`, `status`, `cost` or `doctor` runs the CLI instead of opening the pane and prints
971
983
  one line per result row: `/agent-toggle disable skill foo bar` (it adds `--json`, and
972
984
  `--harness claude` unless you pass `--harness`). A toggle reloads an open pane.
985
+ - Inside Claude Code the mod then applies the change to the running session by queueing the slash
986
+ command for you, once per kind: skills and commands `/reload-skills`, plugins (mods included)
987
+ `/reload-plugins`, an MCP server `/mcp disable|enable <name>`. The toast names it, e.g.
988
+ `⛔ [DISABLED] skill: foo (/reload-skills)`. Agents and rules have no reload and still say
989
+ `takes effect in a new session`. If Claude refuses a queued command, a toast says so.
990
+ - `agent-toggle ui` in another terminal applies on Enter and writes the Claude rows it changed to
991
+ `~/.agent-toggle/reload.json`. Every open session running the mod checks that file's mtime every
992
+ 5 s (one `stat`) and queues the same reloads, toasting `agent-toggle ui: /reload-skills`. A file
993
+ older than the session is ignored.
973
994
  - Needs `agent-toggle` on `PATH`.
974
995
  - On Windows `agent-toggle` must resolve to a real executable on `PATH` (a pip or uv install
975
996
  provides `agent-toggle.exe`).
@@ -78,6 +78,7 @@ class ApplyPlanSeamTest(SandboxCase):
78
78
  self.assertEqual(plan, [ops.Op("claude", "skill", "disable", "demo-skill")])
79
79
  self.assertEqual(spy.call_args.kwargs["batch"], store.BATCH)
80
80
  self.assertFalse((self.home / "skills/demo-skill").exists())
81
+ self.assertFalse(fs.reload_file().exists()) # only `ui` signals: the mod reloads its own runs itself
81
82
 
82
83
  def test_cmd_ui_goes_through_apply_plan(self) -> None:
83
84
  try:
@@ -94,6 +95,10 @@ class ApplyPlanSeamTest(SandboxCase):
94
95
  [ops.Op("claude", "skill", "disable", "demo-skill")])
95
96
  self.assertFalse((self.home / "skills/demo-skill").exists())
96
97
  self.assertIn("claude:skill:demo-skill", store.load_state()["disabled"])
98
+ # Enter applied it: open Claude Code sessions (the mod polls this file) reload the skill
99
+ sig = json.loads(fs.reload_file().read_text(encoding="utf-8"))
100
+ self.assertEqual([(r["type"], r["name"], r["action"], r["status"]) for r in sig["results"]],
101
+ [("skill", "demo-skill", "disable", "ok")])
97
102
 
98
103
  def test_type_without_a_mechanism_is_an_unsupported_row(self) -> None:
99
104
  out = Result()
@@ -176,7 +181,8 @@ class UpdateCheckTest(CliCase):
176
181
  def test_hint_on_stderr_off_a_tty(self) -> None:
177
182
  rc, out, err = self.run_cli("status")
178
183
  self.assertEqual(rc, 0)
179
- self.assertEqual(err.splitlines()[-2:], [HINT, " uv tool upgrade agent-toggle"])
184
+ self.assertEqual(err.splitlines()[-3:], [HINT, " uv tool upgrade agent-toggle",
185
+ " agent-toggle install-shims"])
180
186
  self.assertNotIn("99.0.0", out)
181
187
 
182
188
  def test_opt_out_by_env_and_by_setting(self) -> None:
@@ -228,10 +234,10 @@ class UpdateCheckTest(CliCase):
228
234
  rc, out, err = self.run_cli("--version") # runs no other subprocess
229
235
  self.assertEqual((rc, out.strip()), (0, f"agent-toggle {__version__}"))
230
236
  run.assert_called_once_with(["uv", "tool", "upgrade", "agent-toggle"], check=False,
231
- stdout=mock.ANY)
237
+ stdout=mock.ANY, stdin=subprocess.DEVNULL)
232
238
  self.assertIn("did not finish", run.call_args.kwargs["stdout"].getvalue()) # = stderr
233
- self.assertIn("upgrade did not finish — run it yourself: uv tool upgrade agent-toggle",
234
- err)
239
+ self.assertIn("upgrade did not finish — run it yourself: uv tool upgrade agent-toggle"
240
+ "\n agent-toggle install-shims", err)
235
241
 
236
242
  def test_tty_skip_version_and_ctrl_c(self) -> None:
237
243
  update_prompt.is_interactive.return_value = True
@@ -250,7 +250,7 @@ class SpellingTest(CliCase):
250
250
  ["profile", "list"], ["profile", "diff", "work"],
251
251
  ["profile", "apply", "work", "--dry-run"],
252
252
  ["undo", "--dry-run"], ["enable", "--all", "--dry-run"],
253
- ["doctor"], ["doctor", "--harness", "claude"],
253
+ ["doctor"], ["doctor", "--harness", "claude"], ["update"],
254
254
  ["disable", "skill", "demo-skill", "--project", str(proj), "--dry-run"],
255
255
  )
256
256
  for argv in cases:
@@ -0,0 +1,78 @@
1
+ """The update prompt flushes typeahead around cbreak mode."""
2
+ from __future__ import annotations
3
+
4
+ import io
5
+ import sys
6
+ import unittest
7
+ from unittest import mock
8
+
9
+ from agent_toggle import update_check, update_prompt
10
+ from agent_toggle.ui import theme
11
+
12
+
13
+ class ReleaseNotesTest(unittest.TestCase):
14
+ def test_a_v_prefixed_version_is_not_doubled_in_the_url(self) -> None:
15
+ for latest in ("10.0", "v10.0"):
16
+ found = update_check.UpdateAvailable(latest=latest, current="1.0")
17
+ text = "\n".join(update_prompt._lines(found, 1, False, theme.ASCII))
18
+ self.assertIn("/releases/tag/v10.0", text, latest)
19
+
20
+
21
+ class UpgradeStepsTest(unittest.TestCase):
22
+ def steps(self, claude, plugins):
23
+ with mock.patch.object(update_prompt.plugin_cli, "claude_bin", return_value=claude), \
24
+ mock.patch.object(update_prompt.plugin_cli, "list_plugins",
25
+ return_value=[{"id": i} for i in plugins]):
26
+ return update_prompt.upgrade_steps()
27
+
28
+ def test_the_plugin_is_updated_only_when_it_is_installed(self) -> None:
29
+ cli, shims = update_prompt.UPGRADE, ["agent-toggle", "install-shims"]
30
+ self.assertEqual(self.steps("claude", ["agent-toggle@agent-toggle"]), [
31
+ cli, ["claude", "plugin", "marketplace", "update", "agent-toggle"],
32
+ ["claude", "plugin", "update", "agent-toggle@agent-toggle"], shims])
33
+ self.assertEqual(self.steps("claude", ["other@x"]), [cli, shims])
34
+ self.assertEqual(self.steps(None, []), [cli, shims])
35
+
36
+
37
+ class CmdUpdateTest(unittest.TestCase):
38
+ def run_update(self, latest):
39
+ out = mock.Mock()
40
+ found = update_check.UpdateAvailable("0.1", latest) if latest else None
41
+ with mock.patch.object(update_check, "check", return_value=found) as check, \
42
+ mock.patch.object(update_prompt, "upgrade_steps", return_value=[["a", "b"], ["c"]]), \
43
+ mock.patch.object(update_prompt, "cache_path", return_value="x"):
44
+ update_prompt.cmd_update(out)
45
+ return out, check
46
+
47
+ def test_a_newer_release_lists_every_command_and_ignores_cache_and_skip(self) -> None:
48
+ out, check = self.run_update("0.2")
49
+ row = out.row.call_args.kwargs
50
+ self.assertEqual((row["latest"], row["commands"]), ("0.2", ["a b", "c"]))
51
+ self.assertEqual((check.call_args.kwargs["ttl_seconds"], check.call_args.kwargs["ignore_skip"]), (0, True))
52
+
53
+ def test_nothing_newer_lists_no_command(self) -> None:
54
+ out, _ = self.run_update(None)
55
+ self.assertEqual(out.row.call_args.kwargs["commands"], [])
56
+
57
+
58
+ class FlushTest(unittest.TestCase):
59
+ def test_input_is_flushed_after_entering_and_after_leaving_cbreak(self) -> None:
60
+ termios = mock.MagicMock()
61
+ calls: list[str] = []
62
+ termios.tcgetattr.return_value = []
63
+ termios.tcflush.side_effect = lambda *_a: calls.append("flush")
64
+ termios.tcsetattr.side_effect = lambda *_a: calls.append("restore")
65
+ tty = mock.MagicMock()
66
+ tty.setcbreak.side_effect = lambda *_a: calls.append("cbreak")
67
+ stdin = mock.MagicMock()
68
+ stdin.fileno.return_value = 0
69
+ found = update_check.UpdateAvailable(current="1.0", latest="2.0")
70
+ with (mock.patch.dict(sys.modules, {"termios": termios, "tty": tty}),
71
+ mock.patch.object(sys, "stdin", stdin), mock.patch.object(sys, "stderr", io.StringIO()),
72
+ mock.patch.object(update_prompt, "_posix_keys", lambda _fd: iter(["quit"]))):
73
+ self.assertEqual(update_prompt.ask(found), update_check.SKIP)
74
+ self.assertEqual(calls, ["cbreak", "flush", "restore", "flush"])
75
+
76
+
77
+ if __name__ == "__main__":
78
+ unittest.main()
@@ -1,41 +0,0 @@
1
- """The update prompt flushes typeahead around cbreak mode."""
2
- from __future__ import annotations
3
-
4
- import io
5
- import sys
6
- import unittest
7
- from unittest import mock
8
-
9
- from agent_toggle import update_check, update_prompt
10
- from agent_toggle.ui import theme
11
-
12
-
13
- class ReleaseNotesTest(unittest.TestCase):
14
- def test_a_v_prefixed_version_is_not_doubled_in_the_url(self) -> None:
15
- for latest in ("10.0", "v10.0"):
16
- found = update_check.UpdateAvailable(latest=latest, current="1.0")
17
- text = "\n".join(update_prompt._lines(found, 1, False, theme.ASCII))
18
- self.assertIn("/releases/tag/v10.0", text, latest)
19
-
20
-
21
- class FlushTest(unittest.TestCase):
22
- def test_input_is_flushed_after_entering_and_after_leaving_cbreak(self) -> None:
23
- termios = mock.MagicMock()
24
- calls: list[str] = []
25
- termios.tcgetattr.return_value = []
26
- termios.tcflush.side_effect = lambda *_a: calls.append("flush")
27
- termios.tcsetattr.side_effect = lambda *_a: calls.append("restore")
28
- tty = mock.MagicMock()
29
- tty.setcbreak.side_effect = lambda *_a: calls.append("cbreak")
30
- stdin = mock.MagicMock()
31
- stdin.fileno.return_value = 0
32
- found = update_check.UpdateAvailable(current="1.0", latest="2.0")
33
- with (mock.patch.dict(sys.modules, {"termios": termios, "tty": tty}),
34
- mock.patch.object(sys, "stdin", stdin), mock.patch.object(sys, "stderr", io.StringIO()),
35
- mock.patch.object(update_prompt, "_posix_keys", lambda _fd: iter(["quit"]))):
36
- self.assertEqual(update_prompt.ask(found), update_check.SKIP)
37
- self.assertEqual(calls, ["cbreak", "flush", "restore", "flush"])
38
-
39
-
40
- if __name__ == "__main__":
41
- unittest.main()
File without changes
File without changes