agent-toggle 0.12.1__tar.gz → 0.13.1__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.
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/PKG-INFO +76 -40
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/README.md +75 -39
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/__init__.py +1 -1
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/backends/mcp_json.py +6 -4
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/backends/plugin_cli.py +37 -6
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/cli.py +18 -2
- agent_toggle-0.13.1/agent_toggle/completions.py +210 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/doctor.py +4 -2
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/harnesses.py +13 -19
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/help.py +11 -1
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/locales/zh_tw_cli.py +4 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/mechanisms.py +331 -108
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/ops.py +5 -4
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/store.py +4 -3
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/ui/config_menu.py +12 -6
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle.egg-info/PKG-INFO +76 -40
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle.egg-info/SOURCES.txt +1 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_alias.py +2 -2
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_backends.py +47 -5
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_cli_surface.py +9 -0
- agent_toggle-0.13.1/tests/test_completions.py +98 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_config.py +11 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_config_menu.py +32 -7
- agent_toggle-0.13.1/tests/test_crash.py +1072 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_harnesses.py +22 -0
- agent_toggle-0.13.1/tests/test_mechanisms.py +293 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_project.py +17 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_secret_audit.py +62 -0
- agent_toggle-0.12.1/tests/test_completions.py +0 -37
- agent_toggle-0.12.1/tests/test_crash.py +0 -382
- agent_toggle-0.12.1/tests/test_mechanisms.py +0 -79
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/LICENSE +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/__main__.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/backends/__init__.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/backends/flag_json.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/backends/mcp_toml.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/companions.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/config.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/cost.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/fs.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/i18n.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/locales/__init__.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/locales/zh_tw_common.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/locales/zh_tw_config.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/locales/zh_tw_picker.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/output.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/profiles.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/settings.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/shims/claude.md.tmpl +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/shims/generic.md.tmpl +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/spinner.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/toml_check.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/ui/__init__.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/ui/logo.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/ui/menu.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/ui/model.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/ui/picker.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/ui/theme.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/undo.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/update_check.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle/update_prompt.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle.egg-info/dependency_links.txt +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle.egg-info/entry_points.txt +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle.egg-info/requires.txt +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/agent_toggle.egg-info/top_level.txt +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/pyproject.toml +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/setup.cfg +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_all_type.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_cli.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_color.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_companions.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_conformance.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_containment.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_cost.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_doctor.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_encoding.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_fs.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_help.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_i18n.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_install.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_logo.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_menu.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_mods.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_picker.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_platform.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_profiles.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_safety.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_same_name.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_settings.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_store.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_theme.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_undo.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_update_check.py +0 -0
- {agent_toggle-0.12.1 → agent_toggle-0.13.1}/tests/test_update_prompt.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-toggle
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.1
|
|
4
4
|
Summary: Temporarily disable / re-enable AI-agent skills, agents, commands, rules, plugins and MCP servers across harnesses (Claude Code, Codex, Grok, OpenCode, OpenClaw, Copilot, Vibe, Devin, Antigravity)
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/weskao/agent-toggle
|
|
@@ -59,16 +59,17 @@ language, update check and Telegram notifications. It also runs tools such as `d
|
|
|
59
59
|
|
|
60
60
|

|
|
61
61
|
|
|
62
|
-
**`/agent-toggle` inside Claude Code**: the same picker as a pane,
|
|
63
|
-
servers, plugins and mods on or off without leaving the session
|
|
62
|
+
**`/agent-toggle` inside Claude Code**: the same picker as a pane. Search, then turn skills, MCP
|
|
63
|
+
servers, plugins and mods on or off without leaving the session; each toggle applies at once and
|
|
64
|
+
queues the matching reload ([Claude Code pane mod](#claude-code-pane-mod)).
|
|
64
65
|
|
|
65
|
-

|
|
66
67
|
|
|
67
68
|
## Why
|
|
68
69
|
|
|
69
70
|
| target | native mechanism | the gap |
|
|
70
71
|
|---|---|---|
|
|
71
|
-
| plugin | `claude plugin disable/enable` |
|
|
72
|
+
| plugin | `claude plugin disable/enable` | without `-s` the scope follows the current dir, so a project plugin enabled from elsewhere stays off; this tool pins the install's scope and project |
|
|
72
73
|
| skill | none | moving files by hand hits the rename trap below |
|
|
73
74
|
| agent | none | same |
|
|
74
75
|
| command | none | same, plus nested `group/name.md` paths get flattened |
|
|
@@ -146,17 +147,12 @@ tool itself stays in one place.
|
|
|
146
147
|
**Optional: Telegram alerts for CI** (for maintainers of a fork or clone). The
|
|
147
148
|
Telegram support comes from [telegram-kit](https://pypi.org/project/telegram-kit/)
|
|
148
149
|
(itself stdlib-only), which every install includes, so a reinstall or upgrade keeps
|
|
149
|
-
your settings.
|
|
150
|
-
|
|
151
|
-
```sh
|
|
152
|
-
agent-toggle config # settings menu: bot token + chat id live under Notifications
|
|
153
|
-
agent-toggle config sync-ci # set the two GitHub repository secrets
|
|
154
|
-
```
|
|
150
|
+
your settings. The settings menu hides its Telegram rows for now; set up the alerts as in
|
|
151
|
+
[CI notifications](#ci-notifications).
|
|
155
152
|
|
|
156
153
|
The old `agent-toggle[telegram]` extra still resolves and is now a no-op. If
|
|
157
154
|
telegram-kit is missing from a broken install, `config test` and `config sync-ci`
|
|
158
|
-
exit `4
|
|
159
|
-
`telegram-kit missing: reinstall agent-toggle`.
|
|
155
|
+
exit `4`.
|
|
160
156
|
|
|
161
157
|
**Optional: the Claude Code pane mod** (the picker as a pane inside Claude Code, claude harness
|
|
162
158
|
only). It is not part of the PyPI package; with `agent-toggle` already on `PATH` from one of the
|
|
@@ -172,16 +168,27 @@ See [Claude Code pane mod](#claude-code-pane-mod).
|
|
|
172
168
|
|
|
173
169
|
## Shell completion
|
|
174
170
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
171
|
+
`agent-toggle completion <shell>` prints a static completion script, built from the CLI
|
|
172
|
+
parser, for bash, zsh, fish or PowerShell. Save it once where your shell loads it, and
|
|
173
|
+
run the same line again after an upgrade so new commands complete:
|
|
178
174
|
|
|
179
175
|
```sh
|
|
180
|
-
|
|
176
|
+
# zsh: needs fpath=(~/.zfunc $fpath) before compinit in ~/.zshrc
|
|
177
|
+
mkdir -p ~/.zfunc && agent-toggle completion zsh > ~/.zfunc/_agent-toggle
|
|
178
|
+
# bash: loaded on demand by the bash-completion package
|
|
179
|
+
mkdir -p ~/.local/share/bash-completion/completions && agent-toggle completion bash > ~/.local/share/bash-completion/completions/agent-toggle
|
|
180
|
+
# fish
|
|
181
|
+
agent-toggle completion fish > ~/.config/fish/completions/agent-toggle.fish
|
|
181
182
|
```
|
|
182
183
|
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
```powershell
|
|
185
|
+
# PowerShell: save the script, then load it from your profile
|
|
186
|
+
agent-toggle completion powershell > "$HOME\agent-toggle.ps1"; Add-Content $PROFILE '. "$HOME\agent-toggle.ps1"'
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Without the bash-completion package, add `source <file>` to `~/.bashrc` instead.
|
|
190
|
+
`python3 tools/gen_completions.py completions` writes all four files from a checkout; the
|
|
191
|
+
Release workflow uploads them as its `completions` artifact.
|
|
185
192
|
|
|
186
193
|
## Usage
|
|
187
194
|
|
|
@@ -258,7 +265,9 @@ unshared).
|
|
|
258
265
|
`--dry-run` (`disable` / `enable` / `enable --all` / `undo` / `profile apply` /
|
|
259
266
|
`install-shims` / `ui`) computes the plan --
|
|
260
267
|
moves, companions, backups, MCP edits, warnings -- and writes nothing: no state,
|
|
261
|
-
log, lock or backup, and no chmod; it never shells out to `claude
|
|
268
|
+
log, lock or backup, and no chmod; it never shells out to `claude`, except one
|
|
269
|
+
read-only `claude plugin list --json` (up to 30 s) when a killed run left a plugin
|
|
270
|
+
op pending and has to be settled. `disable` /
|
|
262
271
|
`enable` result rows have action `would-disable` / `would-enable`; every plan
|
|
263
272
|
row has status `planned`. Read-only commands (`list`, `status`, `cost`) also
|
|
264
273
|
change nothing on disk -- `status` warns about a `state.json` or
|
|
@@ -391,8 +400,6 @@ a band above the prompt, buttons) and change Claude Code's behaviour. A plugin w
|
|
|
391
400
|
`hooks.json`, or with only ordinary hooks (shell commands such as `PreToolUse`), is a plain
|
|
392
401
|
plugin: it ships skills, commands, agents, MCP servers or hooks and draws nothing.
|
|
393
402
|
|
|
394
|
-

|
|
395
|
-
|
|
396
403
|
- The check reads the install path `claude plugin list` reports and every cached version
|
|
397
404
|
under `~/.claude/plugins/cache/<marketplace>/<name>/`, so a parked mod is still a mod.
|
|
398
405
|
To list the installed mods by hand:
|
|
@@ -428,8 +435,7 @@ end of input, `q` or an empty line exits `0`
|
|
|
428
435
|
| General | Check for updates, Color, Logo, Language, Default harness |
|
|
429
436
|
| Harnesses | one On/Off per harness, with `found ~/.x` or `not on this machine` |
|
|
430
437
|
| Picker | default sort, harness filter, type filter |
|
|
431
|
-
|
|
|
432
|
-
| Tools | Health check (`doctor`; then asks y/n per fix it can run), Install shims, Undo last change (asks y/n), Send test message, Sync CI secrets (asks y/n); their output shows in an overlay |
|
|
438
|
+
| Tools | Health check (`doctor`; then asks y/n per fix it can run), Install shims, Undo last change (asks y/n); their output shows in an overlay |
|
|
433
439
|
|
|
434
440
|
Keys: `↑` `↓` move (wraps, skips headings), `←` `→` cycle a value, `Enter` / `Space` change or run
|
|
435
441
|
the row, `r` reset the row, `R` reset everything (asks y/n), `q` / `Esc` / `Ctrl-C` quit. In an
|
|
@@ -528,7 +534,9 @@ logo, centred, when the window is tall enough to fit the whole menu beside it; t
|
|
|
528
534
|
| agy | `~/.gemini/config` | ✓ | — | — | — | — | ✓ `mcp_config.json` |
|
|
529
535
|
|
|
530
536
|
Per-harness locations, mechanisms and the harness versions this was checked
|
|
531
|
-
against are in [docs/harnesses.md](docs/harnesses.md).
|
|
537
|
+
against are in [docs/harnesses.md](docs/harnesses.md). What `disable` and `enable`
|
|
538
|
+
change on disk for each type, with example live, parked and backup paths, is in
|
|
539
|
+
[docs/toggle-paths.md](docs/toggle-paths.md).
|
|
532
540
|
|
|
533
541
|
**Homes per OS.** Every `~` above is Python's `Path.home()`: `$HOME` on macOS and
|
|
534
542
|
Linux, `%USERPROFILE%` on Windows (so `~/.claude` is `C:\Users\<you>\.claude`).
|
|
@@ -545,8 +553,13 @@ Plugin ids are passed to `claude plugin ...`, which on Windows runs through
|
|
|
545
553
|
|
|
546
554
|
Copilot's `config.json` is machine-managed and never edited by this tool; Copilot
|
|
547
555
|
plugins are not yet supported. `claude` plugins are toggled through `claude
|
|
548
|
-
plugin enable/disable
|
|
549
|
-
|
|
556
|
+
plugin enable/disable -s <scope>`. The scope comes from the plugin's one install in
|
|
557
|
+
`~/.claude/plugins/installed_plugins.json`, and a project or local install runs in its
|
|
558
|
+
project dir, so a disable and an enable from different directories still hit the same
|
|
559
|
+
setting. With several installs of one id, the CLI picks the scope from the current dir;
|
|
560
|
+
`enable` then runs in the dir the disable ran in. Details are in
|
|
561
|
+
[docs/toggle-paths.md](docs/toggle-paths.md#plugins). `openclaw` plugins are toggled
|
|
562
|
+
through a config flag (see [Assumed formats](#assumed-formats)). Codex has no plugin CLI this tool can drive: `disable
|
|
550
563
|
plugin x --harness codex` exits `4`, which is why the matrix leaves it unchecked.
|
|
551
564
|
|
|
552
565
|
Flag items (openclaw plugins and flagged skills, opencode mcp) are toggled with
|
|
@@ -568,7 +581,7 @@ rather than guessing.
|
|
|
568
581
|
|
|
569
582
|
OpenCode may read skills from another harness's directory through
|
|
570
583
|
`opencode.json` → `skills.paths` (absolute, `~`, `$HOME/...`
|
|
571
|
-
and `${HOME}/...` entries; read from `opencode.json
|
|
584
|
+
and `${HOME}/...` entries; read from `opencode.json` and `opencode.jsonc` (OpenCode loads both and merges them);
|
|
572
585
|
relative entries are skipped, since OpenCode resolves them against the session
|
|
573
586
|
directory). OpenCode also always scans `~/.claude/skills` and `~/.agents/skills`,
|
|
574
587
|
so claude's skills count as shared with it. Such a directory is **one** item, filed under
|
|
@@ -576,6 +589,11 @@ its owner (the harness whose home really holds it): it is
|
|
|
576
589
|
parked once, tracked once, and every row reports `shared_with`, the other
|
|
577
590
|
harnesses it also affects. `status` prints `shared dir with: ...`.
|
|
578
591
|
|
|
592
|
+
Disabling an OpenCode skill while a same-named copy sits in another scanned dir warns
|
|
593
|
+
that OpenCode still loads that copy (stderr, and `warnings` in `--json`). Two copies of
|
|
594
|
+
one name under one owner share one record, so a second `disable` is refused with
|
|
595
|
+
`another copy is already parked at <path> -- enable it first`.
|
|
596
|
+
|
|
579
597
|
If the live directory carries `.synced-from-*` markers, a sync job may
|
|
580
598
|
re-create what you parked; `status` and `disable` warn about it. Park in the
|
|
581
599
|
source harness instead. The warning is printed once per real directory, naming
|
|
@@ -612,6 +630,11 @@ visible rather than silent.
|
|
|
612
630
|
MCP changes need a **new session** to take effect, except in a Claude Code session running the
|
|
613
631
|
agent-toggle mod, which runs `/mcp disable|enable <name>` for you (see the mod section).
|
|
614
632
|
|
|
633
|
+
`/mcp disable <name>` also lists the server in that project's `disabledMcpServers` in
|
|
634
|
+
`~/.claude.json`. Left there, a restored server would stay off in that project, so `enable`
|
|
635
|
+
removes the name again: from every project for a user- or local-scope server, and only from
|
|
636
|
+
that repo and the dirs inside it for a `--project` `.mcp.json` server.
|
|
637
|
+
|
|
615
638
|
### Claude MCP scopes
|
|
616
639
|
|
|
617
640
|
Both scopes stored in `~/.claude.json` are togglable, and the scope round-trips:
|
|
@@ -875,10 +898,11 @@ a warning and a `recovered` log row (which `undo` does not reverse). `status` an
|
|
|
875
898
|
`doctor` report it, with `agent-toggle enable ...` when the item ends up
|
|
876
899
|
disabled. The one case left to you is a cross-filesystem move killed between its
|
|
877
900
|
two renames: both copies are complete, and the report names the `diff -r` to
|
|
878
|
-
check and the `rm -rf` that keeps either one. MCP
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
901
|
+
check and the `rm -rf` that keeps either one. MCP servers (user-scope json,
|
|
902
|
+
claude-json, toml, project `.mcp.json`) and claude plugins have a `pending` record
|
|
903
|
+
too (a plugin is settled from a live `claude plugin list --json`); only a
|
|
904
|
+
`claude.ai` connector toggle is saved per item (when the next one starts), and a
|
|
905
|
+
kill inside that window leaves its change unrecorded.
|
|
882
906
|
A killed run also leaves its `lock`; the next run's error says so.
|
|
883
907
|
|
|
884
908
|
Each `log.jsonl` row is `{ts, harness, type, name, action, result, batch,
|
|
@@ -989,6 +1013,9 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
|
|
|
989
1013
|
`/reload-plugins`, an MCP server `/mcp disable|enable <name>`. The toast names it, e.g.
|
|
990
1014
|
`⛔ [DISABLED] skill: foo (/reload-skills)`. Agents and rules have no reload and still say
|
|
991
1015
|
`takes effect in a new session`. If Claude refuses a queued command, a toast says so.
|
|
1016
|
+
`/reload-skills` and `/reload-plugins` only re-read what is on disk. `/mcp disable` also
|
|
1017
|
+
leaves the per-project mark that `enable` clears (see
|
|
1018
|
+
[MCP backups are verbatim](#mcp-backups-are-verbatim)).
|
|
992
1019
|
- `agent-toggle ui` in another terminal applies on Enter and writes the Claude rows it changed to
|
|
993
1020
|
`~/.agent-toggle/reload.json`. Every open session running the mod checks that file's mtime every
|
|
994
1021
|
5 s (one `stat`) and queues the same reloads, toasting `agent-toggle ui: /reload-skills`. A file
|
|
@@ -1029,21 +1056,24 @@ it on macOS, Linux and Windows.
|
|
|
1029
1056
|
|
|
1030
1057
|
## CI notifications
|
|
1031
1058
|
|
|
1032
|
-
|
|
1033
|
-
push (a lint-only failure does not page).
|
|
1034
|
-
|
|
1059
|
+
CI and the Release workflow can send a Telegram message when they fail: `ci.yml` when the
|
|
1060
|
+
test matrix fails on a push (a lint-only failure does not page), `release.yml` when the
|
|
1061
|
+
build, PyPI publish or post-publish smoke test fails. Alerts never fire for pull requests or
|
|
1062
|
+
green runs. Both call `.github/workflows/notify-telegram.yml`, and the message names the
|
|
1063
|
+
failure, repository, tag or branch, 7-character commit and a link to the run.
|
|
1035
1064
|
|
|
1036
|
-
|
|
1065
|
+
The [settings menu](#settings--config-menu) hides its Telegram rows (bot token, chat ID,
|
|
1066
|
+
Send test message, Sync CI secrets) for now; `SHOW_TELEGRAM` in `agent_toggle/ui/config_menu.py`
|
|
1067
|
+
brings them back. `config test` and `config sync-ci` still work: they read a bot token and
|
|
1068
|
+
chat ID saved earlier, else `TG_BOT_TOKEN` / `TG_CHAT_ID` from the environment. Without
|
|
1069
|
+
either, set the secrets by hand (below).
|
|
1037
1070
|
|
|
1038
1071
|
```sh
|
|
1039
|
-
agent-toggle config # settings menu: Notifications has the bot token (hidden) and chat id
|
|
1040
1072
|
agent-toggle config test # send one test message
|
|
1041
1073
|
agent-toggle config sync-ci # set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID on the repo
|
|
1042
1074
|
```
|
|
1043
1075
|
|
|
1044
|
-
|
|
1045
|
-
`Esc`, and clears on `-` then `Enter`; the Tools group also has Send test message and Sync CI
|
|
1046
|
-
secrets (the same as `config test` / `config sync-ci`). The bot token
|
|
1076
|
+
When the rows are shown, the bot token
|
|
1047
1077
|
is kept only in the OS credential store (macOS Keychain, Linux Secret Service, Windows DPAPI)
|
|
1048
1078
|
through telegram-kit, never in a file or on a command line, and is shown masked. With no
|
|
1049
1079
|
credential store it refuses to store the token; set `TG_BOT_TOKEN` in the environment instead
|
|
@@ -1059,6 +1089,12 @@ gh secret set TELEGRAM_BOT_TOKEN
|
|
|
1059
1089
|
gh secret set TELEGRAM_CHAT_ID
|
|
1060
1090
|
```
|
|
1061
1091
|
|
|
1092
|
+
To check the repository secrets themselves, send a test message through the same workflow:
|
|
1093
|
+
|
|
1094
|
+
```sh
|
|
1095
|
+
gh workflow run notify-telegram.yml # optional: -f title="..."
|
|
1096
|
+
```
|
|
1097
|
+
|
|
1062
1098
|
With either secret unset (a fork, or a clone you have not configured) the notify job
|
|
1063
1099
|
prints a `::notice::` and exits 0, so it never adds a second red X.
|
|
1064
1100
|
|
|
@@ -26,16 +26,17 @@ language, update check and Telegram notifications. It also runs tools such as `d
|
|
|
26
26
|
|
|
27
27
|

|
|
28
28
|
|
|
29
|
-
**`/agent-toggle` inside Claude Code**: the same picker as a pane,
|
|
30
|
-
servers, plugins and mods on or off without leaving the session
|
|
29
|
+
**`/agent-toggle` inside Claude Code**: the same picker as a pane. Search, then turn skills, MCP
|
|
30
|
+
servers, plugins and mods on or off without leaving the session; each toggle applies at once and
|
|
31
|
+
queues the matching reload ([Claude Code pane mod](#claude-code-pane-mod)).
|
|
31
32
|
|
|
32
|
-

|
|
33
34
|
|
|
34
35
|
## Why
|
|
35
36
|
|
|
36
37
|
| target | native mechanism | the gap |
|
|
37
38
|
|---|---|---|
|
|
38
|
-
| plugin | `claude plugin disable/enable` |
|
|
39
|
+
| plugin | `claude plugin disable/enable` | without `-s` the scope follows the current dir, so a project plugin enabled from elsewhere stays off; this tool pins the install's scope and project |
|
|
39
40
|
| skill | none | moving files by hand hits the rename trap below |
|
|
40
41
|
| agent | none | same |
|
|
41
42
|
| command | none | same, plus nested `group/name.md` paths get flattened |
|
|
@@ -113,17 +114,12 @@ tool itself stays in one place.
|
|
|
113
114
|
**Optional: Telegram alerts for CI** (for maintainers of a fork or clone). The
|
|
114
115
|
Telegram support comes from [telegram-kit](https://pypi.org/project/telegram-kit/)
|
|
115
116
|
(itself stdlib-only), which every install includes, so a reinstall or upgrade keeps
|
|
116
|
-
your settings.
|
|
117
|
-
|
|
118
|
-
```sh
|
|
119
|
-
agent-toggle config # settings menu: bot token + chat id live under Notifications
|
|
120
|
-
agent-toggle config sync-ci # set the two GitHub repository secrets
|
|
121
|
-
```
|
|
117
|
+
your settings. The settings menu hides its Telegram rows for now; set up the alerts as in
|
|
118
|
+
[CI notifications](#ci-notifications).
|
|
122
119
|
|
|
123
120
|
The old `agent-toggle[telegram]` extra still resolves and is now a no-op. If
|
|
124
121
|
telegram-kit is missing from a broken install, `config test` and `config sync-ci`
|
|
125
|
-
exit `4
|
|
126
|
-
`telegram-kit missing: reinstall agent-toggle`.
|
|
122
|
+
exit `4`.
|
|
127
123
|
|
|
128
124
|
**Optional: the Claude Code pane mod** (the picker as a pane inside Claude Code, claude harness
|
|
129
125
|
only). It is not part of the PyPI package; with `agent-toggle` already on `PATH` from one of the
|
|
@@ -139,16 +135,27 @@ See [Claude Code pane mod](#claude-code-pane-mod).
|
|
|
139
135
|
|
|
140
136
|
## Shell completion
|
|
141
137
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
138
|
+
`agent-toggle completion <shell>` prints a static completion script, built from the CLI
|
|
139
|
+
parser, for bash, zsh, fish or PowerShell. Save it once where your shell loads it, and
|
|
140
|
+
run the same line again after an upgrade so new commands complete:
|
|
145
141
|
|
|
146
142
|
```sh
|
|
147
|
-
|
|
143
|
+
# zsh: needs fpath=(~/.zfunc $fpath) before compinit in ~/.zshrc
|
|
144
|
+
mkdir -p ~/.zfunc && agent-toggle completion zsh > ~/.zfunc/_agent-toggle
|
|
145
|
+
# bash: loaded on demand by the bash-completion package
|
|
146
|
+
mkdir -p ~/.local/share/bash-completion/completions && agent-toggle completion bash > ~/.local/share/bash-completion/completions/agent-toggle
|
|
147
|
+
# fish
|
|
148
|
+
agent-toggle completion fish > ~/.config/fish/completions/agent-toggle.fish
|
|
148
149
|
```
|
|
149
150
|
|
|
150
|
-
|
|
151
|
-
|
|
151
|
+
```powershell
|
|
152
|
+
# PowerShell: save the script, then load it from your profile
|
|
153
|
+
agent-toggle completion powershell > "$HOME\agent-toggle.ps1"; Add-Content $PROFILE '. "$HOME\agent-toggle.ps1"'
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Without the bash-completion package, add `source <file>` to `~/.bashrc` instead.
|
|
157
|
+
`python3 tools/gen_completions.py completions` writes all four files from a checkout; the
|
|
158
|
+
Release workflow uploads them as its `completions` artifact.
|
|
152
159
|
|
|
153
160
|
## Usage
|
|
154
161
|
|
|
@@ -225,7 +232,9 @@ unshared).
|
|
|
225
232
|
`--dry-run` (`disable` / `enable` / `enable --all` / `undo` / `profile apply` /
|
|
226
233
|
`install-shims` / `ui`) computes the plan --
|
|
227
234
|
moves, companions, backups, MCP edits, warnings -- and writes nothing: no state,
|
|
228
|
-
log, lock or backup, and no chmod; it never shells out to `claude
|
|
235
|
+
log, lock or backup, and no chmod; it never shells out to `claude`, except one
|
|
236
|
+
read-only `claude plugin list --json` (up to 30 s) when a killed run left a plugin
|
|
237
|
+
op pending and has to be settled. `disable` /
|
|
229
238
|
`enable` result rows have action `would-disable` / `would-enable`; every plan
|
|
230
239
|
row has status `planned`. Read-only commands (`list`, `status`, `cost`) also
|
|
231
240
|
change nothing on disk -- `status` warns about a `state.json` or
|
|
@@ -358,8 +367,6 @@ a band above the prompt, buttons) and change Claude Code's behaviour. A plugin w
|
|
|
358
367
|
`hooks.json`, or with only ordinary hooks (shell commands such as `PreToolUse`), is a plain
|
|
359
368
|
plugin: it ships skills, commands, agents, MCP servers or hooks and draws nothing.
|
|
360
369
|
|
|
361
|
-

|
|
362
|
-
|
|
363
370
|
- The check reads the install path `claude plugin list` reports and every cached version
|
|
364
371
|
under `~/.claude/plugins/cache/<marketplace>/<name>/`, so a parked mod is still a mod.
|
|
365
372
|
To list the installed mods by hand:
|
|
@@ -395,8 +402,7 @@ end of input, `q` or an empty line exits `0`
|
|
|
395
402
|
| General | Check for updates, Color, Logo, Language, Default harness |
|
|
396
403
|
| Harnesses | one On/Off per harness, with `found ~/.x` or `not on this machine` |
|
|
397
404
|
| Picker | default sort, harness filter, type filter |
|
|
398
|
-
|
|
|
399
|
-
| Tools | Health check (`doctor`; then asks y/n per fix it can run), Install shims, Undo last change (asks y/n), Send test message, Sync CI secrets (asks y/n); their output shows in an overlay |
|
|
405
|
+
| Tools | Health check (`doctor`; then asks y/n per fix it can run), Install shims, Undo last change (asks y/n); their output shows in an overlay |
|
|
400
406
|
|
|
401
407
|
Keys: `↑` `↓` move (wraps, skips headings), `←` `→` cycle a value, `Enter` / `Space` change or run
|
|
402
408
|
the row, `r` reset the row, `R` reset everything (asks y/n), `q` / `Esc` / `Ctrl-C` quit. In an
|
|
@@ -495,7 +501,9 @@ logo, centred, when the window is tall enough to fit the whole menu beside it; t
|
|
|
495
501
|
| agy | `~/.gemini/config` | ✓ | — | — | — | — | ✓ `mcp_config.json` |
|
|
496
502
|
|
|
497
503
|
Per-harness locations, mechanisms and the harness versions this was checked
|
|
498
|
-
against are in [docs/harnesses.md](docs/harnesses.md).
|
|
504
|
+
against are in [docs/harnesses.md](docs/harnesses.md). What `disable` and `enable`
|
|
505
|
+
change on disk for each type, with example live, parked and backup paths, is in
|
|
506
|
+
[docs/toggle-paths.md](docs/toggle-paths.md).
|
|
499
507
|
|
|
500
508
|
**Homes per OS.** Every `~` above is Python's `Path.home()`: `$HOME` on macOS and
|
|
501
509
|
Linux, `%USERPROFILE%` on Windows (so `~/.claude` is `C:\Users\<you>\.claude`).
|
|
@@ -512,8 +520,13 @@ Plugin ids are passed to `claude plugin ...`, which on Windows runs through
|
|
|
512
520
|
|
|
513
521
|
Copilot's `config.json` is machine-managed and never edited by this tool; Copilot
|
|
514
522
|
plugins are not yet supported. `claude` plugins are toggled through `claude
|
|
515
|
-
plugin enable/disable
|
|
516
|
-
|
|
523
|
+
plugin enable/disable -s <scope>`. The scope comes from the plugin's one install in
|
|
524
|
+
`~/.claude/plugins/installed_plugins.json`, and a project or local install runs in its
|
|
525
|
+
project dir, so a disable and an enable from different directories still hit the same
|
|
526
|
+
setting. With several installs of one id, the CLI picks the scope from the current dir;
|
|
527
|
+
`enable` then runs in the dir the disable ran in. Details are in
|
|
528
|
+
[docs/toggle-paths.md](docs/toggle-paths.md#plugins). `openclaw` plugins are toggled
|
|
529
|
+
through a config flag (see [Assumed formats](#assumed-formats)). Codex has no plugin CLI this tool can drive: `disable
|
|
517
530
|
plugin x --harness codex` exits `4`, which is why the matrix leaves it unchecked.
|
|
518
531
|
|
|
519
532
|
Flag items (openclaw plugins and flagged skills, opencode mcp) are toggled with
|
|
@@ -535,7 +548,7 @@ rather than guessing.
|
|
|
535
548
|
|
|
536
549
|
OpenCode may read skills from another harness's directory through
|
|
537
550
|
`opencode.json` → `skills.paths` (absolute, `~`, `$HOME/...`
|
|
538
|
-
and `${HOME}/...` entries; read from `opencode.json
|
|
551
|
+
and `${HOME}/...` entries; read from `opencode.json` and `opencode.jsonc` (OpenCode loads both and merges them);
|
|
539
552
|
relative entries are skipped, since OpenCode resolves them against the session
|
|
540
553
|
directory). OpenCode also always scans `~/.claude/skills` and `~/.agents/skills`,
|
|
541
554
|
so claude's skills count as shared with it. Such a directory is **one** item, filed under
|
|
@@ -543,6 +556,11 @@ its owner (the harness whose home really holds it): it is
|
|
|
543
556
|
parked once, tracked once, and every row reports `shared_with`, the other
|
|
544
557
|
harnesses it also affects. `status` prints `shared dir with: ...`.
|
|
545
558
|
|
|
559
|
+
Disabling an OpenCode skill while a same-named copy sits in another scanned dir warns
|
|
560
|
+
that OpenCode still loads that copy (stderr, and `warnings` in `--json`). Two copies of
|
|
561
|
+
one name under one owner share one record, so a second `disable` is refused with
|
|
562
|
+
`another copy is already parked at <path> -- enable it first`.
|
|
563
|
+
|
|
546
564
|
If the live directory carries `.synced-from-*` markers, a sync job may
|
|
547
565
|
re-create what you parked; `status` and `disable` warn about it. Park in the
|
|
548
566
|
source harness instead. The warning is printed once per real directory, naming
|
|
@@ -579,6 +597,11 @@ visible rather than silent.
|
|
|
579
597
|
MCP changes need a **new session** to take effect, except in a Claude Code session running the
|
|
580
598
|
agent-toggle mod, which runs `/mcp disable|enable <name>` for you (see the mod section).
|
|
581
599
|
|
|
600
|
+
`/mcp disable <name>` also lists the server in that project's `disabledMcpServers` in
|
|
601
|
+
`~/.claude.json`. Left there, a restored server would stay off in that project, so `enable`
|
|
602
|
+
removes the name again: from every project for a user- or local-scope server, and only from
|
|
603
|
+
that repo and the dirs inside it for a `--project` `.mcp.json` server.
|
|
604
|
+
|
|
582
605
|
### Claude MCP scopes
|
|
583
606
|
|
|
584
607
|
Both scopes stored in `~/.claude.json` are togglable, and the scope round-trips:
|
|
@@ -842,10 +865,11 @@ a warning and a `recovered` log row (which `undo` does not reverse). `status` an
|
|
|
842
865
|
`doctor` report it, with `agent-toggle enable ...` when the item ends up
|
|
843
866
|
disabled. The one case left to you is a cross-filesystem move killed between its
|
|
844
867
|
two renames: both copies are complete, and the report names the `diff -r` to
|
|
845
|
-
check and the `rm -rf` that keeps either one. MCP
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
868
|
+
check and the `rm -rf` that keeps either one. MCP servers (user-scope json,
|
|
869
|
+
claude-json, toml, project `.mcp.json`) and claude plugins have a `pending` record
|
|
870
|
+
too (a plugin is settled from a live `claude plugin list --json`); only a
|
|
871
|
+
`claude.ai` connector toggle is saved per item (when the next one starts), and a
|
|
872
|
+
kill inside that window leaves its change unrecorded.
|
|
849
873
|
A killed run also leaves its `lock`; the next run's error says so.
|
|
850
874
|
|
|
851
875
|
Each `log.jsonl` row is `{ts, harness, type, name, action, result, batch,
|
|
@@ -956,6 +980,9 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
|
|
|
956
980
|
`/reload-plugins`, an MCP server `/mcp disable|enable <name>`. The toast names it, e.g.
|
|
957
981
|
`⛔ [DISABLED] skill: foo (/reload-skills)`. Agents and rules have no reload and still say
|
|
958
982
|
`takes effect in a new session`. If Claude refuses a queued command, a toast says so.
|
|
983
|
+
`/reload-skills` and `/reload-plugins` only re-read what is on disk. `/mcp disable` also
|
|
984
|
+
leaves the per-project mark that `enable` clears (see
|
|
985
|
+
[MCP backups are verbatim](#mcp-backups-are-verbatim)).
|
|
959
986
|
- `agent-toggle ui` in another terminal applies on Enter and writes the Claude rows it changed to
|
|
960
987
|
`~/.agent-toggle/reload.json`. Every open session running the mod checks that file's mtime every
|
|
961
988
|
5 s (one `stat`) and queues the same reloads, toasting `agent-toggle ui: /reload-skills`. A file
|
|
@@ -996,21 +1023,24 @@ it on macOS, Linux and Windows.
|
|
|
996
1023
|
|
|
997
1024
|
## CI notifications
|
|
998
1025
|
|
|
999
|
-
|
|
1000
|
-
push (a lint-only failure does not page).
|
|
1001
|
-
|
|
1026
|
+
CI and the Release workflow can send a Telegram message when they fail: `ci.yml` when the
|
|
1027
|
+
test matrix fails on a push (a lint-only failure does not page), `release.yml` when the
|
|
1028
|
+
build, PyPI publish or post-publish smoke test fails. Alerts never fire for pull requests or
|
|
1029
|
+
green runs. Both call `.github/workflows/notify-telegram.yml`, and the message names the
|
|
1030
|
+
failure, repository, tag or branch, 7-character commit and a link to the run.
|
|
1002
1031
|
|
|
1003
|
-
|
|
1032
|
+
The [settings menu](#settings--config-menu) hides its Telegram rows (bot token, chat ID,
|
|
1033
|
+
Send test message, Sync CI secrets) for now; `SHOW_TELEGRAM` in `agent_toggle/ui/config_menu.py`
|
|
1034
|
+
brings them back. `config test` and `config sync-ci` still work: they read a bot token and
|
|
1035
|
+
chat ID saved earlier, else `TG_BOT_TOKEN` / `TG_CHAT_ID` from the environment. Without
|
|
1036
|
+
either, set the secrets by hand (below).
|
|
1004
1037
|
|
|
1005
1038
|
```sh
|
|
1006
|
-
agent-toggle config # settings menu: Notifications has the bot token (hidden) and chat id
|
|
1007
1039
|
agent-toggle config test # send one test message
|
|
1008
1040
|
agent-toggle config sync-ci # set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID on the repo
|
|
1009
1041
|
```
|
|
1010
1042
|
|
|
1011
|
-
|
|
1012
|
-
`Esc`, and clears on `-` then `Enter`; the Tools group also has Send test message and Sync CI
|
|
1013
|
-
secrets (the same as `config test` / `config sync-ci`). The bot token
|
|
1043
|
+
When the rows are shown, the bot token
|
|
1014
1044
|
is kept only in the OS credential store (macOS Keychain, Linux Secret Service, Windows DPAPI)
|
|
1015
1045
|
through telegram-kit, never in a file or on a command line, and is shown masked. With no
|
|
1016
1046
|
credential store it refuses to store the token; set `TG_BOT_TOKEN` in the environment instead
|
|
@@ -1026,6 +1056,12 @@ gh secret set TELEGRAM_BOT_TOKEN
|
|
|
1026
1056
|
gh secret set TELEGRAM_CHAT_ID
|
|
1027
1057
|
```
|
|
1028
1058
|
|
|
1059
|
+
To check the repository secrets themselves, send a test message through the same workflow:
|
|
1060
|
+
|
|
1061
|
+
```sh
|
|
1062
|
+
gh workflow run notify-telegram.yml # optional: -f title="..."
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1029
1065
|
With either secret unset (a fork, or a clone you have not configured) the notify job
|
|
1030
1066
|
prints a `::notice::` and exits 0, so it never adds a second red X.
|
|
1031
1067
|
|
|
@@ -58,11 +58,12 @@ def claudeai_connector_names() -> set[str]:
|
|
|
58
58
|
return set(cfg.get("claudeAiMcpEverConnected", []) or [])
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
def toggle_claudeai_connector(action: str, name: str,
|
|
62
|
-
|
|
61
|
+
def toggle_claudeai_connector(action: str, name: str, dry_run: bool = False,
|
|
62
|
+
under: Path | None = None) -> tuple[bool, list | str]:
|
|
63
63
|
"""Fan a claude.ai connector on/off across every EXISTING project entry
|
|
64
64
|
in ~/.claude.json's disabledMcpServers list -- the only place /mcp writes
|
|
65
|
-
that state; no `claude mcp` CLI verb reaches it.
|
|
65
|
+
that state; no `claude mcp` CLI verb reaches it. `under`: only the project
|
|
66
|
+
entries that are that dir or inside it.
|
|
66
67
|
|
|
67
68
|
ponytail: covers projects that exist now; a project dir opened for the
|
|
68
69
|
first time later starts without the entry until this is re-run for it.
|
|
@@ -75,7 +76,8 @@ def toggle_claudeai_connector(action: str, name: str,
|
|
|
75
76
|
|
|
76
77
|
touched = []
|
|
77
78
|
for proj, pdata in cfg.get("projects", {}).items():
|
|
78
|
-
if not isinstance(pdata, dict)
|
|
79
|
+
if not isinstance(pdata, dict) or (
|
|
80
|
+
under is not None and under != Path(proj) and under not in Path(proj).parents):
|
|
79
81
|
continue
|
|
80
82
|
cur = pdata.get("disabledMcpServers") or []
|
|
81
83
|
has = name in cur
|
|
@@ -10,6 +10,7 @@ import os
|
|
|
10
10
|
import shutil
|
|
11
11
|
import subprocess
|
|
12
12
|
import sys
|
|
13
|
+
from pathlib import Path
|
|
13
14
|
from typing import Callable
|
|
14
15
|
|
|
15
16
|
from .. import fs
|
|
@@ -17,6 +18,8 @@ from .. import fs
|
|
|
17
18
|
which = shutil.which
|
|
18
19
|
runner = subprocess.run
|
|
19
20
|
|
|
21
|
+
PLUGIN_SCOPES = ("user", "project", "local") # `claude plugin enable|disable -s`
|
|
22
|
+
|
|
20
23
|
TIMEOUT_ENV = "AGENT_TOGGLE_CLI_TIMEOUT"
|
|
21
24
|
MUTATE_TIMEOUT = 120 # enable / disable / install / mcp add|remove: may fetch from a marketplace
|
|
22
25
|
READ_TIMEOUT = 30 # read-only `plugin list`
|
|
@@ -59,8 +62,8 @@ def run_cli(binary: str | None, args: list[str], cwd: str | None = None,
|
|
|
59
62
|
return False, "CLI not found on PATH"
|
|
60
63
|
limit = _timeout(timeout)
|
|
61
64
|
try:
|
|
62
|
-
p = runner([binary, *args], capture_output=True, text=True,
|
|
63
|
-
timeout=limit, cwd=cwd)
|
|
65
|
+
p = runner([binary, *args], capture_output=True, text=True, encoding="utf-8",
|
|
66
|
+
errors="replace", timeout=limit, cwd=cwd) # Windows: not cp1252
|
|
64
67
|
except subprocess.TimeoutExpired:
|
|
65
68
|
return False, f"CLI timed out after {limit:g}s (raise it with {TIMEOUT_ENV}=<seconds>)"
|
|
66
69
|
except OSError as e: # cwd gone, binary unexecutable
|
|
@@ -68,18 +71,46 @@ def run_cli(binary: str | None, args: list[str], cwd: str | None = None,
|
|
|
68
71
|
return p.returncode == 0, (p.stdout + p.stderr).strip()
|
|
69
72
|
|
|
70
73
|
|
|
71
|
-
def
|
|
72
|
-
"""
|
|
74
|
+
def plugin_where(name: str, claude_home: Path) -> tuple[str | None, str | None]:
|
|
75
|
+
"""(scope, cwd) of plugin `name`'s one install, from Claude Code's install registry
|
|
76
|
+
(`<claude_home>/plugins/installed_plugins.json`; a bare name: its one `name@mkt` id).
|
|
77
|
+
|
|
78
|
+
Without `-s` the CLI picks the scope from its cwd, so a project-scope plugin toggled
|
|
79
|
+
from another dir lands in user scope. (None, None) when the registry cannot say --
|
|
80
|
+
absent, another shape, several installs, a project dir that is gone: the caller
|
|
81
|
+
then lets the CLI auto-detect, as before. A file read, not `claude plugin list`
|
|
82
|
+
(~0.8 s per call, paid on every pane toggle)."""
|
|
83
|
+
try:
|
|
84
|
+
reg = json.loads((claude_home / "plugins" / "installed_plugins.json")
|
|
85
|
+
.read_text(encoding="utf-8"))
|
|
86
|
+
rows = [r for k, v in reg["plugins"].items()
|
|
87
|
+
if k == name or ("@" not in name and k.partition("@")[0] == name) for r in v]
|
|
88
|
+
except (OSError, ValueError, KeyError, TypeError, AttributeError):
|
|
89
|
+
return None, None
|
|
90
|
+
if len(rows) != 1 or not isinstance(rows[0], dict) or rows[0].get("scope") not in PLUGIN_SCOPES:
|
|
91
|
+
return None, None
|
|
92
|
+
scope, proj = rows[0]["scope"], rows[0].get("projectPath")
|
|
93
|
+
if scope == "user":
|
|
94
|
+
return scope, None
|
|
95
|
+
if isinstance(proj, str) and os.path.isabs(proj) and os.path.isdir(proj):
|
|
96
|
+
return scope, proj
|
|
97
|
+
return None, None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def list_plugins(warn: Callable[[str], None], cwd: str | None = None) -> list[dict]:
|
|
101
|
+
"""`claude plugin list --json` through the injectable runner; [] when unavailable.
|
|
102
|
+
`cwd`: its `enabled` is the plugin's state as seen from there."""
|
|
73
103
|
exe = claude_bin()
|
|
74
104
|
if not exe:
|
|
75
105
|
return []
|
|
76
|
-
ok, text = run_cli(exe, ["plugin", "list", "--json"], timeout=READ_TIMEOUT)
|
|
106
|
+
ok, text = run_cli(exe, ["plugin", "list", "--json"], cwd=cwd, timeout=READ_TIMEOUT)
|
|
77
107
|
try: # run_cli merges stderr into the text: skip any notice before the array
|
|
78
108
|
data = json.JSONDecoder().raw_decode(text, max(text.find("["), 0))[0] if ok else None
|
|
79
109
|
except ValueError:
|
|
80
110
|
data = None
|
|
81
111
|
if not isinstance(data, list):
|
|
82
|
-
|
|
112
|
+
why = f" ({text.splitlines()[-1][:120]})" if not ok and text else "" # e.g. a timeout
|
|
113
|
+
warn(f"could not read `claude plugin list --json`{why}; plugin rows come from state only")
|
|
83
114
|
return []
|
|
84
115
|
return [p for p in data if isinstance(p, dict) and isinstance(p.get("id"), str)]
|
|
85
116
|
|