agent-toggle 0.7.3__tar.gz → 0.8.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.
Files changed (88) hide show
  1. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/PKG-INFO +43 -11
  2. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/README.md +42 -10
  3. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/__init__.py +1 -1
  4. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/cli.py +35 -16
  5. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/cost.py +28 -4
  6. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/help.py +4 -2
  7. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/locales/zh_tw_cli.py +1 -0
  8. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/locales/zh_tw_picker.py +2 -0
  9. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/settings.py +1 -1
  10. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/shims/claude.md.tmpl +4 -1
  11. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/shims/generic.md.tmpl +1 -1
  12. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/ui/menu.py +5 -1
  13. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/ui/model.py +28 -8
  14. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/ui/picker.py +9 -3
  15. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle.egg-info/PKG-INFO +43 -11
  16. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle.egg-info/SOURCES.txt +3 -0
  17. agent_toggle-0.8.1/tests/test_all_type.py +74 -0
  18. agent_toggle-0.8.1/tests/test_mods.py +208 -0
  19. agent_toggle-0.8.1/tests/test_same_name.py +48 -0
  20. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/LICENSE +0 -0
  21. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/__main__.py +0 -0
  22. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/backends/__init__.py +0 -0
  23. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/backends/flag_json.py +0 -0
  24. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/backends/mcp_json.py +0 -0
  25. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/backends/mcp_toml.py +0 -0
  26. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/backends/plugin_cli.py +0 -0
  27. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/companions.py +0 -0
  28. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/config.py +0 -0
  29. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/doctor.py +0 -0
  30. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/fs.py +0 -0
  31. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/harnesses.py +0 -0
  32. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/i18n.py +0 -0
  33. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/locales/__init__.py +0 -0
  34. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/locales/zh_tw_common.py +0 -0
  35. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/locales/zh_tw_config.py +0 -0
  36. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/mechanisms.py +0 -0
  37. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/ops.py +0 -0
  38. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/output.py +0 -0
  39. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/profiles.py +0 -0
  40. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/spinner.py +0 -0
  41. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/store.py +0 -0
  42. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/toml_check.py +0 -0
  43. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/ui/__init__.py +0 -0
  44. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/ui/config_menu.py +0 -0
  45. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/ui/theme.py +0 -0
  46. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/undo.py +0 -0
  47. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/update_check.py +0 -0
  48. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle/update_prompt.py +0 -0
  49. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle.egg-info/dependency_links.txt +0 -0
  50. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle.egg-info/entry_points.txt +0 -0
  51. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle.egg-info/requires.txt +0 -0
  52. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/agent_toggle.egg-info/top_level.txt +0 -0
  53. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/pyproject.toml +0 -0
  54. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/setup.cfg +0 -0
  55. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_alias.py +0 -0
  56. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_backends.py +0 -0
  57. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_cli.py +0 -0
  58. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_cli_surface.py +0 -0
  59. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_color.py +0 -0
  60. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_companions.py +0 -0
  61. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_completions.py +0 -0
  62. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_config.py +0 -0
  63. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_config_menu.py +0 -0
  64. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_conformance.py +0 -0
  65. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_containment.py +0 -0
  66. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_cost.py +0 -0
  67. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_crash.py +0 -0
  68. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_doctor.py +0 -0
  69. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_encoding.py +0 -0
  70. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_fs.py +0 -0
  71. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_harnesses.py +0 -0
  72. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_help.py +0 -0
  73. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_i18n.py +0 -0
  74. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_install.py +0 -0
  75. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_mechanisms.py +0 -0
  76. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_menu.py +0 -0
  77. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_picker.py +0 -0
  78. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_platform.py +0 -0
  79. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_profiles.py +0 -0
  80. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_project.py +0 -0
  81. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_safety.py +0 -0
  82. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_secret_audit.py +0 -0
  83. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_settings.py +0 -0
  84. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_store.py +0 -0
  85. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_theme.py +0 -0
  86. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_undo.py +0 -0
  87. {agent_toggle-0.7.3 → agent_toggle-0.8.1}/tests/test_update_check.py +0 -0
  88. {agent_toggle-0.7.3 → agent_toggle-0.8.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.7.3
3
+ Version: 0.8.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
@@ -33,7 +33,7 @@ Dynamic: license-file
33
33
  # agent-toggle
34
34
 
35
35
  Temporarily disable and restore AI-agent resources — skills, agents, commands,
36
- rules, plugins, MCP servers — across Claude Code, Codex, Grok CLI, OpenCode,
36
+ rules, plugins (mods included), MCP servers — across Claude Code, Codex, Grok CLI, OpenCode,
37
37
  OpenClaw, Copilot, Vibe, Devin and Antigravity — and show what each one costs at
38
38
  session start.
39
39
 
@@ -154,13 +154,14 @@ agent-toggle <command> [args] # or: python3 agent_toggle.py <command> [
154
154
 
155
155
  | command | what it does |
156
156
  |---|---|
157
- | `ui` (alias `pick`) | interactive picker — harness tabs, type groups, cost bars, search, sort, profiles (`p`); `--dry-run` shows the plan for what you stage and changes nothing; `--project <dir>` picks in a repo's own scope; see [Interactive picker](#interactive-picker) |
157
+ | `ui` (alias `pick`) | interactive picker — harness tabs, type groups (plus a Mods group), cost bars, search, sort, profiles (`p`); `--dry-run` shows the plan for what you stage and changes nothing; `--project <dir>` picks in a repo's own scope; see [Interactive picker](#interactive-picker) |
158
158
  | `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`) |
159
159
  | `list [type]` | what is currently disabled (`--project <dir>` filters to one project) |
160
160
  | `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) |
161
161
  | `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)) |
162
162
  | `disable <type> <name>...` | park one or more items (`--dry-run` shows the plan; `--project <dir>` for a repo's own `.claude/` and `.mcp.json`) |
163
163
  | `enable <type> <name>...` | put them back (`--dry-run` shows the plan; `--project <dir>` likewise) |
164
+ | `disable\|enable all <name>...` | every type on the harness with that name at once (skill, command, MCP, ...); a name with no match is an `error` row; one batch, so `undo` reverses it |
164
165
  | `enable --all` | put back **every** disabled item (`--harness H` narrows it, `--project <dir>` takes only that project's) |
165
166
  | `undo` | reverse the last logged batch (`--dry-run` shows the plan) |
166
167
  | `profile save\|apply\|diff\|list` | named sets of live items; see [Profiles](#profiles) |
@@ -174,6 +175,8 @@ Every command also works with a leading `--` (`agent-toggle --status`, `--help`,
174
175
 
175
176
  `<type>` = `skill` / `agent` / `command` / `rule` / `plugin` / `mcp`.
176
177
  `rule` is claude-only (`~/.claude/rules/*.md`, parked in `~/.agent-toggle/parked/user/claude/rules/`).
178
+ A **mod** (a claude plugin that loads UI code, see [Mods](#mods)) is type `plugin` on the
179
+ command line: `disable plugin blast-radius@example-marketplace`.
177
180
  Disabling a rule prints a warning (also in `--json` `warnings`, dry run included)
178
181
  that rules may carry safety constraints.
179
182
 
@@ -237,6 +240,7 @@ agent-toggle disable skill demo-skill other-skill
237
240
  agent-toggle disable command demo:batch # nested commands/demo/batch.md
238
241
  agent-toggle disable agent demo-agent
239
242
  agent-toggle disable mcp example-mcp
243
+ agent-toggle disable all example-tool # skill, command, mcp... named example-tool
240
244
  agent-toggle disable skill demo-skill --harness codex
241
245
  agent-toggle enable mcp example-mcp
242
246
  agent-toggle cost --type skill --json
@@ -273,12 +277,14 @@ What is on screen:
273
277
  - **Title bar**: version and a summary (resources, live tokens, staged changes and their token effect).
274
278
  - **Harness tab bar**: `All` plus each enabled harness, with counts. Harnesses switched off in
275
279
  the [settings](#settings--config-menu) are hidden.
276
- - **Rows grouped by type** (Skills, Agents, Commands, ...) under a heading with a count.
280
+ - **Rows grouped by type** (Skills, Agents, Commands, Rules, Plugins, Mods, MCP) under a heading
281
+ with a count.
277
282
  `●` is live, `○` parked (`*` / `o` in ASCII); a `+` / `-` before the name marks a staged change,
278
283
  and `+shared` marks a directory another harness also reads.
279
284
  - **Cost column**: estimated startup tokens (chars / 4, about +-25 %) with a colored bar. A parked
280
285
  row shows `(N)` in brackets, what restoring it would load. Plugins appear as rows too (via
281
286
  `claude plugin list --json`; skipped under `ui --dry-run`, which never shells out).
287
+ Mods get their own **Mods** heading right after Plugins (see [Mods](#mods)).
282
288
  - **Detail pane** at 100 columns or wider: the file's frontmatter `description` (skills,
283
289
  agents, commands, rules), then harness, type, state, staged, path, cost, since,
284
290
  mechanism, and what it is shared with.
@@ -296,7 +302,7 @@ What is on screen:
296
302
  | `Enter` | apply every staged change (with `--dry-run`: show the plan) |
297
303
  | `Esc` / `Ctrl-C` | cancel; nothing is applied |
298
304
  | `↑` `↓` `PgUp` `PgDn` `Home` `End`, `Ctrl-P` / `Ctrl-N` | move |
299
- | `/` | start a search; any other non-command letter starts one too. Terms are ANDed, case-insensitive, and match the harness/type/name and the path; when nothing matches, letters-in-order (fuzzy) is tried: `ctxmd` finds `context-md`, and the bar says `≈ fuzzy match` |
305
+ | `/` | start a search; any other non-command letter starts one too. Terms are ANDed, case-insensitive, and match the harness/type/name, the group (`mod` or `mods` finds every mod) and the path; when nothing matches, letters-in-order (fuzzy) is tried: `ctxmd` finds `context-md`, and the bar says `≈ fuzzy match` |
300
306
  | `Backspace` / `Ctrl-U` | delete one character / clear the filter |
301
307
  | `←` `→` | switch harness tab (also while typing a filter); `0` = All, `1`-`9` = that tab, `h` = next tab |
302
308
  | `t` | cycle the type filter |
@@ -304,8 +310,9 @@ What is on screen:
304
310
  | `p` | profiles: stage a saved one, or save the live state (see below) |
305
311
  | `?` | show the key list |
306
312
  | `a` / `Ctrl-A` | toggle every visible row |
313
+ | `n` | toggle every row on this row's harness with the same name, of any type (skill, command, MCP, ...), shown or filtered out; like `disable all <name>` |
307
314
 
308
- The command keys (`t`, `s`, `p`, `h`, `a`, `0`-`9`, `?`) act only while no filter is active.
315
+ The command keys (`t`, `s`, `p`, `h`, `a`, `n`, `0`-`9`, `?`) act only while no filter is active.
309
316
  Press `/` first to type a filter that begins with one of them; once the filter is non-empty,
310
317
  every letter just types.
311
318
 
@@ -331,11 +338,35 @@ Built on stdlib `curses`, so there is nothing to install on macOS and Linux
331
338
  (on Windows, `pip install "agent-toggle[windows]"` pulls `windows-curses`).
332
339
  Without curses, `ui` falls back to a numbered menu with the same staging and the
333
340
  same result. It is grouped by type, with the same glyphs and color: type row
334
- numbers (`1 3 5-7`) to tick or untick, `/text` to filter (`/` alone clears it),
341
+ numbers (`1 3 5-7`) to tick or untick, `n <number>` to toggle every type with that
342
+ row's name, `/text` to filter (`/` alone clears it),
335
343
  `s` / `h` / `t` to sort and cycle the harness and type filters, `p` to list profiles,
336
344
  `p <number|name>` to stage one and `p save <name>` to save the live state, `a` to
337
345
  apply, `q` (or end of input) to cancel.
338
346
 
347
+ ### Mods
348
+
349
+ A mod is a claude plugin whose `hooks/hooks.json` has a top-level `modules` key, for example
350
+ `{ "modules": ["./register.tsx"] }`: it loads `.mjs` / `.tsx` code that can draw UI (a pane,
351
+ a band above the prompt, buttons) and change Claude Code's behaviour. A plugin with no
352
+ `hooks.json`, or with only ordinary hooks (shell commands such as `PreToolUse`), is a plain
353
+ plugin: it ships skills, commands, agents, MCP servers or hooks and draws nothing.
354
+
355
+ - The check reads the install path `claude plugin list` reports and every cached version
356
+ under `~/.claude/plugins/cache/<marketplace>/<name>/`, so a parked mod is still a mod.
357
+ To list the installed mods by hand:
358
+
359
+ ```sh
360
+ grep -l modules ~/.claude/plugins/cache/*/*/*/hooks/hooks.json
361
+ ```
362
+
363
+ - The picker lists mods under **Mods**, right after Plugins. `t` cycles to it, the
364
+ `picker_type` setting can open on it, and a search for `mod` or `mods` finds every mod.
365
+ The detail pane shows its type as `mod (plugin)`.
366
+ - A mod is still a plugin everywhere else: it toggles through `claude plugin
367
+ enable/disable`, `disable plugin <id>` reaches it, profiles store it as `plugin`, and
368
+ `list` / `cost` show it with the plugins (`cost --json` marks it `"mod": true`).
369
+
339
370
  The chrome (title, tabs, footer, help, detail labels) is translated when `language` is
340
371
  `zh-TW`; item names and paths are never translated.
341
372
 
@@ -377,7 +408,7 @@ tool does not know are kept):
377
408
  | `harness.<name>` | `true` / `false`, for `claude` `codex` `grok` `opencode` `openclaw` `copilot` `vibe` `devin` `agy` | `true` | |
378
409
  | `picker_sort` | `name` / `cost` | `name` | |
379
410
  | `picker_harness` | `all` or a harness name | `all` | |
380
- | `picker_type` | `all` or a type | `all` | |
411
+ | `picker_type` | `all`, a type, or `mod` | `all` | |
381
412
  | `telegram_chat_id` | a number, `-100...` or `@channel` | unset | `TG_CHAT_ID` |
382
413
 
383
414
  The bot token is not in this file: it is in the OS keystore through telegram-kit
@@ -432,7 +463,7 @@ translated when `language` is `zh-TW`.
432
463
 
433
464
  | harness | home | skill | agent | command | rule | plugin | mcp |
434
465
  |---|---|:-:|:-:|:-:|:-:|:-:|:-:|
435
- | claude | `~/.claude` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ `~/.claude.json` |
466
+ | claude | `~/.claude` | ✓ | ✓ | ✓ | ✓ | ✓ (mods too) | ✓ `~/.claude.json` |
436
467
  | codex | `~/.codex` | ✓ | ✓ | ✓ (`commands/` + `prompts/`) | — | — | ✓ `config.toml` |
437
468
  | grok | `~/.grok` | ✓ | — | — | — | — | ✓ `config.toml` |
438
469
  | opencode | `$XDG_CONFIG_HOME/opencode`, else `~/.config/opencode` | ✓ | — | ✓ (`command/`) | — | — | ✓ `opencode.json` (flag, *assumed*) |
@@ -842,8 +873,9 @@ Both sides are resolved first.
842
873
  ## Design and roadmap
843
874
 
844
875
  Architecture, harness survey, cost model, known gaps and the phased roadmap
845
- toward a public multi-OS release live in `docs/DESIGN.md`. Release notes are in
846
- `CHANGELOG.md`.
876
+ toward a public multi-OS release live in `docs/DESIGN.md`. The design for the
877
+ Claude Code pane mod (the picker inside Claude, driven by this CLI) is in
878
+ `docs/MOD.md`. Release notes are in `CHANGELOG.md`.
847
879
 
848
880
  ## Tests
849
881
 
@@ -1,7 +1,7 @@
1
1
  # agent-toggle
2
2
 
3
3
  Temporarily disable and restore AI-agent resources — skills, agents, commands,
4
- rules, plugins, MCP servers — across Claude Code, Codex, Grok CLI, OpenCode,
4
+ rules, plugins (mods included), MCP servers — across Claude Code, Codex, Grok CLI, OpenCode,
5
5
  OpenClaw, Copilot, Vibe, Devin and Antigravity — and show what each one costs at
6
6
  session start.
7
7
 
@@ -122,13 +122,14 @@ agent-toggle <command> [args] # or: python3 agent_toggle.py <command> [
122
122
 
123
123
  | command | what it does |
124
124
  |---|---|
125
- | `ui` (alias `pick`) | interactive picker — harness tabs, type groups, cost bars, search, sort, profiles (`p`); `--dry-run` shows the plan for what you stage and changes nothing; `--project <dir>` picks in a repo's own scope; see [Interactive picker](#interactive-picker) |
125
+ | `ui` (alias `pick`) | interactive picker — harness tabs, type groups (plus a Mods group), cost bars, search, sort, profiles (`p`); `--dry-run` shows the plan for what you stage and changes nothing; `--project <dir>` picks in a repo's own scope; see [Interactive picker](#interactive-picker) |
126
126
  | `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`) |
127
127
  | `list [type]` | what is currently disabled (`--project <dir>` filters to one project) |
128
128
  | `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) |
129
129
  | `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)) |
130
130
  | `disable <type> <name>...` | park one or more items (`--dry-run` shows the plan; `--project <dir>` for a repo's own `.claude/` and `.mcp.json`) |
131
131
  | `enable <type> <name>...` | put them back (`--dry-run` shows the plan; `--project <dir>` likewise) |
132
+ | `disable\|enable all <name>...` | every type on the harness with that name at once (skill, command, MCP, ...); a name with no match is an `error` row; one batch, so `undo` reverses it |
132
133
  | `enable --all` | put back **every** disabled item (`--harness H` narrows it, `--project <dir>` takes only that project's) |
133
134
  | `undo` | reverse the last logged batch (`--dry-run` shows the plan) |
134
135
  | `profile save\|apply\|diff\|list` | named sets of live items; see [Profiles](#profiles) |
@@ -142,6 +143,8 @@ Every command also works with a leading `--` (`agent-toggle --status`, `--help`,
142
143
 
143
144
  `<type>` = `skill` / `agent` / `command` / `rule` / `plugin` / `mcp`.
144
145
  `rule` is claude-only (`~/.claude/rules/*.md`, parked in `~/.agent-toggle/parked/user/claude/rules/`).
146
+ A **mod** (a claude plugin that loads UI code, see [Mods](#mods)) is type `plugin` on the
147
+ command line: `disable plugin blast-radius@example-marketplace`.
145
148
  Disabling a rule prints a warning (also in `--json` `warnings`, dry run included)
146
149
  that rules may carry safety constraints.
147
150
 
@@ -205,6 +208,7 @@ agent-toggle disable skill demo-skill other-skill
205
208
  agent-toggle disable command demo:batch # nested commands/demo/batch.md
206
209
  agent-toggle disable agent demo-agent
207
210
  agent-toggle disable mcp example-mcp
211
+ agent-toggle disable all example-tool # skill, command, mcp... named example-tool
208
212
  agent-toggle disable skill demo-skill --harness codex
209
213
  agent-toggle enable mcp example-mcp
210
214
  agent-toggle cost --type skill --json
@@ -241,12 +245,14 @@ What is on screen:
241
245
  - **Title bar**: version and a summary (resources, live tokens, staged changes and their token effect).
242
246
  - **Harness tab bar**: `All` plus each enabled harness, with counts. Harnesses switched off in
243
247
  the [settings](#settings--config-menu) are hidden.
244
- - **Rows grouped by type** (Skills, Agents, Commands, ...) under a heading with a count.
248
+ - **Rows grouped by type** (Skills, Agents, Commands, Rules, Plugins, Mods, MCP) under a heading
249
+ with a count.
245
250
  `●` is live, `○` parked (`*` / `o` in ASCII); a `+` / `-` before the name marks a staged change,
246
251
  and `+shared` marks a directory another harness also reads.
247
252
  - **Cost column**: estimated startup tokens (chars / 4, about +-25 %) with a colored bar. A parked
248
253
  row shows `(N)` in brackets, what restoring it would load. Plugins appear as rows too (via
249
254
  `claude plugin list --json`; skipped under `ui --dry-run`, which never shells out).
255
+ Mods get their own **Mods** heading right after Plugins (see [Mods](#mods)).
250
256
  - **Detail pane** at 100 columns or wider: the file's frontmatter `description` (skills,
251
257
  agents, commands, rules), then harness, type, state, staged, path, cost, since,
252
258
  mechanism, and what it is shared with.
@@ -264,7 +270,7 @@ What is on screen:
264
270
  | `Enter` | apply every staged change (with `--dry-run`: show the plan) |
265
271
  | `Esc` / `Ctrl-C` | cancel; nothing is applied |
266
272
  | `↑` `↓` `PgUp` `PgDn` `Home` `End`, `Ctrl-P` / `Ctrl-N` | move |
267
- | `/` | start a search; any other non-command letter starts one too. Terms are ANDed, case-insensitive, and match the harness/type/name and the path; when nothing matches, letters-in-order (fuzzy) is tried: `ctxmd` finds `context-md`, and the bar says `≈ fuzzy match` |
273
+ | `/` | start a search; any other non-command letter starts one too. Terms are ANDed, case-insensitive, and match the harness/type/name, the group (`mod` or `mods` finds every mod) and the path; when nothing matches, letters-in-order (fuzzy) is tried: `ctxmd` finds `context-md`, and the bar says `≈ fuzzy match` |
268
274
  | `Backspace` / `Ctrl-U` | delete one character / clear the filter |
269
275
  | `←` `→` | switch harness tab (also while typing a filter); `0` = All, `1`-`9` = that tab, `h` = next tab |
270
276
  | `t` | cycle the type filter |
@@ -272,8 +278,9 @@ What is on screen:
272
278
  | `p` | profiles: stage a saved one, or save the live state (see below) |
273
279
  | `?` | show the key list |
274
280
  | `a` / `Ctrl-A` | toggle every visible row |
281
+ | `n` | toggle every row on this row's harness with the same name, of any type (skill, command, MCP, ...), shown or filtered out; like `disable all <name>` |
275
282
 
276
- The command keys (`t`, `s`, `p`, `h`, `a`, `0`-`9`, `?`) act only while no filter is active.
283
+ The command keys (`t`, `s`, `p`, `h`, `a`, `n`, `0`-`9`, `?`) act only while no filter is active.
277
284
  Press `/` first to type a filter that begins with one of them; once the filter is non-empty,
278
285
  every letter just types.
279
286
 
@@ -299,11 +306,35 @@ Built on stdlib `curses`, so there is nothing to install on macOS and Linux
299
306
  (on Windows, `pip install "agent-toggle[windows]"` pulls `windows-curses`).
300
307
  Without curses, `ui` falls back to a numbered menu with the same staging and the
301
308
  same result. It is grouped by type, with the same glyphs and color: type row
302
- numbers (`1 3 5-7`) to tick or untick, `/text` to filter (`/` alone clears it),
309
+ numbers (`1 3 5-7`) to tick or untick, `n <number>` to toggle every type with that
310
+ row's name, `/text` to filter (`/` alone clears it),
303
311
  `s` / `h` / `t` to sort and cycle the harness and type filters, `p` to list profiles,
304
312
  `p <number|name>` to stage one and `p save <name>` to save the live state, `a` to
305
313
  apply, `q` (or end of input) to cancel.
306
314
 
315
+ ### Mods
316
+
317
+ A mod is a claude plugin whose `hooks/hooks.json` has a top-level `modules` key, for example
318
+ `{ "modules": ["./register.tsx"] }`: it loads `.mjs` / `.tsx` code that can draw UI (a pane,
319
+ a band above the prompt, buttons) and change Claude Code's behaviour. A plugin with no
320
+ `hooks.json`, or with only ordinary hooks (shell commands such as `PreToolUse`), is a plain
321
+ plugin: it ships skills, commands, agents, MCP servers or hooks and draws nothing.
322
+
323
+ - The check reads the install path `claude plugin list` reports and every cached version
324
+ under `~/.claude/plugins/cache/<marketplace>/<name>/`, so a parked mod is still a mod.
325
+ To list the installed mods by hand:
326
+
327
+ ```sh
328
+ grep -l modules ~/.claude/plugins/cache/*/*/*/hooks/hooks.json
329
+ ```
330
+
331
+ - The picker lists mods under **Mods**, right after Plugins. `t` cycles to it, the
332
+ `picker_type` setting can open on it, and a search for `mod` or `mods` finds every mod.
333
+ The detail pane shows its type as `mod (plugin)`.
334
+ - A mod is still a plugin everywhere else: it toggles through `claude plugin
335
+ enable/disable`, `disable plugin <id>` reaches it, profiles store it as `plugin`, and
336
+ `list` / `cost` show it with the plugins (`cost --json` marks it `"mod": true`).
337
+
307
338
  The chrome (title, tabs, footer, help, detail labels) is translated when `language` is
308
339
  `zh-TW`; item names and paths are never translated.
309
340
 
@@ -345,7 +376,7 @@ tool does not know are kept):
345
376
  | `harness.<name>` | `true` / `false`, for `claude` `codex` `grok` `opencode` `openclaw` `copilot` `vibe` `devin` `agy` | `true` | |
346
377
  | `picker_sort` | `name` / `cost` | `name` | |
347
378
  | `picker_harness` | `all` or a harness name | `all` | |
348
- | `picker_type` | `all` or a type | `all` | |
379
+ | `picker_type` | `all`, a type, or `mod` | `all` | |
349
380
  | `telegram_chat_id` | a number, `-100...` or `@channel` | unset | `TG_CHAT_ID` |
350
381
 
351
382
  The bot token is not in this file: it is in the OS keystore through telegram-kit
@@ -400,7 +431,7 @@ translated when `language` is `zh-TW`.
400
431
 
401
432
  | harness | home | skill | agent | command | rule | plugin | mcp |
402
433
  |---|---|:-:|:-:|:-:|:-:|:-:|:-:|
403
- | claude | `~/.claude` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ `~/.claude.json` |
434
+ | claude | `~/.claude` | ✓ | ✓ | ✓ | ✓ | ✓ (mods too) | ✓ `~/.claude.json` |
404
435
  | codex | `~/.codex` | ✓ | ✓ | ✓ (`commands/` + `prompts/`) | — | — | ✓ `config.toml` |
405
436
  | grok | `~/.grok` | ✓ | — | — | — | — | ✓ `config.toml` |
406
437
  | opencode | `$XDG_CONFIG_HOME/opencode`, else `~/.config/opencode` | ✓ | — | ✓ (`command/`) | — | — | ✓ `opencode.json` (flag, *assumed*) |
@@ -810,8 +841,9 @@ Both sides are resolved first.
810
841
  ## Design and roadmap
811
842
 
812
843
  Architecture, harness survey, cost model, known gaps and the phased roadmap
813
- toward a public multi-OS release live in `docs/DESIGN.md`. Release notes are in
814
- `CHANGELOG.md`.
844
+ toward a public multi-OS release live in `docs/DESIGN.md`. The design for the
845
+ Claude Code pane mod (the picker inside Claude, driven by this CLI) is in
846
+ `docs/MOD.md`. Release notes are in `CHANGELOG.md`.
815
847
 
816
848
  ## Tests
817
849
 
@@ -1,3 +1,3 @@
1
1
  """agent-toggle: temporarily disable / re-enable AI-agent resources."""
2
2
 
3
- __version__ = "0.7.3"
3
+ __version__ = "0.8.1"
@@ -67,6 +67,7 @@ from .backends.plugin_cli import claude_bin
67
67
  from .harnesses import TYPES, harness_of, harnesses, project_view
68
68
  from .mechanisms import (
69
69
  dir_view,
70
+ fail_row,
70
71
  legacy_parks,
71
72
  migrate_parks,
72
73
  settle,
@@ -285,7 +286,7 @@ def cmd_cost(state: dict, out: Result, harness: str | None = None,
285
286
  + (tail if i.enabled else out.paint(tail, "dim")))
286
287
  out.row(i.harness, i.type, i.name, "cost", "ok", i.basis, show=False,
287
288
  enabled=i.enabled, tokens=i.tokens, would_save=i.would_save,
288
- chars=i.chars, shared_with=list(i.shared_with))
289
+ chars=i.chars, shared_with=list(i.shared_with), mod=i.mod)
289
290
  live = sum(i.tokens for i in items)
290
291
  saved = sum(i.would_save for i in items)
291
292
  out.say(f"\n{len(items)} item(s): ~{live} tok loaded at startup; "
@@ -337,21 +338,39 @@ def cmd_toggle(args: argparse.Namespace, out: Result) -> None:
337
338
  if harness != "claude":
338
339
  die(f"--project supports only the claude layout, not {harness}", 4)
339
340
  h = project_view(args.project)
340
- if type_ not in h.types:
341
+ if type_ not in (*h.types, "all"):
341
342
  die(f"--project supports {', '.join(h.types)} (not {type_})", 4)
342
- ops.apply_plan([ops.Op(harness, type_, action, n, str(h.project)) for n in names],
343
- out, args.dry_run, batch=store.BATCH)
344
- return
345
- h = harness_of(harness)
346
- home, supported = h.home, h.types
347
- if not home.is_dir():
348
- die(f"{harness} is not installed ({home} does not exist)", 4)
349
- if type_ not in supported:
350
- have = f"it has: {', '.join(supported)}" if supported else "no supported types"
351
- die(f"{harness} has no {type_} support ({have})", 4)
352
-
353
- ops.apply_plan([ops.Op(harness, type_, action, n) for n in names], out, args.dry_run,
354
- batch=store.BATCH)
343
+ project = str(h.project)
344
+ else:
345
+ h, project = harness_of(harness), None
346
+ home, supported = h.home, h.types
347
+ if not home.is_dir():
348
+ die(f"{harness} is not installed ({home} does not exist)", 4)
349
+ if type_ not in (*supported, "all"):
350
+ have = f"it has: {', '.join(supported)}" if supported else "no supported types"
351
+ die(f"{harness} has no {type_} support ({have})", 4)
352
+ pairs = [(type_, n) for n in names]
353
+ if type_ == "all":
354
+ pairs = _every_type(action, names, harness, h.project)
355
+ for name in sorted(set(names) - {n for _, n in pairs}):
356
+ fail_row(out, args.dry_run, harness, "all", action, name,
357
+ f"no {'live' if action == 'disable' else 'disabled'} item named {name} "
358
+ f"on {harness}", batch=store.BATCH, project=project)
359
+ ops.apply_plan([ops.Op(harness, t, action, n, project) for t, n in pairs], out,
360
+ args.dry_run, batch=store.BATCH)
361
+
362
+
363
+ def _every_type(action: str, names: list[str], harness: str,
364
+ project: Path | None) -> list[tuple[str, str]]:
365
+ """`all`: each (type, name) on `harness` that `action` can act on -- live items for
366
+ disable, parked ones for enable -- in one scope (user, or the --project dir)."""
367
+ state, table = profiles.scope(load_state(write_back=False), project)
368
+ want = action == "disable"
369
+ # plugin ids are `name@marketplace`: skip the claude CLI call unless one could match
370
+ plugins = project is None and any("@" in n for n in names)
371
+ return sorted({(i.type, i.name) for i in cost.inventory(state, table, plugins=plugins)
372
+ if i.name in names and i.enabled == want
373
+ and harness in (i.harness, *i.shared_with)})
355
374
 
356
375
 
357
376
  def cmd_migrate(out: Result) -> None:
@@ -690,7 +709,7 @@ def build_parser() -> argparse.ArgumentParser:
690
709
  up2.add_argument("--dry-run", action="store_true", help="show the plan; change nothing")
691
710
  for name, verb in (("disable", "park"), ("enable", "restore")):
692
711
  sp = sub.add_parser(name, parents=[common], help=f"{verb} one or more items")
693
- sp.add_argument("type", choices=TYPES, nargs="?" if name == "enable" else None)
712
+ sp.add_argument("type", choices=(*TYPES, "all"), nargs="?" if name == "enable" else None)
694
713
  sp.add_argument("names", nargs="*" if name == "enable" else "+", metavar="name")
695
714
  sp.add_argument("--dry-run", action="store_true",
696
715
  help="show the plan; change nothing")
@@ -71,6 +71,7 @@ class Item:
71
71
  basis: str
72
72
  shared_with: tuple[str, ...] = ()
73
73
  description: str = "" # frontmatter description, when the file has one
74
+ mod: bool = False # a plugin that loads UI modules (see is_mod)
74
75
 
75
76
 
76
77
  def tokens_of(chars: int) -> int:
@@ -206,6 +207,26 @@ def backup_tools(entry: dict) -> int | None:
206
207
  return _entry_tools(raw)
207
208
 
208
209
 
210
+ def is_mod(root: Path) -> bool:
211
+ """A mod is a plugin whose hooks/hooks.json has a top-level `modules` key: it loads
212
+ .mjs/.tsx code that draws UI; plain `hooks` (shell commands) do not make one."""
213
+ try:
214
+ data = json.loads((root / "hooks" / "hooks.json").read_text(encoding="utf-8"))
215
+ except (OSError, ValueError):
216
+ return False
217
+ return isinstance(data, dict) and "modules" in data
218
+
219
+
220
+ def plugin_is_mod(cache: Path | None, pid: str, root: object = None) -> bool:
221
+ """`pid` (`name@marketplace`) is a mod at its reported install path, or in any cached
222
+ version under `cache/<marketplace>/<name>/`: the CLI's path can differ, and a parked
223
+ plugin has none."""
224
+ if isinstance(root, str) and is_mod(Path(root)):
225
+ return True
226
+ name, _, market = pid.partition("@")
227
+ return bool(cache and name and market) and any(is_mod(d) for d in (cache / market / name).glob("*"))
228
+
229
+
209
230
  def plugin_estimate(root: Path) -> Estimate:
210
231
  """A plugin bundle: sum of what it ships, read from its install path."""
211
232
  parts = [("skill", d.name, d / "SKILL.md") for d in sorted((root / "skills").glob("*"))
@@ -253,13 +274,14 @@ def inventory(state: dict, table: dict, warn: Callable[[str], None] = lambda m:
253
274
  if plugins and claude and claude.home.is_dir() and "plugin" in claude.types
254
275
  else None)
255
276
  deferred: list[tuple[Item, Callable[[], Estimate]]] = [] # file reads, run in parallel below
277
+ cache = claude.home / "plugins" / "cache" if claude else None
256
278
 
257
- def add(h: str, t: str, n: str, enabled: bool, est, shared=()) -> None:
279
+ def add(h: str, t: str, n: str, enabled: bool, est, shared=(), mod: bool = False) -> None:
258
280
  """`est` is an Estimate, or a no-arg callable returning one (read later, in a pool)."""
259
281
  enabled = enabled and (h, t, n) not in flagged
260
282
  if (h, t, n) not in seen:
261
283
  seen.add((h, t, n))
262
- item = Item(h, t, n, enabled, 0, 0, None, "", tuple(shared))
284
+ item = Item(h, t, n, enabled, 0, 0, None, "", tuple(shared), mod=mod)
263
285
  items.append(item)
264
286
  if callable(est):
265
287
  deferred.append((item, est))
@@ -300,7 +322,8 @@ def inventory(state: dict, table: dict, warn: Callable[[str], None] = lambda m:
300
322
  root = p.get("installPath")
301
323
  add("claude", "plugin", p["id"], p.get("enabled", True) is not False,
302
324
  (lambda r=root: plugin_estimate(Path(r))) if isinstance(root, str)
303
- else (0, None, "no installPath"))
325
+ else (0, None, "no installPath"),
326
+ mod=plugin_is_mod(cache, p["id"], root))
304
327
 
305
328
  for e in state.get("disabled", {}).values():
306
329
  h, t, n = e.get("harness", "claude"), e.get("type"), e.get("name")
@@ -319,7 +342,8 @@ def inventory(state: dict, table: dict, warn: Callable[[str], None] = lambda m:
319
342
  return file_estimate(t, n, Path(p) if isinstance(p, str) and p else None)
320
343
  shared = e.get("shared_with")
321
344
  add(h, t, n, False, est,
322
- [x for x in shared if isinstance(x, str)] if isinstance(shared, list) else ())
345
+ [x for x in shared if isinstance(x, str)] if isinstance(shared, list) else (),
346
+ mod=t == "plugin" and h == "claude" and plugin_is_mod(cache, n))
323
347
  with pool:
324
348
  for (item, _), est in zip(deferred, pool.map(lambda d: d[1](), deferred)):
325
349
  _fill(item, est)
@@ -37,7 +37,8 @@ def _dry_run() -> tuple[str, str]:
37
37
 
38
38
 
39
39
  def _type_arg() -> tuple[str, str]:
40
- return ("<type>", " | ".join(TYPES))
40
+ return ("<type>", " | ".join(TYPES) + " | all "
41
+ + t("help.arg.all", "(every type with that name)"))
41
42
 
42
43
 
43
44
  def commands(default_harness: str = "claude") -> dict[str, Cmd]:
@@ -57,7 +58,8 @@ def commands(default_harness: str = "claude") -> dict[str, Cmd]:
57
58
  "Without --harness this acts on %s.", default_harness),
58
59
  args=[_type_arg(), ("<name>...", t("help.arg.names", "one or more item names"))],
59
60
  options=[_dry_run(), _project()],
60
- examples=["disable skill my-skill", "disable mcp my-server --harness codex",
61
+ examples=["disable skill my-skill", "disable all my-tool",
62
+ "disable mcp my-server --harness codex",
61
63
  "disable agent my-agent --project . --dry-run"]),
62
64
  "enable": Cmd(
63
65
  "enable <type> <name>... | enable --all [--dry-run] [--project dir]",
@@ -13,6 +13,7 @@ CATALOG: dict[str, str] = {
13
13
  "help.disable.summary": "停用(暫存)一或多個項目",
14
14
  "help.disable.desc": "把項目移出 harness 的視野,不會刪除任何東西。未指定 --harness 時作用於 %s。",
15
15
  "help.enable.summary": "還原一或多個項目",
16
+ "help.arg.all": "(同名的所有類型)",
16
17
  "help.enable.desc": "把暫存的項目放回去。未指定 --harness 時作用於 %s;"
17
18
  "--all 還原全部(可搭配一個 --harness)。",
18
19
  "help.undo.summary": "復原最近一次記錄的批次",
@@ -12,6 +12,7 @@ CATALOG: dict[str, str] = {
12
12
  "picker.type.command": "指令",
13
13
  "picker.type.rule": "規則",
14
14
  "picker.type.plugin": "外掛",
15
+ "picker.type.mod": "Mods",
15
16
  "picker.type.mcp": "MCP",
16
17
  "picker.sort.name": "名稱",
17
18
  "picker.sort.cost": "成本",
@@ -73,6 +74,7 @@ CATALOG: dict[str, str] = {
73
74
  "picker.help.sort": "循環切換排序:名稱 / 成本(由大到小)",
74
75
  "picker.help.profile": "設定檔:暫存一個,或儲存目前啟用的狀態",
75
76
  "picker.help.all": "切換所有可見的列",
77
+ "picker.help.same_name": "切換此 harness 上同名的所有類型",
76
78
  "picker.help.help": "顯示此說明",
77
79
  "picker.help.cost": "成本 = 工作階段開始時載入的約略 token 數(字元數/4)。",
78
80
  "picker.help.close": "按任意鍵關閉說明",
@@ -41,7 +41,7 @@ DEFAULTS: dict[str, Spec] = {
41
41
  **{f"harness.{n}": Spec("bool", True) for n in HARNESS_NAMES},
42
42
  "picker_sort": Spec("choice", "name", ("name", "cost")),
43
43
  "picker_harness": Spec("choice", "all", ("all", *HARNESS_NAMES)),
44
- "picker_type": Spec("choice", "all", ("all", *harnesses.TYPES)),
44
+ "picker_type": Spec("choice", "all", ("all", *harnesses.TYPES, "mod")),
45
45
  "language": Spec("choice", "en", ("en", "zh-TW"), "AGENT_TOGGLE_LANG"),
46
46
  "telegram_chat_id": Spec("chat_id"),
47
47
  }
@@ -17,12 +17,15 @@ python3 __AGENT_TOGGLE_ROOT__/agent_toggle.py <command> --json # if agent-togg
17
17
  ```
18
18
 
19
19
  Commands: `status` (run first), `doctor` (read-only health check), `list [type]`,
20
- `cost [--type T]` (startup token estimates, biggest first), `disable <type> <name>...`,
20
+ `cost [--type T]` (startup token estimates, biggest first), `disable <type> <name>...` (type `all`: every type with that name),
21
21
  `enable <type> <name>...`, `enable --all`, `undo` (reverse the last batch),
22
22
  `profile save|apply|diff <name>`, `profile list`, `migrate`; add `--harness claude|codex|grok|opencode|openclaw|copilot|vibe|devin|agy`
23
23
  (default `claude`) and `--dry-run` to preview. `--project <dir>` targets a repo's own `.claude/` and `.mcp.json`
24
24
  (warn the user: git shows the deletion). `<type>` = `skill` / `agent` /
25
25
  `command` / `rule` / `plugin` / `mcp`. A colon addresses nesting: `orch:batch`.
26
+ A mod (a plugin whose `hooks/hooks.json` has a `modules` key) is type `plugin`;
27
+ `cost --type plugin --json` marks each one `"mod": true`, so "turn off the mods" means
28
+ disabling those plugin ids.
26
29
  A row with `shared_with` means another harness reads the same directory (OpenCode
27
30
  aliasing a skills dir): the change affects them too, so say so.
28
31
  `ui` is an interactive picker for the user, not for you.
@@ -21,7 +21,7 @@ You are running in `__HARNESS__`: pass `--harness __HARNESS__` for its own items
21
21
 
22
22
  Commands: `status` (run first; shows which types this harness supports),
23
23
  `doctor` (read-only health check), `list [type]`, `cost [--type T]` (startup
24
- token estimates, biggest first), `disable <type> <name>...`, `enable <type> <name>...`,
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 <name>`,
26
26
  `profile list`; add `--dry-run` to preview. `<type>` = `skill` / `agent` /
27
27
  `command` / `rule` / `plugin` / `mcp`. A colon addresses nesting: `orch:batch`.
@@ -17,6 +17,7 @@ from .model import (
17
17
  prefs,
18
18
  profile_command,
19
19
  profile_listing,
20
+ toggle_same_name,
20
21
  type_label,
21
22
  visible,
22
23
  )
@@ -106,9 +107,12 @@ def loop(rows: list[Row], stdin, stdout, project: Path | None = None,
106
107
  f"{profile_listing()}\np <number|name> | p save <name>")
107
108
  + "\n")
108
109
  redraw = bool(line)
110
+ elif low.startswith("n ") and (idx := _numbers(cmd[2:], len(shown))) is not None:
111
+ for i in idx: # every type with that row's name
112
+ toggle_same_name(kept, shown[i])
109
113
  elif low in ("?", "help"):
110
114
  stdout.write("Numbers tick/untick rows (unticked = parked); nothing is applied "
111
- "until `a`. `p` lists profiles; `p <number|name>` stages one, "
115
+ "until `a`. `n <number>` toggles every type with that row's name. `p` lists profiles; `p <number|name>` stages one, "
112
116
  "`p save <name>` saves the live state.\n")
113
117
  redraw = False
114
118
  elif cmd and (idx := _numbers(cmd, len(shown))) is not None: