agent-toggle 0.18.0__tar.gz → 0.19.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 (92) hide show
  1. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/PKG-INFO +21 -8
  2. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/README.md +20 -7
  3. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/__init__.py +1 -1
  4. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/mechanisms.py +34 -50
  5. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle.egg-info/PKG-INFO +21 -8
  6. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_all_type.py +1 -4
  7. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_backends.py +39 -4
  8. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_conformance.py +5 -5
  9. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_crash.py +44 -15
  10. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_safety.py +14 -14
  11. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_secret_audit.py +19 -0
  12. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/LICENSE +0 -0
  13. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/__main__.py +0 -0
  14. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/backends/__init__.py +0 -0
  15. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/backends/flag_json.py +0 -0
  16. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/backends/mcp_json.py +0 -0
  17. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/backends/mcp_toml.py +0 -0
  18. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/backends/plugin_cli.py +0 -0
  19. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/cli.py +0 -0
  20. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/companions.py +0 -0
  21. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/completions.py +0 -0
  22. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/config.py +0 -0
  23. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/cost.py +0 -0
  24. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/doctor.py +0 -0
  25. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/fs.py +0 -0
  26. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/harnesses.py +0 -0
  27. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/help.py +0 -0
  28. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/i18n.py +0 -0
  29. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/locales/__init__.py +0 -0
  30. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/locales/zh_tw_cli.py +0 -0
  31. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/locales/zh_tw_common.py +0 -0
  32. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/locales/zh_tw_config.py +0 -0
  33. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/locales/zh_tw_picker.py +0 -0
  34. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/ops.py +0 -0
  35. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/output.py +0 -0
  36. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/profiles.py +0 -0
  37. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/settings.py +0 -0
  38. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/shims/claude.md.tmpl +0 -0
  39. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/shims/generic.md.tmpl +0 -0
  40. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/spinner.py +0 -0
  41. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/store.py +0 -0
  42. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/toml_check.py +0 -0
  43. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/ui/__init__.py +0 -0
  44. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/ui/config_menu.py +0 -0
  45. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/ui/logo.py +0 -0
  46. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/ui/menu.py +0 -0
  47. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/ui/model.py +0 -0
  48. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/ui/picker.py +0 -0
  49. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/ui/theme.py +0 -0
  50. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/undo.py +0 -0
  51. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/update_check.py +0 -0
  52. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle/update_prompt.py +0 -0
  53. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle.egg-info/SOURCES.txt +0 -0
  54. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle.egg-info/dependency_links.txt +0 -0
  55. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle.egg-info/entry_points.txt +0 -0
  56. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle.egg-info/requires.txt +0 -0
  57. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/agent_toggle.egg-info/top_level.txt +0 -0
  58. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/pyproject.toml +0 -0
  59. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/setup.cfg +0 -0
  60. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_alias.py +0 -0
  61. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_cli.py +0 -0
  62. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_cli_surface.py +0 -0
  63. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_color.py +0 -0
  64. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_companions.py +0 -0
  65. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_completions.py +0 -0
  66. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_config.py +0 -0
  67. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_config_menu.py +0 -0
  68. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_containment.py +0 -0
  69. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_cost.py +0 -0
  70. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_doctor.py +0 -0
  71. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_encoding.py +0 -0
  72. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_fs.py +0 -0
  73. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_harnesses.py +0 -0
  74. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_help.py +0 -0
  75. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_i18n.py +0 -0
  76. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_install.py +0 -0
  77. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_logo.py +0 -0
  78. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_mechanisms.py +0 -0
  79. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_menu.py +0 -0
  80. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_mod_version.py +0 -0
  81. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_mods.py +0 -0
  82. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_picker.py +0 -0
  83. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_platform.py +0 -0
  84. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_profiles.py +0 -0
  85. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_project.py +0 -0
  86. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_same_name.py +0 -0
  87. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_settings.py +0 -0
  88. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_store.py +0 -0
  89. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_theme.py +0 -0
  90. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_undo.py +0 -0
  91. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_update_check.py +0 -0
  92. {agent_toggle-0.18.0 → agent_toggle-0.19.0}/tests/test_update_prompt.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-toggle
3
- Version: 0.18.0
3
+ Version: 0.19.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
@@ -665,7 +665,9 @@ visible rather than silent.
665
665
 
666
666
  ## MCP backups are verbatim
667
667
 
668
- - **Claude**: the raw entry from `~/.claude.json`. Never `claude mcp get` —
668
+ - **Claude**: only servers an older agent-toggle removed have one; a disable now marks the
669
+ server off in place (see [Claude MCP scopes](#claude-mcp-scopes)). The backup is the raw
670
+ entry from `~/.claude.json`. Never `claude mcp get` —
669
671
  that prints a human summary and silently drops auth fields (`headers`,
670
672
  `headersHelper`), so a server restored from it fails with 401.
671
673
  - **Codex**: the exact `[mcp_servers.<name>]` text block including its
@@ -687,15 +689,20 @@ Both scopes stored in `~/.claude.json` are togglable, and the scope round-trips:
687
689
 
688
690
  | scope | lives in | toggle |
689
691
  |---|---|---|
690
- | user | top-level `mcpServers` | ✓ |
691
- | local | `projects/<dir>/mcpServers` | ✓ — project path saved with the backup |
692
+ | user | top-level `mcpServers` | ✓ — the entry stays; recorded in each *existing* project's `disabledMcpServers` |
693
+ | local | `projects/<dir>/mcpServers` | ✓ — as user scope |
692
694
  | project | the repo's own `.mcp.json` | ✓ with `--project <dir>` only, see [Project scope](#project-scope) |
693
695
  | claude.ai connector | your account | ✓ — recorded in each *existing* project's `disabledMcpServers`; a project first opened later needs the toggle re-run |
694
696
 
695
- `claude mcp remove -s local` only sees the project it runs in, so the project
696
- path is recorded at disable time and the restore runs back in that directory.
697
- One name that is local-scope in several projects is refused unless your cwd
698
- picks the winner — guessing would restore it into the wrong project.
697
+ Claude Code reads its MCP config once, at session start, and `/mcp enable` reaches only a server
698
+ it listed then. A removed server could not load until a new session, so a user- or local-scope
699
+ server is never removed: it stays in `~/.claude.json`, off in every project that existed at
700
+ disable time. A project first opened later loads it, and the mod turns it off at that session's
701
+ start (`/mcp disable`, which also marks the project). Without the mod, re-run the disable.
702
+
703
+ A server an older agent-toggle removed still comes back from its backup, with
704
+ `claude mcp add-json` run in the project recorded at disable time. A running session did not list
705
+ it, so it loads in a new session.
699
706
 
700
707
  ## Profiles
701
708
 
@@ -1064,6 +1071,12 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
1064
1071
  `/reload-skills` and `/reload-plugins` only re-read what is on disk. `/mcp disable` also
1065
1072
  leaves the per-project mark that `enable` clears (see
1066
1073
  [MCP backups are verbatim](#mcp-backups-are-verbatim)).
1074
+ `/mcp` only reaches a server the session listed at its start. The CLI keeps a disabled server
1075
+ listed, so `/mcp enable` loads it in any session, even one started while it was off. Only a
1076
+ server an older version removed and `enable` restores from its backup gets no command; its note
1077
+ says `example-mcp: loads in a new session`. At session start the mod also runs
1078
+ `/mcp disable <name>` for each server agent-toggle keeps off that the session's project has not
1079
+ marked (a project dir first opened after the disable).
1067
1080
  - `agent-toggle ui` in another terminal applies on Enter and writes the Claude rows it changed to
1068
1081
  `~/.agent-toggle/reload.json`. Every open session running the mod checks that file's mtime every
1069
1082
  5 s (one `stat`) and queues the same reloads, toasting `agent-toggle ui: /reload-skills`. A file
@@ -631,7 +631,9 @@ visible rather than silent.
631
631
 
632
632
  ## MCP backups are verbatim
633
633
 
634
- - **Claude**: the raw entry from `~/.claude.json`. Never `claude mcp get` —
634
+ - **Claude**: only servers an older agent-toggle removed have one; a disable now marks the
635
+ server off in place (see [Claude MCP scopes](#claude-mcp-scopes)). The backup is the raw
636
+ entry from `~/.claude.json`. Never `claude mcp get` —
635
637
  that prints a human summary and silently drops auth fields (`headers`,
636
638
  `headersHelper`), so a server restored from it fails with 401.
637
639
  - **Codex**: the exact `[mcp_servers.<name>]` text block including its
@@ -653,15 +655,20 @@ Both scopes stored in `~/.claude.json` are togglable, and the scope round-trips:
653
655
 
654
656
  | scope | lives in | toggle |
655
657
  |---|---|---|
656
- | user | top-level `mcpServers` | ✓ |
657
- | local | `projects/<dir>/mcpServers` | ✓ — project path saved with the backup |
658
+ | user | top-level `mcpServers` | ✓ — the entry stays; recorded in each *existing* project's `disabledMcpServers` |
659
+ | local | `projects/<dir>/mcpServers` | ✓ — as user scope |
658
660
  | project | the repo's own `.mcp.json` | ✓ with `--project <dir>` only, see [Project scope](#project-scope) |
659
661
  | claude.ai connector | your account | ✓ — recorded in each *existing* project's `disabledMcpServers`; a project first opened later needs the toggle re-run |
660
662
 
661
- `claude mcp remove -s local` only sees the project it runs in, so the project
662
- path is recorded at disable time and the restore runs back in that directory.
663
- One name that is local-scope in several projects is refused unless your cwd
664
- picks the winner — guessing would restore it into the wrong project.
663
+ Claude Code reads its MCP config once, at session start, and `/mcp enable` reaches only a server
664
+ it listed then. A removed server could not load until a new session, so a user- or local-scope
665
+ server is never removed: it stays in `~/.claude.json`, off in every project that existed at
666
+ disable time. A project first opened later loads it, and the mod turns it off at that session's
667
+ start (`/mcp disable`, which also marks the project). Without the mod, re-run the disable.
668
+
669
+ A server an older agent-toggle removed still comes back from its backup, with
670
+ `claude mcp add-json` run in the project recorded at disable time. A running session did not list
671
+ it, so it loads in a new session.
665
672
 
666
673
  ## Profiles
667
674
 
@@ -1030,6 +1037,12 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
1030
1037
  `/reload-skills` and `/reload-plugins` only re-read what is on disk. `/mcp disable` also
1031
1038
  leaves the per-project mark that `enable` clears (see
1032
1039
  [MCP backups are verbatim](#mcp-backups-are-verbatim)).
1040
+ `/mcp` only reaches a server the session listed at its start. The CLI keeps a disabled server
1041
+ listed, so `/mcp enable` loads it in any session, even one started while it was off. Only a
1042
+ server an older version removed and `enable` restores from its backup gets no command; its note
1043
+ says `example-mcp: loads in a new session`. At session start the mod also runs
1044
+ `/mcp disable <name>` for each server agent-toggle keeps off that the session's project has not
1045
+ marked (a project dir first opened after the disable).
1033
1046
  - `agent-toggle ui` in another terminal applies on Enter and writes the Claude rows it changed to
1034
1047
  `~/.agent-toggle/reload.json`. Every open session running the mod checks that file's mtime every
1035
1048
  5 s (one `stat`) and queues the same reloads, toasting `agent-toggle ui: /reload-skills`. A file
@@ -1,3 +1,3 @@
1
1
  """agent-toggle: temporarily disable / re-enable AI-agent resources."""
2
2
 
3
- __version__ = "0.18.0"
3
+ __version__ = "0.19.0"
@@ -18,7 +18,6 @@ from .backends.mcp_json import (
18
18
  _finite,
19
19
  _no_dup,
20
20
  _not_json,
21
- claude_mcp_config,
22
21
  claudeai_connector_names,
23
22
  project_mcp_backup,
24
23
  project_mcp_remove,
@@ -1042,7 +1041,13 @@ def toggle_mcp(action: str, names: list[str], state: dict,
1042
1041
  fails += fail(name, refused)
1043
1042
  continue
1044
1043
 
1045
- if backend == "claude-json" and name in claudeai_connector_names():
1044
+ connector = backend == "claude-json" and name in claudeai_connector_names()
1045
+ # A configured server is marked off per project like a connector, never removed:
1046
+ # Claude Code builds its MCP list once per session and `/mcp enable` reaches only a
1047
+ # listed server, so a removed one could not load until a new session.
1048
+ if connector or backend == "claude-json" and (
1049
+ (entry or {}).get("connector")
1050
+ or action == "disable" and name in live_mcp(home, backend)):
1046
1051
  ok, detail = toggle_claudeai_connector(action, name, dry_run)
1047
1052
  if not ok:
1048
1053
  fails += fail(name, str(detail))
@@ -1057,15 +1062,17 @@ def toggle_mcp(action: str, names: list[str], state: dict,
1057
1062
  }
1058
1063
  _ok(out, dry_run, harness, "mcp", action, name,
1059
1064
  f"disabled across {n} project(s) "
1060
- f"(claude.ai connector -- account stays connected, "
1061
- f"just parked per-project; new project dirs need a re-run)",
1065
+ + ("(claude.ai connector -- account stays connected, "
1066
+ "just parked per-project; new project dirs need a re-run)" if connector
1067
+ else "(config kept in ~/.claude.json; the mod marks new project dirs)"),
1062
1068
  connector=True, projects=detail)
1063
1069
  else:
1064
1070
  if not dry_run:
1065
1071
  state["disabled"].pop(key, None)
1066
1072
  _ok(out, dry_run, harness, "mcp", action, name,
1067
1073
  f"restored across {n} project(s) "
1068
- f"(open a NEW session for it to load)",
1074
+ + ("(open a NEW session for it to load)" if connector
1075
+ else f"(`/mcp enable {name}` loads it in a running session)"),
1069
1076
  connector=True, projects=detail)
1070
1077
  if not dry_run:
1071
1078
  log(action, "mcp", name, "ok", json.dumps(detail), harness=harness,
@@ -1076,35 +1083,23 @@ def toggle_mcp(action: str, names: list[str], state: dict,
1076
1083
  scope, project = "user", None
1077
1084
 
1078
1085
  if action == "disable":
1079
- if backend == "claude-json":
1080
- try:
1081
- found = claude_mcp_config(name)
1082
- except LookupError as e:
1083
- fails += fail(name, str(e))
1084
- continue
1085
- if found is None:
1086
- hint = ("not in ~/.claude.json (user or local scope) -- "
1087
- "a repo's own .mcp.json needs --project <dir>")
1088
- if not dry_run: # a dry run never shells out to claude
1089
- ok, cfg = run_cli(claude_bin(), ["mcp", "get", name])
1090
- if ok and "claude.ai config" in cfg:
1091
- hint = ("account-level claude.ai connector -- disable it "
1092
- "at claude.ai (Settings -> Connectors)")
1093
- fails += fail(name, hint)
1094
- continue
1095
- raw, scope, project = found
1096
- if dry_run and not claude_bin():
1097
- fails += fail(name, "claude CLI not found")
1098
- continue
1099
- saved = json.dumps(raw, indent=2, ensure_ascii=False)
1100
- else: # toml
1101
- config = home / "config.toml"
1102
- # plan first, back up, THEN edit: the block is never only in memory
1103
- block = codex_mcp_remove(config, name, True) if config.exists() else None
1104
- if block is None:
1105
- fails += fail(name, f"no [mcp_servers.{name}] in {config}")
1106
- continue
1107
- saved = json.dumps({"toml": block}, ensure_ascii=False, indent=2)
1086
+ if backend == "claude-json": # a configured server was marked off above
1087
+ hint = ("not in ~/.claude.json (user or local scope) -- "
1088
+ "a repo's own .mcp.json needs --project <dir>")
1089
+ if not dry_run: # a dry run never shells out to claude
1090
+ ok, cfg = run_cli(claude_bin(), ["mcp", "get", name])
1091
+ if ok and "claude.ai config" in cfg:
1092
+ hint = ("account-level claude.ai connector -- disable it "
1093
+ "at claude.ai (Settings -> Connectors)")
1094
+ fails += fail(name, hint)
1095
+ continue
1096
+ config = home / "config.toml"
1097
+ # plan first, back up, THEN edit: the block is never only in memory
1098
+ block = codex_mcp_remove(config, name, True) if config.exists() else None
1099
+ if block is None:
1100
+ fails += fail(name, f"no [mcp_servers.{name}] in {config}")
1101
+ continue
1102
+ saved = json.dumps({"toml": block}, ensure_ascii=False, indent=2)
1108
1103
 
1109
1104
  where = f" [{scope}{f': {project}' if project else ''}]"
1110
1105
  if not dry_run:
@@ -1123,25 +1118,14 @@ def toggle_mcp(action: str, names: list[str], state: dict,
1123
1118
  bp.unlink(missing_ok=True)
1124
1119
  fails += fail(name, f"state not saved, nothing changed: {e}")
1125
1120
  continue
1126
- if backend == "claude-json":
1127
- ok, res = run_cli(claude_bin(), ["mcp", "remove", name, "-s", scope],
1128
- cwd=project)
1129
- err = None if ok else redact(res, raw)[:100]
1130
- else:
1131
- try:
1132
- codex_mcp_remove(config, name)
1133
- err = None
1134
- except fs.WriteError as e: # config rolled back
1135
- err = str(e)
1136
- if err is not None: # settled from the file: untouched -> backup dropped
1137
- settle(state, key, out, inline=True)
1138
- fails += fail(name, err)
1121
+ try:
1122
+ codex_mcp_remove(config, name)
1123
+ except fs.WriteError as e: # config rolled back; settled from the file:
1124
+ settle(state, key, out, inline=True) # untouched -> backup dropped
1125
+ fails += fail(name, str(e))
1139
1126
  continue
1140
1127
  state["disabled"][key] = new
1141
1128
  end(state, key)
1142
- # claude local scope logs its real scope + cwd; undo treats every
1143
- # scope but `project` (a --project change) as user scope, which is right
1144
- # for local: the replay finds the user-keyed entry and its own cwd.
1145
1129
  log(action, "mcp", name, "ok", str(bp), harness=harness, batch=batch,
1146
1130
  project=project, scope=scope)
1147
1131
  _ok(out, dry_run, harness, "mcp", action, name,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-toggle
3
- Version: 0.18.0
3
+ Version: 0.19.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
@@ -665,7 +665,9 @@ visible rather than silent.
665
665
 
666
666
  ## MCP backups are verbatim
667
667
 
668
- - **Claude**: the raw entry from `~/.claude.json`. Never `claude mcp get` —
668
+ - **Claude**: only servers an older agent-toggle removed have one; a disable now marks the
669
+ server off in place (see [Claude MCP scopes](#claude-mcp-scopes)). The backup is the raw
670
+ entry from `~/.claude.json`. Never `claude mcp get` —
669
671
  that prints a human summary and silently drops auth fields (`headers`,
670
672
  `headersHelper`), so a server restored from it fails with 401.
671
673
  - **Codex**: the exact `[mcp_servers.<name>]` text block including its
@@ -687,15 +689,20 @@ Both scopes stored in `~/.claude.json` are togglable, and the scope round-trips:
687
689
 
688
690
  | scope | lives in | toggle |
689
691
  |---|---|---|
690
- | user | top-level `mcpServers` | ✓ |
691
- | local | `projects/<dir>/mcpServers` | ✓ — project path saved with the backup |
692
+ | user | top-level `mcpServers` | ✓ — the entry stays; recorded in each *existing* project's `disabledMcpServers` |
693
+ | local | `projects/<dir>/mcpServers` | ✓ — as user scope |
692
694
  | project | the repo's own `.mcp.json` | ✓ with `--project <dir>` only, see [Project scope](#project-scope) |
693
695
  | claude.ai connector | your account | ✓ — recorded in each *existing* project's `disabledMcpServers`; a project first opened later needs the toggle re-run |
694
696
 
695
- `claude mcp remove -s local` only sees the project it runs in, so the project
696
- path is recorded at disable time and the restore runs back in that directory.
697
- One name that is local-scope in several projects is refused unless your cwd
698
- picks the winner — guessing would restore it into the wrong project.
697
+ Claude Code reads its MCP config once, at session start, and `/mcp enable` reaches only a server
698
+ it listed then. A removed server could not load until a new session, so a user- or local-scope
699
+ server is never removed: it stays in `~/.claude.json`, off in every project that existed at
700
+ disable time. A project first opened later loads it, and the mod turns it off at that session's
701
+ start (`/mcp disable`, which also marks the project). Without the mod, re-run the disable.
702
+
703
+ A server an older agent-toggle removed still comes back from its backup, with
704
+ `claude mcp add-json` run in the project recorded at disable time. A running session did not list
705
+ it, so it loads in a new session.
699
706
 
700
707
  ## Profiles
701
708
 
@@ -1064,6 +1071,12 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
1064
1071
  `/reload-skills` and `/reload-plugins` only re-read what is on disk. `/mcp disable` also
1065
1072
  leaves the per-project mark that `enable` clears (see
1066
1073
  [MCP backups are verbatim](#mcp-backups-are-verbatim)).
1074
+ `/mcp` only reaches a server the session listed at its start. The CLI keeps a disabled server
1075
+ listed, so `/mcp enable` loads it in any session, even one started while it was off. Only a
1076
+ server an older version removed and `enable` restores from its backup gets no command; its note
1077
+ says `example-mcp: loads in a new session`. At session start the mod also runs
1078
+ `/mcp disable <name>` for each server agent-toggle keeps off that the session's project has not
1079
+ marked (a project dir first opened after the disable).
1067
1080
  - `agent-toggle ui` in another terminal applies on Enter and writes the Claude rows it changed to
1068
1081
  `~/.agent-toggle/reload.json`. Every open session running the mod checks that file's mtime every
1069
1082
  5 s (one `stat`) and queues the same reloads, toasting `agent-toggle ui: /reload-skills`. A file
@@ -5,7 +5,7 @@ import json
5
5
 
6
6
  from test_cli_surface import CliCase
7
7
 
8
- from agent_toggle import fs, mechanisms
8
+ from agent_toggle import fs
9
9
 
10
10
 
11
11
  class AllTypeTest(CliCase):
@@ -42,9 +42,6 @@ class AllTypeTest(CliCase):
42
42
 
43
43
  def test_mcp_with_the_same_name_is_included(self) -> None:
44
44
  self.claude_json({"mcpServers": {"dup": {"command": "dup-server"}}})
45
- saved = mechanisms.claude_mcp_config
46
- self.addCleanup(setattr, mechanisms, "claude_mcp_config", saved)
47
- mechanisms.claude_mcp_config = lambda name: ({"command": "dup-server"}, "user", None)
48
45
  rc, env = self.run_json("disable", "all", "dup")
49
46
  self.assertEqual(rc, 0)
50
47
  self.assertIn(("mcp", "ok"), [(r["type"], r["status"]) for r in env["results"]])
@@ -103,13 +103,15 @@ class ClaudeJsonScopeTest(SandboxCase):
103
103
  # ambiguous local scope is refused
104
104
  with self.assertRaisesRegex(LookupError, "2 projects"):
105
105
  mcp_json.claude_mcp_config("dup")
106
+ # a disable marks it off in every project and never removes either copy, so
107
+ # nothing has to be guessed
106
108
  state = {"version": 2, "disabled": {}}
107
109
  fails = mechanisms.toggle_mcp("disable", ["dup"], state, "claude", self.home,
108
110
  "claude-json")
109
- # ambiguity counts as a failure, not a crash
110
- self.assertEqual(fails, 1)
111
- # nothing recorded for the ambiguous server
112
- self.assertFalse(state["disabled"])
111
+ self.assertEqual(fails, 0)
112
+ projects = json.loads(fs.claude_json().read_text(encoding="utf-8"))["projects"]
113
+ self.assertEqual([(d["mcpServers"]["dup"]["command"], d["disabledMcpServers"])
114
+ for d in projects.values()], [("a", ["dup"]), ("b", ["dup"])])
113
115
 
114
116
  def test_mcp_enable_defaults_to_user_scope_for_old_entries(self) -> None:
115
117
  """Entries parked before scope tracking have no scope field."""
@@ -151,6 +153,39 @@ class ClaudeJsonScopeTest(SandboxCase):
151
153
  self.assertEqual(projects["/work/a"]["disabledMcpServers"], ["other"])
152
154
  self.assertEqual(projects["/work/b"]["disabledMcpServers"], ["other"])
153
155
 
156
+ def test_configured_server_is_marked_off_in_place(self) -> None:
157
+ """Claude Code builds its MCP list once per session and `/mcp enable` reaches only a
158
+ listed server: disable keeps the config entry and marks it off per project, so a
159
+ session started while it is off still lists it and `/mcp enable` loads it there."""
160
+ entry = {"type": "http", "url": "https://example.invalid/mcp"}
161
+ self.claude_json({"mcpServers": {"example-mcp": entry}, "projects": {
162
+ "/work/a": {"disabledMcpServers": ["other"]}, "/work/b": {}}})
163
+ state = {"version": 3, "disabled": {}}
164
+ out = Result()
165
+ fails = mechanisms.toggle_mcp("disable", ["example-mcp"], state, "claude",
166
+ self.home, "claude-json", out)
167
+ self.assertEqual(fails, 0)
168
+ cfg = json.loads(fs.claude_json().read_text(encoding="utf-8"))
169
+ # the entry stays where it was; no `claude mcp remove`, no backup file
170
+ self.assertEqual(cfg["mcpServers"]["example-mcp"], entry)
171
+ self.assertEqual(self.cli_calls, [])
172
+ self.assertFalse(mechanisms._json_backup_path("claude", "example-mcp").exists())
173
+ self.assertEqual({p: d.get("disabledMcpServers") for p, d in cfg["projects"].items()},
174
+ {"/work/a": ["other", "example-mcp"], "/work/b": ["example-mcp"]})
175
+ row = out.rows[-1]
176
+ self.assertEqual((row["status"], row.get("connector"), row.get("backup")), ("ok", True, None))
177
+ self.assertTrue(state["disabled"]["claude:mcp:example-mcp"]["connector"])
178
+
179
+ fails = mechanisms.toggle_mcp("enable", ["example-mcp"], state, "claude",
180
+ self.home, "claude-json", Result())
181
+ self.assertEqual(fails, 0)
182
+ cfg = json.loads(fs.claude_json().read_text(encoding="utf-8"))
183
+ self.assertEqual(cfg["mcpServers"]["example-mcp"], entry)
184
+ self.assertEqual({p: d.get("disabledMcpServers") for p, d in cfg["projects"].items()},
185
+ {"/work/a": ["other"], "/work/b": []})
186
+ self.assertEqual(state["disabled"], {})
187
+ self.assertEqual(self.cli_calls, [])
188
+
154
189
 
155
190
  FIXTURES = Path(__file__).parent / "fixtures"
156
191
 
@@ -98,11 +98,11 @@ class ConformanceTest(CliCase):
98
98
  self.assertEqual(self.run_cli("disable", "mcp", "example-mcp")[0], 0)
99
99
  self.assertEqual(self.run_cli("enable", "mcp", "example-mcp")[0], 0)
100
100
  calls = [c for c, _ in self.cli_calls]
101
- self.assertEqual(calls[:3], [["plugin", "disable", "demo-plugin"],
102
- ["plugin", "enable", "demo-plugin"],
103
- ["mcp", "remove", "example-mcp", "-s", "user"]])
104
- self.assertEqual(calls[3][:4], ["mcp", "add-json", "--scope", "user"])
105
- self.assertEqual(json.loads(calls[3][5]), orig) # restored payload is the original
101
+ # an MCP server is marked off in place: no `claude mcp remove|add-json`
102
+ self.assertEqual(calls, [["plugin", "disable", "demo-plugin"],
103
+ ["plugin", "enable", "demo-plugin"]])
104
+ cfg = json.loads((self.tmp / ".claude.json").read_text(encoding="utf-8"))
105
+ self.assertEqual(cfg["mcpServers"]["example-mcp"], orig)
106
106
 
107
107
  def test_dry_run_writes_nothing(self) -> None:
108
108
  for harness, type_ in PAIRS:
@@ -850,8 +850,10 @@ class PluginCrashTest(CliCrashCase):
850
850
 
851
851
 
852
852
  class ClaudeMcpCrashTest(CliCrashCase):
853
- """claude MCP servers: `claude mcp remove|add-json` write ~/.claude.json, so settle
854
- reads that file strictly (no CLI call); unreadable -> stuck."""
853
+ """claude MCP servers an older version removed (`claude mcp remove`, config in a
854
+ backup): enable's `claude mcp add-json` writes ~/.claude.json, so settle reads that
855
+ file strictly (no CLI call); unreadable -> stuck. A disable now only marks a server
856
+ off (no write-ahead), so a pending disable is one an older version left."""
855
857
 
856
858
  KEY = "claude:mcp:demo-mcp"
857
859
  ARGS = ("mcp", "demo-mcp", "--harness", "claude")
@@ -873,8 +875,35 @@ class ClaudeMcpCrashTest(CliCrashCase):
873
875
  "name": "demo-mcp", "backend": "claude-json", "backup": str(self.bp),
874
876
  "scope": "user", "project": None, "at": "2026-01-01T00:00:00+0000", **fields}
875
877
 
878
+ def old_disable(self, landed: bool, name: str = "demo-mcp", project: str | None = None) -> None:
879
+ """A disable by an older version, killed around its `claude mcp remove`: the
880
+ backup written, the op pending, the server gone from ~/.claude.json if it landed."""
881
+ cfg = json.loads(self.cj.read_text(encoding="utf-8"))
882
+ box = cfg if project is None else cfg["projects"][project]
883
+ bp = fs.backup_dir() / f"claude__{name}.json"
884
+ fs.private_dir(fs.backup_dir())
885
+ bp.write_text(json.dumps(box["mcpServers"][name]), encoding="utf-8")
886
+ if landed:
887
+ del box["mcpServers"][name]
888
+ self.cj.write_text(json.dumps(cfg), encoding="utf-8")
889
+ self.forge(f"claude:mcp:{name}", "disable", self.entry(
890
+ name=name, backup=str(bp), scope="local" if project else "user", project=project))
891
+
892
+ def old_disabled(self) -> None:
893
+ """A server an older version disabled: removed, its config in the backup."""
894
+ self.old_disable(landed=True)
895
+ store.save_state({"version": 3, "disabled": {self.KEY: self.entry()}})
896
+
897
+ def test_a_disable_marks_it_off_and_never_calls_the_cli(self) -> None:
898
+ self.assertEqual(self.run_json("disable", *self.ARGS)[0], 0)
899
+ self.assertNotIn("pending", self.raw_state())
900
+ self.assertIn(self.KEY, self.raw_state()["disabled"])
901
+ self.assertIn("demo-mcp", self.servers())
902
+ self.assertFalse(self.bp.exists())
903
+ self.assertEqual(self.cli_calls, [])
904
+
876
905
  def test_kill_after_the_cli_remove_is_recorded_and_restorable(self) -> None:
877
- self.kill_in_cli("disable", *self.ARGS, landed=True)
906
+ self.old_disable(landed=True)
878
907
  self.assertEqual(self.pending()[self.KEY]["entry"]["backend"], "claude-json")
879
908
  self.assertNotIn("demo-mcp", self.servers())
880
909
  self.assertEqual(self.raw_state()["disabled"], {})
@@ -890,7 +919,7 @@ class ClaudeMcpCrashTest(CliCrashCase):
890
919
  self.assertEqual(self.servers()["demo-mcp"], {"command": "demo"})
891
920
 
892
921
  def test_kill_before_the_cli_remove_rolls_back_and_drops_the_backup(self) -> None:
893
- self.kill_in_cli("disable", *self.ARGS, landed=False)
922
+ self.old_disable(landed=False)
894
923
  self.assertTrue(self.bp.exists())
895
924
  self.assertTrue(any("never reached disk" in w for w in self.warnings("status")))
896
925
  self.run_json(*NEXT)
@@ -898,15 +927,14 @@ class ClaudeMcpCrashTest(CliCrashCase):
898
927
  self.assertFalse(self.bp.exists())
899
928
 
900
929
  def test_kill_after_the_cli_add_drops_the_entry(self) -> None:
901
- self.assertEqual(self.run_json("disable", *self.ARGS)[0], 0)
902
- self.assertNotIn("pending", self.raw_state())
930
+ self.old_disabled()
903
931
  self.kill_in_cli("enable", *self.ARGS, landed=True)
904
932
  self.assertEqual(len(self.doctor_rows("done")), 1)
905
933
  self.run_json(*NEXT)
906
934
  self.assert_settled(False)
907
935
 
908
936
  def test_kill_before_the_cli_add_keeps_it_disabled(self) -> None:
909
- self.assertEqual(self.run_json("disable", *self.ARGS)[0], 0)
937
+ self.old_disabled()
910
938
  self.kill_in_cli("enable", *self.ARGS, landed=False)
911
939
  self.assertEqual(len(self.doctor_rows("undone")), 1)
912
940
  self.run_json(*NEXT)
@@ -915,20 +943,21 @@ class ClaudeMcpCrashTest(CliCrashCase):
915
943
 
916
944
  def test_settle_needs_no_cli(self) -> None:
917
945
  from agent_toggle.backends import plugin_cli
918
- self.kill_in_cli("disable", *self.ARGS, landed=True)
946
+ self.old_disable(landed=True)
919
947
  plugin_cli.which = lambda name: None
920
948
  self.run_json(*NEXT)
921
949
  self.assert_settled(True)
922
950
 
923
- def test_a_failed_cli_remove_settles_inline(self) -> None:
951
+ def test_a_failed_cli_add_settles_inline(self) -> None:
952
+ self.old_disabled()
924
953
  self.broken = True
925
- rc, env = self.run_json("disable", *self.ARGS)
954
+ rc, env = self.run_json("enable", *self.ARGS)
926
955
  self.assertEqual(rc, 1)
927
- self.assert_settled(False)
928
- self.assertFalse(self.bp.exists())
956
+ self.assert_settled(True)
957
+ self.assertTrue(self.bp.exists())
929
958
 
930
959
  def test_an_unreadable_claude_json_is_stuck_never_guessed(self) -> None:
931
- self.kill_in_cli("disable", *self.ARGS, landed=True)
960
+ self.old_disable(landed=True)
932
961
  for body in ("{broken", "[]", '{"mcpServers": []}'):
933
962
  with self.subTest(body):
934
963
  self.cj.write_text(body, encoding="utf-8")
@@ -941,7 +970,7 @@ class ClaudeMcpCrashTest(CliCrashCase):
941
970
  self.assertTrue(self.bp.exists())
942
971
 
943
972
  def test_a_gone_server_without_its_backup_is_stuck(self) -> None:
944
- self.kill_in_cli("disable", *self.ARGS, landed=True)
973
+ self.old_disable(landed=True)
945
974
  self.bp.unlink()
946
975
  stuck = self.doctor_rows("stuck")
947
976
  self.assertEqual(len(stuck), 1, stuck)
@@ -955,7 +984,7 @@ class ClaudeMcpCrashTest(CliCrashCase):
955
984
  proj.mkdir()
956
985
  self.cj.write_text(json.dumps({"mcpServers": {}, "projects": {
957
986
  str(proj): {"mcpServers": {"loc-mcp": {"command": "loc"}}}}}), encoding="utf-8")
958
- self.kill_in_cli("disable", "mcp", "loc-mcp", "--harness", "claude", landed=True)
987
+ self.old_disable(landed=True, name="loc-mcp", project=str(proj))
959
988
  entry = self.pending()["claude:mcp:loc-mcp"]["entry"]
960
989
  self.assertEqual((entry["scope"], entry["project"]), ("local", str(proj)))
961
990
  self.run_json(*NEXT)
@@ -23,6 +23,14 @@ def mode(p: Path) -> int:
23
23
  return p.stat().st_mode & 0o777
24
24
 
25
25
 
26
+ def codex_toml(home: Path) -> None:
27
+ """A codex config whose MCP server carries a token: its disable writes a backup."""
28
+ (home / ".codex").mkdir()
29
+ (home / ".codex" / "config.toml").write_text(
30
+ '[mcp_servers.example-mcp]\ncommand = "example"\n\n'
31
+ '[mcp_servers.example-mcp.env]\nTOKEN = "Bearer test-token-000"\n', encoding="utf-8")
32
+
33
+
26
34
  class LockTest(SandboxCase):
27
35
  def setUp(self) -> None:
28
36
  super().setUp()
@@ -121,16 +129,12 @@ class PrivateModeTest(SandboxCase):
121
129
  self.assertEqual([x.name for x in self.tmp.glob(".out.json*")], [])
122
130
 
123
131
  def test_mcp_disable_writes_0600_backup_and_state(self) -> None:
124
- raw = {"type": "http", "headers": {"Authorization": "Bearer test-token-000"}}
125
- saved = mechanisms.claude_mcp_config
126
- self.addCleanup(setattr, mechanisms, "claude_mcp_config", saved)
127
- mechanisms.claude_mcp_config = lambda name: (raw, "user", None)
128
- self.cli_rc = 0 # stubbed `claude mcp remove` succeeds
132
+ codex_toml(self.tmp)
129
133
  state = {"version": 3, "disabled": {}}
130
- fails = mechanisms.toggle_mcp("disable", ["example-mcp"], state, "claude",
131
- self.home, "claude-json")
134
+ fails = mechanisms.toggle_mcp("disable", ["example-mcp"], state, "codex",
135
+ self.tmp / ".codex", "toml")
132
136
  self.assertEqual(fails, 0)
133
- entry = state["disabled"]["claude:mcp:example-mcp"]
137
+ entry = state["disabled"]["codex:mcp:example-mcp"]
134
138
  self.assertEqual(entry["mechanism"], "remove_backup")
135
139
  self.assertEqual(mode(Path(entry["backup"])), 0o600)
136
140
  store.save_state(state)
@@ -159,12 +163,8 @@ class PrivateModeTest(SandboxCase):
159
163
  class PrivateDirTest(SandboxCase):
160
164
  def test_state_backup_dirs_and_log_are_private_under_open_umask(self) -> None:
161
165
  self.addCleanup(os.umask, os.umask(0))
162
- raw = {"type": "http", "headers": {"Authorization": "Bearer test-token-000"}}
163
- saved = mechanisms.claude_mcp_config
164
- self.addCleanup(setattr, mechanisms, "claude_mcp_config", saved)
165
- mechanisms.claude_mcp_config = lambda name: (raw, "user", None)
166
- self.cli_rc = 0
167
- self.assertEqual(cli.main(["disable", "mcp", "example-mcp"]), 0)
166
+ codex_toml(self.tmp)
167
+ self.assertEqual(cli.main(["disable", "mcp", "example-mcp", "--harness", "codex"]), 0)
168
168
  for d in (fs.state_dir(), fs.backup_dir()):
169
169
  self.assertEqual(mode(d), 0o700, d)
170
170
  self.assertEqual(mode(fs.log_file()), 0o600)
@@ -114,6 +114,23 @@ class SecretAuditTest(CliCase):
114
114
  self.assertEqual(stat.S_IMODE(b.stat().st_mode), 0o600, b)
115
115
  self.assertEqual(stat.S_IMODE(fs.backup_dir().stat().st_mode) & 0o077, 0)
116
116
 
117
+ def park_claude_legacy(self) -> None:
118
+ """The claude servers as an older version disabled them: removed from ~/.claude.json,
119
+ their config in a backup, so enable runs `claude mcp add-json` with it."""
120
+ cj = json.loads(fs.claude_json().read_text(encoding="utf-8"))
121
+ state = store.load_state()
122
+ fs.private_dir(fs.backup_dir())
123
+ for name, box, proj in (("secret-mcp", cj, None),
124
+ ("local-mcp", cj["projects"][str(self.proj)], str(self.proj))):
125
+ bp = fs.backup_dir() / f"claude__{name}.json"
126
+ fs.atomic_write(bp, json.dumps(box["mcpServers"].pop(name), indent=2))
127
+ state["disabled"][f"claude:mcp:{name}"] = {
128
+ "mechanism": "remove_backup", "harness": "claude", "type": "mcp", "name": name,
129
+ "backend": "claude-json", "backup": str(bp), "scope": "local" if proj else "user",
130
+ "project": proj, "at": "2026-01-01T00:00:00+0000"}
131
+ fs.claude_json().write_text(json.dumps(cj), encoding="utf-8")
132
+ store.save_state(state)
133
+
117
134
  def batch(self, name: str):
118
135
  return mock.patch.object(store, "BATCH", name)
119
136
 
@@ -189,6 +206,7 @@ class SecretAuditTest(CliCase):
189
206
 
190
207
  def test_cli_error_echoing_the_config_is_redacted(self) -> None:
191
208
  self.each("disable")
209
+ self.park_claude_legacy()
192
210
  self.cli_rc = 1 # add-json fails and echoes its config
193
211
  for extra in VARIANTS:
194
212
  for t in self.TARGETS[:2]:
@@ -205,6 +223,7 @@ class SecretAuditTest(CliCase):
205
223
 
206
224
  def test_unexpected_exception_traceback_shows_no_value(self) -> None:
207
225
  self.each("disable")
226
+ self.park_claude_legacy()
208
227
 
209
228
  def boom(cmd, **kw):
210
229
  raise RuntimeError("cli crashed")
File without changes
File without changes