agent-toggle 0.16.1__tar.gz → 0.17.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.16.1 → agent_toggle-0.17.0}/PKG-INFO +20 -14
  2. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/README.md +19 -13
  3. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/__init__.py +1 -1
  4. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/backends/flag_json.py +1 -1
  5. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/backends/plugin_cli.py +1 -1
  6. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/cli.py +1 -1
  7. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/cost.py +1 -1
  8. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/doctor.py +2 -1
  9. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/harnesses.py +1 -1
  10. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/ui/config_menu.py +1 -1
  11. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/ui/picker.py +1 -1
  12. agent_toggle-0.17.0/agent_toggle/update_check.py +209 -0
  13. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/update_prompt.py +12 -9
  14. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle.egg-info/PKG-INFO +20 -14
  15. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle.egg-info/SOURCES.txt +1 -0
  16. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/pyproject.toml +5 -1
  17. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_backends.py +1 -1
  18. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_cli.py +8 -8
  19. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_cost.py +2 -1
  20. agent_toggle-0.17.0/tests/test_mod_version.py +20 -0
  21. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_picker.py +2 -2
  22. agent_toggle-0.17.0/tests/test_update_check.py +240 -0
  23. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_update_prompt.py +12 -0
  24. agent_toggle-0.16.1/agent_toggle/update_check.py +0 -361
  25. agent_toggle-0.16.1/tests/test_update_check.py +0 -423
  26. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/LICENSE +0 -0
  27. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/__main__.py +0 -0
  28. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/backends/__init__.py +0 -0
  29. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/backends/mcp_json.py +0 -0
  30. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/backends/mcp_toml.py +0 -0
  31. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/companions.py +0 -0
  32. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/completions.py +0 -0
  33. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/config.py +0 -0
  34. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/fs.py +0 -0
  35. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/help.py +0 -0
  36. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/i18n.py +0 -0
  37. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/locales/__init__.py +0 -0
  38. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/locales/zh_tw_cli.py +0 -0
  39. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/locales/zh_tw_common.py +0 -0
  40. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/locales/zh_tw_config.py +0 -0
  41. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/locales/zh_tw_picker.py +0 -0
  42. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/mechanisms.py +0 -0
  43. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/ops.py +0 -0
  44. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/output.py +0 -0
  45. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/profiles.py +0 -0
  46. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/settings.py +0 -0
  47. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/shims/claude.md.tmpl +0 -0
  48. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/shims/generic.md.tmpl +0 -0
  49. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/spinner.py +0 -0
  50. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/store.py +0 -0
  51. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/toml_check.py +0 -0
  52. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/ui/__init__.py +0 -0
  53. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/ui/logo.py +0 -0
  54. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/ui/menu.py +0 -0
  55. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/ui/model.py +0 -0
  56. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/ui/theme.py +0 -0
  57. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle/undo.py +0 -0
  58. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle.egg-info/dependency_links.txt +0 -0
  59. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle.egg-info/entry_points.txt +0 -0
  60. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle.egg-info/requires.txt +0 -0
  61. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/agent_toggle.egg-info/top_level.txt +0 -0
  62. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/setup.cfg +0 -0
  63. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_alias.py +0 -0
  64. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_all_type.py +0 -0
  65. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_cli_surface.py +0 -0
  66. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_color.py +0 -0
  67. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_companions.py +0 -0
  68. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_completions.py +0 -0
  69. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_config.py +0 -0
  70. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_config_menu.py +0 -0
  71. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_conformance.py +0 -0
  72. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_containment.py +0 -0
  73. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_crash.py +0 -0
  74. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_doctor.py +0 -0
  75. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_encoding.py +0 -0
  76. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_fs.py +0 -0
  77. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_harnesses.py +0 -0
  78. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_help.py +0 -0
  79. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_i18n.py +0 -0
  80. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_install.py +0 -0
  81. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_logo.py +0 -0
  82. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_mechanisms.py +0 -0
  83. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_menu.py +0 -0
  84. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_mods.py +0 -0
  85. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_platform.py +0 -0
  86. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_profiles.py +0 -0
  87. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_project.py +0 -0
  88. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_safety.py +0 -0
  89. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_same_name.py +0 -0
  90. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_secret_audit.py +0 -0
  91. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_settings.py +0 -0
  92. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_store.py +0 -0
  93. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_theme.py +0 -0
  94. {agent_toggle-0.16.1 → agent_toggle-0.17.0}/tests/test_undo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-toggle
3
- Version: 0.16.1
3
+ Version: 0.17.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
@@ -168,6 +168,9 @@ installs above, type this at the prompt of a Claude Code terminal session (2.1.2
168
168
  ```
169
169
 
170
170
  Answer `y` to add the marketplace, pick a scope (user is the default), then run `/agent-toggle`.
171
+ The same mod is also listed in the `weskao-agent-plugins` marketplace
172
+ (`claude plugin marketplace add weskao/weskao-agent-plugins`, then
173
+ `claude plugin install agent-toggle@weskao-agent-plugins`); install it from one of the two, not both.
171
174
 
172
175
  See [Claude Code pane mod](#claude-code-pane-mod).
173
176
 
@@ -260,7 +263,7 @@ Flags accepted by every command, before or after the subcommand:
260
263
 
261
264
  Extra row fields: `list` rows carry `at`, `mechanism`, `companions`; `cost` rows
262
265
  carry `enabled`, `tokens`, `would_save`, `chars`, and the final `total` row
263
- carries `total_tokens`, `saved_tokens`, `formula`. Rows for a
266
+ carries `total_tokens`, `saved_tokens`, `formula`, `version`. Rows for a
264
267
  directory shared with another harness (see below) carry `shared_with`, the other
265
268
  harnesses that read it: `disable` / `enable` rows relative to the harness you
266
269
  asked, plus `owner`, the harness whose park dir and state entry hold the item;
@@ -498,7 +501,7 @@ invalid value in either is ignored with one warning on stderr.
498
501
 
499
502
  ## Update check
500
503
 
501
- On every invocation (unless disabled) agent-toggle checks PyPI for a newer release. It starts
504
+ On every invocation (unless disabled) agent-toggle checks the repository's release tags for a newer release. It starts
502
505
  before the command and is offered after it, on every exit path; it overlaps the command and waits at most 0.8 s afterwards.
503
506
  `update` and `completion` skip it.
504
507
 
@@ -513,9 +516,9 @@ before the command and is offered after it, on every exit path; it overlaps the
513
516
  then ` agent-toggle update`.
514
517
  - It never changes the exit code or stdout, so `--json` output stays one document.
515
518
 
516
- It is one `GET https://pypi.org/pypi/agent-toggle/json` on a background thread with a 0.8 s
517
- timeout, cached for 10 minutes in `~/.agent-toggle/update-check.json`. Nothing is sent beyond a
518
- normal HTTP request with the User-Agent `agent-toggle-update-check`. See `SECURITY.md`
519
+ The lookup is update-kit's: one `git ls-remote --tags --refs` of the repository on a background
520
+ thread, cached for 10 minutes in `~/.update-kit/state.json`. Only the version you chose to skip is
521
+ kept in `~/.agent-toggle/update-check.json`. Nothing is sent beyond a normal git request. See `SECURITY.md`
519
522
  for the hardening. To turn it off, set `AGENT_TOGGLE_UPDATE_CHECK=0` or the `update_check` setting
520
523
  to off (the settings menu's "Check for updates").
521
524
 
@@ -527,9 +530,9 @@ tag (`git ls-remote --tags`) and always installs that tag with
527
530
  `uv tool install --force --no-sources git+<repo>@<tag>` (`pipx install --force` for a pipx install).
528
531
  A dev checkout, a branch or commit install, or an unmanaged install is replaced by the tag too
529
532
  (with uv, else pipx); only an install made from exactly that tag is already up to date.
530
- It then runs, with the new binary and in order: when the Claude Code plugin is installed,
531
- `claude plugin marketplace update agent-toggle` and `claude plugin update agent-toggle@agent-toggle`,
532
- then `agent-toggle install-shims`. A failing step prints a warning and makes the exit code `1`.
533
+ It then runs, with the new binary and in order: when the Claude Code plugin is installed (from any
534
+ marketplace), `claude plugin marketplace update <marketplace>` and
535
+ `claude plugin update agent-toggle@<marketplace>`, then `agent-toggle install-shims`. A failing step prints a warning and makes the exit code `1`.
533
536
  It never prompts and writes to stderr only.
534
537
 
535
538
  | Exit | Meaning |
@@ -931,7 +934,7 @@ bookkeeping.
931
934
  | `parked/<sha8>/` | items parked by `--project` (`<sha8>` = first 8 hex of the SHA-1 of the resolved project dir) |
932
935
  | `profiles/` | `<name>.json` profiles (dir `0700`, files `0600`) |
933
936
  | `config.json` | [settings](#settings--config-menu) and the Telegram chat id (mode `0600`, atomic write, unknown keys kept) |
934
- | `update-check.json` | the [update check](#update-check)'s 10-minute cache (mode `0600`, atomic write) |
937
+ | `update-check.json` | the [update check](#update-check)'s skipped version (mode `0600`, atomic write) |
935
938
 
936
939
  A killed run loses at most the one item in flight. If that was a flag write or a
937
940
  dir move (with its companions), its `pending` record (the full entry, a flag's
@@ -1011,8 +1014,9 @@ CLI (`cost --json`, `disable|enable --json`) and works for the claude harness on
1011
1014
  `agent-toggle ui` stays the cross-harness UI.
1012
1015
 
1013
1016
  Install it with `/plugin install agent-toggle --marketplace weskao/agent-toggle` (see
1014
- [Install](#install)); `claude plugin update agent-toggle@agent-toggle` picks up a new release. To
1015
- run your own checkout instead (edits reload as you save):
1017
+ [Install](#install)); `/agent-toggle update` picks up a new release: it runs `agent-toggle update`,
1018
+ which updates the CLI, this mod and the shims together (restart Claude Code to load the new mod).
1019
+ The mod and the CLI share one version. To run your own checkout instead (edits reload as you save):
1016
1020
 
1017
1021
  ```sh
1018
1022
  claude --plugin-dir ./mod # then run /agent-toggle inside Claude
@@ -1144,8 +1148,10 @@ prints a `::notice::` and exits 0, so it never adds a second red X.
1144
1148
  ## Releasing
1145
1149
 
1146
1150
  A release is a git tag `vX.Y.Z` (where `X.Y.Z` must match `agent_toggle.__version__`).
1147
- Bump `version` in `mod/.claude-plugin/plugin.json` to the same value by hand; nothing checks it,
1148
- and without it `claude plugin update` does not hand marketplace installs the new mod.
1151
+ `version` in `mod/.claude-plugin/plugin.json` must be the same value (the mod ships in lockstep;
1152
+ without the bump `claude plugin update` does not hand marketplace installs the new mod).
1153
+ `tests/test_mod_version.py` and the release workflow both fail on a mismatch.
1154
+ The `weskao-agent-plugins` marketplace lists the mod without a version, so it needs no bump there.
1149
1155
  Pushing the tag triggers `.github/workflows/release.yml`:
1150
1156
 
1151
1157
  1. **Build** job checks that the tag matches `__version__`, builds a wheel and
@@ -134,6 +134,9 @@ installs above, type this at the prompt of a Claude Code terminal session (2.1.2
134
134
  ```
135
135
 
136
136
  Answer `y` to add the marketplace, pick a scope (user is the default), then run `/agent-toggle`.
137
+ The same mod is also listed in the `weskao-agent-plugins` marketplace
138
+ (`claude plugin marketplace add weskao/weskao-agent-plugins`, then
139
+ `claude plugin install agent-toggle@weskao-agent-plugins`); install it from one of the two, not both.
137
140
 
138
141
  See [Claude Code pane mod](#claude-code-pane-mod).
139
142
 
@@ -226,7 +229,7 @@ Flags accepted by every command, before or after the subcommand:
226
229
 
227
230
  Extra row fields: `list` rows carry `at`, `mechanism`, `companions`; `cost` rows
228
231
  carry `enabled`, `tokens`, `would_save`, `chars`, and the final `total` row
229
- carries `total_tokens`, `saved_tokens`, `formula`. Rows for a
232
+ carries `total_tokens`, `saved_tokens`, `formula`, `version`. Rows for a
230
233
  directory shared with another harness (see below) carry `shared_with`, the other
231
234
  harnesses that read it: `disable` / `enable` rows relative to the harness you
232
235
  asked, plus `owner`, the harness whose park dir and state entry hold the item;
@@ -464,7 +467,7 @@ invalid value in either is ignored with one warning on stderr.
464
467
 
465
468
  ## Update check
466
469
 
467
- On every invocation (unless disabled) agent-toggle checks PyPI for a newer release. It starts
470
+ On every invocation (unless disabled) agent-toggle checks the repository's release tags for a newer release. It starts
468
471
  before the command and is offered after it, on every exit path; it overlaps the command and waits at most 0.8 s afterwards.
469
472
  `update` and `completion` skip it.
470
473
 
@@ -479,9 +482,9 @@ before the command and is offered after it, on every exit path; it overlaps the
479
482
  then ` agent-toggle update`.
480
483
  - It never changes the exit code or stdout, so `--json` output stays one document.
481
484
 
482
- It is one `GET https://pypi.org/pypi/agent-toggle/json` on a background thread with a 0.8 s
483
- timeout, cached for 10 minutes in `~/.agent-toggle/update-check.json`. Nothing is sent beyond a
484
- normal HTTP request with the User-Agent `agent-toggle-update-check`. See `SECURITY.md`
485
+ The lookup is update-kit's: one `git ls-remote --tags --refs` of the repository on a background
486
+ thread, cached for 10 minutes in `~/.update-kit/state.json`. Only the version you chose to skip is
487
+ kept in `~/.agent-toggle/update-check.json`. Nothing is sent beyond a normal git request. See `SECURITY.md`
485
488
  for the hardening. To turn it off, set `AGENT_TOGGLE_UPDATE_CHECK=0` or the `update_check` setting
486
489
  to off (the settings menu's "Check for updates").
487
490
 
@@ -493,9 +496,9 @@ tag (`git ls-remote --tags`) and always installs that tag with
493
496
  `uv tool install --force --no-sources git+<repo>@<tag>` (`pipx install --force` for a pipx install).
494
497
  A dev checkout, a branch or commit install, or an unmanaged install is replaced by the tag too
495
498
  (with uv, else pipx); only an install made from exactly that tag is already up to date.
496
- It then runs, with the new binary and in order: when the Claude Code plugin is installed,
497
- `claude plugin marketplace update agent-toggle` and `claude plugin update agent-toggle@agent-toggle`,
498
- then `agent-toggle install-shims`. A failing step prints a warning and makes the exit code `1`.
499
+ It then runs, with the new binary and in order: when the Claude Code plugin is installed (from any
500
+ marketplace), `claude plugin marketplace update <marketplace>` and
501
+ `claude plugin update agent-toggle@<marketplace>`, then `agent-toggle install-shims`. A failing step prints a warning and makes the exit code `1`.
499
502
  It never prompts and writes to stderr only.
500
503
 
501
504
  | Exit | Meaning |
@@ -897,7 +900,7 @@ bookkeeping.
897
900
  | `parked/<sha8>/` | items parked by `--project` (`<sha8>` = first 8 hex of the SHA-1 of the resolved project dir) |
898
901
  | `profiles/` | `<name>.json` profiles (dir `0700`, files `0600`) |
899
902
  | `config.json` | [settings](#settings--config-menu) and the Telegram chat id (mode `0600`, atomic write, unknown keys kept) |
900
- | `update-check.json` | the [update check](#update-check)'s 10-minute cache (mode `0600`, atomic write) |
903
+ | `update-check.json` | the [update check](#update-check)'s skipped version (mode `0600`, atomic write) |
901
904
 
902
905
  A killed run loses at most the one item in flight. If that was a flag write or a
903
906
  dir move (with its companions), its `pending` record (the full entry, a flag's
@@ -977,8 +980,9 @@ CLI (`cost --json`, `disable|enable --json`) and works for the claude harness on
977
980
  `agent-toggle ui` stays the cross-harness UI.
978
981
 
979
982
  Install it with `/plugin install agent-toggle --marketplace weskao/agent-toggle` (see
980
- [Install](#install)); `claude plugin update agent-toggle@agent-toggle` picks up a new release. To
981
- run your own checkout instead (edits reload as you save):
983
+ [Install](#install)); `/agent-toggle update` picks up a new release: it runs `agent-toggle update`,
984
+ which updates the CLI, this mod and the shims together (restart Claude Code to load the new mod).
985
+ The mod and the CLI share one version. To run your own checkout instead (edits reload as you save):
982
986
 
983
987
  ```sh
984
988
  claude --plugin-dir ./mod # then run /agent-toggle inside Claude
@@ -1110,8 +1114,10 @@ prints a `::notice::` and exits 0, so it never adds a second red X.
1110
1114
  ## Releasing
1111
1115
 
1112
1116
  A release is a git tag `vX.Y.Z` (where `X.Y.Z` must match `agent_toggle.__version__`).
1113
- Bump `version` in `mod/.claude-plugin/plugin.json` to the same value by hand; nothing checks it,
1114
- and without it `claude plugin update` does not hand marketplace installs the new mod.
1117
+ `version` in `mod/.claude-plugin/plugin.json` must be the same value (the mod ships in lockstep;
1118
+ without the bump `claude plugin update` does not hand marketplace installs the new mod).
1119
+ `tests/test_mod_version.py` and the release workflow both fail on a mismatch.
1120
+ The `weskao-agent-plugins` marketplace lists the mod without a version, so it needs no bump there.
1115
1121
  Pushing the tag triggers `.github/workflows/release.yml`:
1116
1122
 
1117
1123
  1. **Build** job checks that the tag matches `__version__`, builds a wheel and
@@ -1,3 +1,3 @@
1
1
  """agent-toggle: temporarily disable / re-enable AI-agent resources."""
2
2
 
3
- __version__ = "0.16.1"
3
+ __version__ = "0.17.0"
@@ -25,9 +25,9 @@ from __future__ import annotations
25
25
 
26
26
  import json
27
27
  import re
28
+ from collections.abc import Sequence
28
29
  from json.decoder import WHITESPACE, scanstring
29
30
  from pathlib import Path
30
- from typing import Sequence
31
31
 
32
32
  from agent_toggle import fs
33
33
 
@@ -10,8 +10,8 @@ import os
10
10
  import shutil
11
11
  import subprocess
12
12
  import sys
13
+ from collections.abc import Callable
13
14
  from pathlib import Path
14
- from typing import Callable
15
15
 
16
16
  from .. import fs
17
17
 
@@ -298,7 +298,7 @@ def cmd_cost(state: dict, out: Result, harness: str | None = None,
298
298
  if len(shown) > len(items):
299
299
  _say_hidden(out, f"{len(shown) - len(items)} hidden by settings (agent-toggle config)")
300
300
  out.row(harness, type_, None, "cost", "ok", "total", show=False, items=len(items),
301
- total_tokens=live, saved_tokens=saved, formula=cost.FORMULA,
301
+ total_tokens=live, saved_tokens=saved, formula=cost.FORMULA, version=__version__,
302
302
  **({"project": str(proj)} if proj else {}))
303
303
 
304
304
 
@@ -26,10 +26,10 @@ from __future__ import annotations
26
26
  import json
27
27
  import re
28
28
  from collections import Counter
29
+ from collections.abc import Callable
29
30
  from concurrent.futures import ThreadPoolExecutor
30
31
  from dataclasses import dataclass
31
32
  from pathlib import Path
32
- from typing import Callable
33
33
 
34
34
  from . import fs
35
35
  from .backends.flag_json import jsonc_loads
@@ -15,8 +15,9 @@ import filecmp
15
15
  import json
16
16
  import os
17
17
  import shutil
18
+ from collections.abc import Callable
18
19
  from pathlib import Path
19
- from typing import Callable, NamedTuple
20
+ from typing import NamedTuple
20
21
 
21
22
  from . import fs, ops, store
22
23
  from .backends.flag_json import jsonc_loads
@@ -2,10 +2,10 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import os
5
+ from collections.abc import Mapping
5
6
  from dataclasses import dataclass, field
6
7
  from pathlib import Path
7
8
  from types import MappingProxyType
8
- from typing import Mapping
9
9
 
10
10
  from . import fs
11
11
  from .backends import flag_json
@@ -17,8 +17,8 @@ import contextlib
17
17
  import io
18
18
  import sys
19
19
  import time
20
+ from collections.abc import Callable
20
21
  from dataclasses import dataclass
21
- from typing import Callable
22
22
 
23
23
  from .. import __version__, config, doctor, fs, harnesses, i18n, settings, undo
24
24
  from ..i18n import t
@@ -498,7 +498,7 @@ def ask_save(win, v: View, pal: theme.Palette, g: theme.Glyphs,
498
498
  top = min(top, max(0, len(changed) - max(1, page)))
499
499
  row_y = y0 + (2 if bh >= 7 else 1)
500
500
 
501
- def put(y, segments):
501
+ def put(y, segments, x0=x0, inner=inner):
502
502
  theme.draw_segments(win, y, x0, [(g.v, "accent"),
503
503
  *clip(segments, inner, g.ellipsis), (g.v, "accent")], pal)
504
504
 
@@ -0,0 +1,209 @@
1
+ """Update check on top of update-kit, plus the "skip this version" memory.
2
+
3
+ from agent_toggle.update_check import check
4
+ found = check("1.0.0", cache_path=Path.home() / ".agent-toggle" / "update-check.json")
5
+ if found:
6
+ print(f"{found.latest} is available (you have {found.current})")
7
+
8
+ The lookup itself -- ``git ls-remote`` of the release tags, the 10-minute cache in
9
+ ``~/.update-kit/state.json``, the timeout -- is :func:`update_kit.check`; ``latest`` is
10
+ the tag as it returns it (``v1.2.0``). Only the skipped version lives in *cache_path*.
11
+
12
+ To overlap the lookup with the command's own work instead of paying for
13
+ it at exit, start it early and collect the result at the end:
14
+
15
+ started = start("1.0.0", cache_path=...)
16
+ ... # the command's own work
17
+ found = collect(started)
18
+
19
+ To ask the user what to do instead of only hinting, hand :func:`offer` the
20
+ UI as a callback -- it owns what each answer does, the UI only picks one:
21
+
22
+ offer(started, ask, cache_path=..., upgrade=["uv", "tool", "upgrade", "agent-toggle"])
23
+
24
+ where ``ask(found)`` returns ``UPDATE_NOW``, ``SKIP`` or ``SKIP_VERSION``.
25
+ When the upgrade depends on the version or takes several steps, pass a
26
+ callable instead: ``upgrade=lambda found: [[...], [...]]`` runs each command
27
+ in order and stops at the first one that fails.
28
+ ``SKIP_VERSION`` is remembered in *cache_path*; a newer release asks again.
29
+
30
+ Nothing here is allowed to raise to the caller: a missing cache, a bad
31
+ payload, or a dead network all become ``None``.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import json
37
+ import os
38
+ import re
39
+ import subprocess
40
+ import threading
41
+ from collections.abc import Callable, Sequence
42
+ from dataclasses import dataclass, field
43
+ from pathlib import Path
44
+
45
+ __all__ = [
46
+ "DEFAULT_TIMEOUT",
47
+ "REPO",
48
+ "SKIP",
49
+ "SKIP_VERSION",
50
+ "UPDATE_NOW",
51
+ "UPGRADE_FAILED",
52
+ "Started",
53
+ "UpdateAvailable",
54
+ "check",
55
+ "collect",
56
+ "offer",
57
+ "skip",
58
+ "start",
59
+ ]
60
+
61
+ REPO = "weskao/agent-toggle"
62
+ #: How long :func:`collect` waits for the background lookup before giving up on it.
63
+ DEFAULT_TIMEOUT = 0.8
64
+
65
+ #: Answers an ``ask`` callback returns to :func:`offer`.
66
+ UPDATE_NOW = "update-now"
67
+ SKIP = "skip"
68
+ SKIP_VERSION = "skip-version"
69
+ #: What :func:`offer` returns when UPDATE_NOW's command could not run or failed.
70
+ UPGRADE_FAILED = "upgrade-failed"
71
+
72
+ #: What a skipped version may look like before it is trusted: it is read from a file.
73
+ _VERSION_RE = re.compile(r"v?\d{1,6}(\.\d{1,6}){1,3}([a-zA-Z0-9.+-]{0,20})?")
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class UpdateAvailable:
78
+ current: str
79
+ latest: str
80
+
81
+
82
+ def check(current: str, *, cache_path: Path) -> UpdateAvailable | None:
83
+ """Return an update when the latest release is newer than *current* and not the
84
+ version skipped in *cache_path*, else None. Never raises."""
85
+ try:
86
+ import update_kit # lazy: a checkout run or a stale env lacks it
87
+
88
+ latest = update_kit.check(repo=REPO, current=current)
89
+ if not isinstance(latest, str) or _bare(latest) == _bare(_load_cache(cache_path).get("skipped")):
90
+ return None
91
+ return UpdateAvailable(current=current, latest=latest)
92
+ except Exception: # noqa: BLE001 - a hint must never fail the command
93
+ return None
94
+
95
+
96
+ @dataclass
97
+ class Started:
98
+ """A background check in flight; pass to :func:`collect` to get its result."""
99
+
100
+ thread: threading.Thread
101
+ _result: list = field(default_factory=list)
102
+
103
+
104
+ def start(current: str, *, cache_path: Path) -> Started:
105
+ """Run :func:`check` on a daemon thread so it overlaps the caller's own work."""
106
+ result: list = []
107
+
108
+ def run() -> None:
109
+ result.append(check(current, cache_path=cache_path))
110
+
111
+ thread = threading.Thread(target=run, name="agent-toggle-update-check", daemon=True)
112
+ thread.start()
113
+ return Started(thread=thread, _result=result)
114
+
115
+
116
+ def collect(started: Started | None, *, timeout: float = DEFAULT_TIMEOUT) -> UpdateAvailable | None:
117
+ """Wait for a :func:`start`-ed check and return its result. Never raises."""
118
+ if started is None:
119
+ return None
120
+ try:
121
+ # Usually already done: it ran alongside the caller's own work.
122
+ started.thread.join(timeout)
123
+ return started._result[0] if started._result else None
124
+ except Exception: # noqa: BLE001 - a hint must never fail the command
125
+ return None
126
+
127
+
128
+ def skip(cache_path: Path, version: str) -> None:
129
+ """Stop reporting *version*; a newer release is reported again. Never raises."""
130
+ data = _load_cache(cache_path)
131
+ data["skipped"] = version
132
+ _write_cache(cache_path, data)
133
+
134
+
135
+ def offer(
136
+ started: Started | None,
137
+ ask: Callable[[UpdateAvailable], str],
138
+ *,
139
+ cache_path: Path,
140
+ upgrade: Sequence[str] | Callable[[UpdateAvailable], Sequence[Sequence[str]]],
141
+ run: Callable[..., object] = subprocess.run,
142
+ ) -> str | None:
143
+ """Collect *started*, let *ask* pick an answer, then act on it.
144
+
145
+ *upgrade* is one command, or a callable building the commands from the
146
+ found release, run in order. Returns the answer acted on
147
+ (``UPGRADE_FAILED`` when a command is missing or exits nonzero — the
148
+ rest are then skipped), or None when there was nothing to offer. Ctrl+C
149
+ inside *ask* is ``SKIP``. Never raises.
150
+ """
151
+ try:
152
+ found = collect(started)
153
+ if found is None:
154
+ return None
155
+ try:
156
+ answer = ask(found)
157
+ except KeyboardInterrupt:
158
+ return SKIP
159
+ if answer == SKIP_VERSION:
160
+ skip(cache_path, found.latest)
161
+ elif answer == UPDATE_NOW:
162
+ steps = upgrade(found) if callable(upgrade) else [upgrade]
163
+ for step in steps:
164
+ try:
165
+ done = run(list(step), check=False)
166
+ except OSError:
167
+ return UPGRADE_FAILED
168
+ if getattr(done, "returncode", 1) != 0:
169
+ return UPGRADE_FAILED
170
+ return answer
171
+ except Exception: # noqa: BLE001 - an update offer must never fail the command
172
+ return None
173
+
174
+
175
+ def _bare(version: object) -> str | None:
176
+ """*version* without a leading ``v`` (an old cache holds PyPI's ``0.10.0``, now tags)."""
177
+ return version.removeprefix("v") if isinstance(version, str) else None
178
+
179
+
180
+ def _load_cache(path: Path) -> dict:
181
+ try:
182
+ data = json.loads(path.read_text(encoding="utf-8"))
183
+ except (OSError, ValueError, RecursionError): # RecursionError: a deeply nested file
184
+ return {}
185
+ if not isinstance(data, dict):
186
+ return {}
187
+ # Only a valid skipped version survives: a hostile file can't smuggle anything
188
+ # (e.g. a 100k-deep dict that json.dumps can't write back), and old
189
+ # checked_at/latest keys are dropped on the next write.
190
+ skipped = data.get("skipped")
191
+ return {"skipped": skipped} if isinstance(skipped, str) and _VERSION_RE.fullmatch(skipped) else {}
192
+
193
+
194
+ def _write_cache(path: Path, data: dict) -> None:
195
+ # tmp + os.replace never follows a symlink at *path*; 0600 from creation.
196
+ tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
197
+ try:
198
+ path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
199
+ tmp.unlink(missing_ok=True)
200
+ fd = os.open(tmp, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
201
+ try:
202
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
203
+ fh.write(json.dumps(data) + "\n")
204
+ os.replace(tmp, path)
205
+ except BaseException:
206
+ tmp.unlink(missing_ok=True)
207
+ raise
208
+ except (OSError, RecursionError, ValueError):
209
+ return
@@ -28,8 +28,7 @@ from .output import use_color
28
28
  from .ui import theme
29
29
 
30
30
  DIST = "agent-toggle"
31
- REPO = "weskao/agent-toggle"
32
- PLUGIN = "agent-toggle@agent-toggle"
31
+ REPO = update_check.REPO
33
32
  UPDATE = [DIST, "update"]
34
33
  RELEASE_NOTES = "https://github.com/weskao/agent-toggle/releases/tag/v%s"
35
34
  ANSWERS = (update_check.UPDATE_NOW, update_check.SKIP, update_check.SKIP_VERSION)
@@ -48,9 +47,13 @@ def after_steps() -> list[list[str]]:
48
47
  installed, then the shims."""
49
48
  steps: list[list[str]] = []
50
49
  # bare "claude": what a person pastes; _run() resolves it, as claude is often off PATH
51
- if plugin_cli.claude_bin() and any(p["id"] == PLUGIN for p in plugin_cli.list_plugins(lambda _msg: None)):
52
- steps += [["claude", "plugin", "marketplace", "update", DIST],
53
- ["claude", "plugin", "update", PLUGIN]]
50
+ if plugin_cli.claude_bin():
51
+ # any marketplace the mod came from; one id lists once per scope
52
+ installed = sorted({p["id"] for p in plugin_cli.list_plugins(lambda _msg: None)
53
+ if p["id"].partition("@")[0] == DIST})
54
+ for pid in installed:
55
+ steps += [["claude", "plugin", "marketplace", "update", pid.partition("@")[2]],
56
+ ["claude", "plugin", "update", pid]]
54
57
  return [*steps, [DIST, "install-shims"]]
55
58
 
56
59
 
@@ -93,12 +96,12 @@ def cache_path() -> Path:
93
96
 
94
97
 
95
98
  def start() -> update_check.Started | None:
96
- """Kick off the PyPI check on a daemon thread, unless the `update_check` setting
97
- (env AGENT_TOGGLE_UPDATE_CHECK wins) is off. Never raises."""
99
+ """Kick off the release check (update_kit) on a daemon thread, unless the `update_check`
100
+ setting (env AGENT_TOGGLE_UPDATE_CHECK wins) is off. Never raises."""
98
101
  try:
99
102
  if not settings.get("update_check"):
100
103
  return None
101
- return update_check.start(DIST, __version__, cache_path=cache_path())
104
+ return update_check.start(__version__, cache_path=cache_path())
102
105
  except Exception: # noqa: BLE001 - a hint must never fail the command
103
106
  return None
104
107
 
@@ -112,7 +115,7 @@ def is_interactive() -> bool:
112
115
 
113
116
  def _available(found: update_check.UpdateAvailable) -> str:
114
117
  return t("update.available", "agent-toggle %s is available (you have %s)",
115
- _clean(found.latest), _clean(found.current))
118
+ _clean(found.latest).removeprefix("v"), _clean(found.current))
116
119
 
117
120
 
118
121
  def hint(found: update_check.UpdateAvailable) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-toggle
3
- Version: 0.16.1
3
+ Version: 0.17.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
@@ -168,6 +168,9 @@ installs above, type this at the prompt of a Claude Code terminal session (2.1.2
168
168
  ```
169
169
 
170
170
  Answer `y` to add the marketplace, pick a scope (user is the default), then run `/agent-toggle`.
171
+ The same mod is also listed in the `weskao-agent-plugins` marketplace
172
+ (`claude plugin marketplace add weskao/weskao-agent-plugins`, then
173
+ `claude plugin install agent-toggle@weskao-agent-plugins`); install it from one of the two, not both.
171
174
 
172
175
  See [Claude Code pane mod](#claude-code-pane-mod).
173
176
 
@@ -260,7 +263,7 @@ Flags accepted by every command, before or after the subcommand:
260
263
 
261
264
  Extra row fields: `list` rows carry `at`, `mechanism`, `companions`; `cost` rows
262
265
  carry `enabled`, `tokens`, `would_save`, `chars`, and the final `total` row
263
- carries `total_tokens`, `saved_tokens`, `formula`. Rows for a
266
+ carries `total_tokens`, `saved_tokens`, `formula`, `version`. Rows for a
264
267
  directory shared with another harness (see below) carry `shared_with`, the other
265
268
  harnesses that read it: `disable` / `enable` rows relative to the harness you
266
269
  asked, plus `owner`, the harness whose park dir and state entry hold the item;
@@ -498,7 +501,7 @@ invalid value in either is ignored with one warning on stderr.
498
501
 
499
502
  ## Update check
500
503
 
501
- On every invocation (unless disabled) agent-toggle checks PyPI for a newer release. It starts
504
+ On every invocation (unless disabled) agent-toggle checks the repository's release tags for a newer release. It starts
502
505
  before the command and is offered after it, on every exit path; it overlaps the command and waits at most 0.8 s afterwards.
503
506
  `update` and `completion` skip it.
504
507
 
@@ -513,9 +516,9 @@ before the command and is offered after it, on every exit path; it overlaps the
513
516
  then ` agent-toggle update`.
514
517
  - It never changes the exit code or stdout, so `--json` output stays one document.
515
518
 
516
- It is one `GET https://pypi.org/pypi/agent-toggle/json` on a background thread with a 0.8 s
517
- timeout, cached for 10 minutes in `~/.agent-toggle/update-check.json`. Nothing is sent beyond a
518
- normal HTTP request with the User-Agent `agent-toggle-update-check`. See `SECURITY.md`
519
+ The lookup is update-kit's: one `git ls-remote --tags --refs` of the repository on a background
520
+ thread, cached for 10 minutes in `~/.update-kit/state.json`. Only the version you chose to skip is
521
+ kept in `~/.agent-toggle/update-check.json`. Nothing is sent beyond a normal git request. See `SECURITY.md`
519
522
  for the hardening. To turn it off, set `AGENT_TOGGLE_UPDATE_CHECK=0` or the `update_check` setting
520
523
  to off (the settings menu's "Check for updates").
521
524
 
@@ -527,9 +530,9 @@ tag (`git ls-remote --tags`) and always installs that tag with
527
530
  `uv tool install --force --no-sources git+<repo>@<tag>` (`pipx install --force` for a pipx install).
528
531
  A dev checkout, a branch or commit install, or an unmanaged install is replaced by the tag too
529
532
  (with uv, else pipx); only an install made from exactly that tag is already up to date.
530
- It then runs, with the new binary and in order: when the Claude Code plugin is installed,
531
- `claude plugin marketplace update agent-toggle` and `claude plugin update agent-toggle@agent-toggle`,
532
- then `agent-toggle install-shims`. A failing step prints a warning and makes the exit code `1`.
533
+ It then runs, with the new binary and in order: when the Claude Code plugin is installed (from any
534
+ marketplace), `claude plugin marketplace update <marketplace>` and
535
+ `claude plugin update agent-toggle@<marketplace>`, then `agent-toggle install-shims`. A failing step prints a warning and makes the exit code `1`.
533
536
  It never prompts and writes to stderr only.
534
537
 
535
538
  | Exit | Meaning |
@@ -931,7 +934,7 @@ bookkeeping.
931
934
  | `parked/<sha8>/` | items parked by `--project` (`<sha8>` = first 8 hex of the SHA-1 of the resolved project dir) |
932
935
  | `profiles/` | `<name>.json` profiles (dir `0700`, files `0600`) |
933
936
  | `config.json` | [settings](#settings--config-menu) and the Telegram chat id (mode `0600`, atomic write, unknown keys kept) |
934
- | `update-check.json` | the [update check](#update-check)'s 10-minute cache (mode `0600`, atomic write) |
937
+ | `update-check.json` | the [update check](#update-check)'s skipped version (mode `0600`, atomic write) |
935
938
 
936
939
  A killed run loses at most the one item in flight. If that was a flag write or a
937
940
  dir move (with its companions), its `pending` record (the full entry, a flag's
@@ -1011,8 +1014,9 @@ CLI (`cost --json`, `disable|enable --json`) and works for the claude harness on
1011
1014
  `agent-toggle ui` stays the cross-harness UI.
1012
1015
 
1013
1016
  Install it with `/plugin install agent-toggle --marketplace weskao/agent-toggle` (see
1014
- [Install](#install)); `claude plugin update agent-toggle@agent-toggle` picks up a new release. To
1015
- run your own checkout instead (edits reload as you save):
1017
+ [Install](#install)); `/agent-toggle update` picks up a new release: it runs `agent-toggle update`,
1018
+ which updates the CLI, this mod and the shims together (restart Claude Code to load the new mod).
1019
+ The mod and the CLI share one version. To run your own checkout instead (edits reload as you save):
1016
1020
 
1017
1021
  ```sh
1018
1022
  claude --plugin-dir ./mod # then run /agent-toggle inside Claude
@@ -1144,8 +1148,10 @@ prints a `::notice::` and exits 0, so it never adds a second red X.
1144
1148
  ## Releasing
1145
1149
 
1146
1150
  A release is a git tag `vX.Y.Z` (where `X.Y.Z` must match `agent_toggle.__version__`).
1147
- Bump `version` in `mod/.claude-plugin/plugin.json` to the same value by hand; nothing checks it,
1148
- and without it `claude plugin update` does not hand marketplace installs the new mod.
1151
+ `version` in `mod/.claude-plugin/plugin.json` must be the same value (the mod ships in lockstep;
1152
+ without the bump `claude plugin update` does not hand marketplace installs the new mod).
1153
+ `tests/test_mod_version.py` and the release workflow both fail on a mismatch.
1154
+ The `weskao-agent-plugins` marketplace lists the mod without a version, so it needs no bump there.
1149
1155
  Pushing the tag triggers `.github/workflows/release.yml`:
1150
1156
 
1151
1157
  1. **Build** job checks that the tag matches `__version__`, builds a wheel and
@@ -73,6 +73,7 @@ tests/test_install.py
73
73
  tests/test_logo.py
74
74
  tests/test_mechanisms.py
75
75
  tests/test_menu.py
76
+ tests/test_mod_version.py
76
77
  tests/test_mods.py
77
78
  tests/test_picker.py
78
79
  tests/test_platform.py