agent-toggle 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,580 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-toggle
3
+ Version: 0.1.0
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
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/weskao/agent-toggle
7
+ Project-URL: Repository, https://github.com/weskao/agent-toggle
8
+ Project-URL: Issues, https://github.com/weskao/agent-toggle/issues
9
+ Project-URL: Changelog, https://github.com/weskao/agent-toggle/blob/main/CHANGELOG.md
10
+ Keywords: ai-agents,claude-code,codex,skills,mcp,cli,toggle
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development
23
+ Classifier: Topic :: Utilities
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Provides-Extra: windows
28
+ Requires-Dist: windows-curses; sys_platform == "win32" and extra == "windows"
29
+ Dynamic: license-file
30
+
31
+ # agent-toggle
32
+
33
+ Temporarily disable and restore AI-agent resources — skills, agents, commands,
34
+ rules, plugins, MCP servers — across Claude Code, Codex, Grok CLI, OpenCode,
35
+ OpenClaw, Copilot, Vibe, Devin and Antigravity — and show what each one costs at
36
+ session start.
37
+
38
+ **Nothing is ever deleted.** Everything is parked and recorded, and `enable`
39
+ puts it back where it came from.
40
+
41
+ ## Why
42
+
43
+ | target | native mechanism | the gap |
44
+ |---|---|---|
45
+ | plugin | `claude plugin disable/enable` | fine — this tool just batches it |
46
+ | skill | none | moving files by hand hits the rename trap below |
47
+ | agent | none | same |
48
+ | command | none | same, plus nested `group/name.md` paths get flattened |
49
+ | MCP | `remove` only | the config is gone unless you saved it first |
50
+
51
+ ## Install
52
+
53
+ Latest release (on PyPI from the first tagged release):
54
+
55
+ ```sh
56
+ uv tool install agent-toggle
57
+ agent-toggle --version
58
+ ```
59
+
60
+ Pre-release versions from this repository:
61
+
62
+ ```sh
63
+ uv tool install git+<repo-url>
64
+ agent-toggle --version
65
+ ```
66
+
67
+ Alternatively, use `pipx`:
68
+
69
+ ```sh
70
+ pipx install agent-toggle # from the first tagged release; or: pipx install git+<repo-url>
71
+ ```
72
+
73
+ From a checkout, either install it editable or run it in place:
74
+
75
+ ```sh
76
+ git clone <repo-url> agent-toggle
77
+ cd agent-toggle
78
+ pip install -e . # puts the `agent-toggle` console script on PATH
79
+ python3 agent_toggle.py status # no install needed; same CLI
80
+ ```
81
+
82
+ Python 3.10+, no runtime dependencies. Then write the skill shim:
83
+
84
+ ```sh
85
+ agent-toggle install-shims # installed, or: ./install.sh (a thin wrapper)
86
+ python3 agent_toggle.py install-shims # from a checkout, no install needed
87
+ ```
88
+
89
+ `install-shims` writes a thin skill shim (`skills/agent-toggle/SKILL.md`) into
90
+ every installed harness that supports skills, so `/agent-toggle` works from any
91
+ of them, and adds the park dirs (`skills-disabled/` etc.) to a `.gitignore`
92
+ already present in that harness home. It is idempotent; `--dry-run` shows the
93
+ plan. The tool itself stays in one place.
94
+
95
+ ## Usage
96
+
97
+ ```sh
98
+ agent-toggle <command> [args] # or: python3 agent_toggle.py <command> [args]
99
+ ```
100
+
101
+ | command | what it does |
102
+ |---|---|
103
+ | `ui` | interactive picker — cost column, sort, filters; `--dry-run` shows the plan for what you stage and changes nothing |
104
+ | `status` | health check: harnesses found, types each supports, parked counts, gitignore, untracked parked items, stale live twins, shared dirs |
105
+ | `list [type]` | what is currently disabled (`--project <dir>` filters to one project) |
106
+ | `cost [--type T]` | estimated startup tokens per item, biggest first (read-only; `--harness H` filters) |
107
+ | `install-shims` | write the skill shim into every installed harness (`--dry-run` shows the plan) |
108
+ | `disable <type> <name>...` | park one or more items (`--dry-run` shows the plan; `--project <dir>` for a repo's own `.claude/` and `.mcp.json`) |
109
+ | `enable <type> <name>...` | put them back (`--dry-run` shows the plan; `--project <dir>` likewise) |
110
+ | `enable --all` | put back **every** disabled item (`--harness H` narrows it, `--project <dir>` takes only that project's) |
111
+ | `undo` | reverse the last logged batch (`--dry-run` shows the plan) |
112
+ | `profile save\|apply\|diff\|list` | named sets of live items; see [Profiles](#profiles) |
113
+ | `doctor` | read-only check of each harness layout and of `state.json` against disk; exit `1` only on an `error` row |
114
+ | `migrate` | import an older `~/.claude-toggle/` state |
115
+
116
+ `<type>` = `skill` / `agent` / `command` / `rule` / `plugin` / `mcp`.
117
+ `rule` is claude-only (`~/.claude/rules/*.md`, parked in `rules-disabled/`).
118
+
119
+ Flags accepted by every command, before or after the subcommand:
120
+
121
+ - `--harness claude|codex|grok|opencode|openclaw|copilot|vibe|devin|agy` picks the
122
+ target (default `claude`; on `list` / `status` / `cost` it filters when given).
123
+ - `--json` prints exactly one JSON document and nothing else on stdout:
124
+ `{"ok", "command", "results": [{harness, type, name, action, status, detail, ...}],
125
+ "warnings", "needs_new_session"}`. Errors -- including unexpected ones, as
126
+ `{ExceptionType}: {message}` -- always emit it, with `"ok": false`.
127
+ Exception: `--help` / `--version` print plain text even with `--json`, and
128
+ `ui` is interactive so it rejects `--json` (exit 2).
129
+ - `--project <dir>` (`disable` / `enable` / `enable --all` / `list` / `profile
130
+ save|apply|diff`) switches to project scope; see [Project scope](#project-scope).
131
+ - `--version` prints the version.
132
+ - `-v` / `--verbose` (or `AGENT_TOGGLE_DEBUG=1`) adds a traceback on stderr for
133
+ unexpected errors; otherwise they are a single `error:` line.
134
+ - `--color auto|always|never` sets ANSI color for human output: green ok, red errors,
135
+ yellow warnings, cyan harness names, dim secondary text (`status`, `list`, `cost`,
136
+ `disable` / `enable`, `doctor`, warnings and errors; the picker uses curses color pairs
137
+ and stays monochrome when the terminal has no colors). `--json` is never colored.
138
+ `never` turns it off and `always` forces it even when piped; `auto` (default) checks
139
+ `NO_COLOR`, then `FORCE_COLOR`, then `TERM=dumb`, and otherwise colors only on a TTY.
140
+ `always` still stays plain on a Windows console that cannot do ANSI.
141
+
142
+ Extra row fields: `list` rows carry `at`, `mechanism`, `companions`; `cost` rows
143
+ carry `enabled`, `tokens`, `would_save`, `chars`, and the final `total` row
144
+ carries `total_tokens`, `saved_tokens`, `formula`. Rows for a
145
+ directory shared with another harness (see below) carry `shared_with`, the other
146
+ harnesses that read it: `disable` / `enable` rows relative to the harness you
147
+ asked, plus `owner`, the harness whose park dir and state entry hold the item;
148
+ `list` and `cost` rows are filed under the owner (`shared_with` is `[]` when
149
+ unshared).
150
+
151
+ `--dry-run` (`disable` / `enable` / `enable --all` / `undo` / `profile apply` /
152
+ `install-shims` / `ui`) computes the plan --
153
+ moves, companions, backups, MCP edits, warnings -- and writes nothing: no state,
154
+ log, lock or backup, and no chmod; it never shells out to `claude`. `disable` /
155
+ `enable` result rows have action `would-disable` / `would-enable`; every plan
156
+ row has status `planned`. Read-only commands (`list`, `status`, `cost`) also
157
+ change nothing on disk -- `status` warns about a `state.json` or
158
+ backup looser than `0600` instead of fixing it. A dry run whose plan contains a
159
+ failing item exits `1`, like the real run would.
160
+
161
+ | exit code | meaning |
162
+ |:-:|---|
163
+ | `0` | ok |
164
+ | `1` | partial failure (some items failed), or an unexpected error |
165
+ | `2` | usage error, including an unknown harness or type |
166
+ | `3` | locked by another run |
167
+ | `4` | unsupported harness/type pair, or harness not installed |
168
+
169
+ ```sh
170
+ agent-toggle disable skill demo-skill other-skill
171
+ agent-toggle disable command demo:batch # nested commands/demo/batch.md
172
+ agent-toggle disable agent demo-agent
173
+ agent-toggle disable mcp example-mcp
174
+ agent-toggle disable skill demo-skill --harness codex
175
+ agent-toggle enable mcp example-mcp
176
+ agent-toggle cost --type skill --json
177
+ ```
178
+
179
+ A colon addresses nesting: `demo:batch` is `commands/demo/batch.md`.
180
+
181
+ ## Interactive picker
182
+
183
+ ```sh
184
+ agent-toggle ui
185
+ ```
186
+
187
+ ```
188
+ filter: telegram█
189
+ *[x] (92) claude command telegram-summary
190
+ [ ] (61) claude skill telegram-display
191
+ [x] 48 claude skill telegram-group-send
192
+ [x] 20 claude mcp telegram-example
193
+
194
+ 4 shown | ~68 tok | harness:all type:all sort:name | 1 staged -- Enter to apply
195
+ Tab tick Enter apply Esc cancel s sort h/t filter ? keys / type to filter
196
+ ```
197
+
198
+ The number column is the estimated startup tokens (chars / 4, about +-25 %);
199
+ a parked row shows `(N)`, what restoring it would load. Plugins appear as rows
200
+ too (via `claude plugin list --json`; skipped under `ui --dry-run`, which never
201
+ shells out).
202
+
203
+ | key | action |
204
+ |---|---|
205
+ | `s` | cycle sort: name, cost (biggest first) |
206
+ | `h` | cycle the harness filter |
207
+ | `t` | cycle the type filter |
208
+ | `?` | show the key list |
209
+ | `/` | start typing a filter |
210
+ | any other printable character | appends to the filter (terms are ANDed, case-insensitive) |
211
+ | `Backspace` / `Ctrl-U` | delete one character / clear the filter |
212
+ | `↑` `↓` / `Ctrl-P` `Ctrl-N` | move; `PgUp`/`PgDn` jump a screen |
213
+ | `Tab` | tick / untick the highlighted row |
214
+ | `Enter` | apply every staged change (with `--dry-run`: show the plan) |
215
+ | `Esc` / `Ctrl-C` | cancel — nothing is applied |
216
+
217
+ `s`, `h`, `t` and `?` are commands while the filter is empty. Press `/` first
218
+ to type a filter that begins with one of them (the example above is typed
219
+ `/telegram`); once the filter is non-empty, every letter just types.
220
+
221
+ The checkbox shows the **enabled** state: `[x]` is live, `[ ]` is parked. A
222
+ `*` marks a row you changed.
223
+
224
+ Nothing happens while the picker is open. Changes are staged, the screen is
225
+ torn down, and only then do the real operations run — so their output (which
226
+ companion files moved, which were kept because they are shared) is readable
227
+ instead of fighting curses for the terminal.
228
+
229
+ Built on stdlib `curses`, so there is nothing to install on macOS and Linux
230
+ (on Windows, `pip install "agent-toggle[windows]"` pulls `windows-curses`).
231
+
232
+ ## What each harness supports
233
+
234
+ | harness | home | skill | agent | command | rule | plugin | mcp |
235
+ |---|---|:-:|:-:|:-:|:-:|:-:|:-:|
236
+ | claude | `~/.claude` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ `~/.claude.json` |
237
+ | codex | `~/.codex` | ✓ | ✓ | ✓ (`commands/` + `prompts/`) | — | — | ✓ `config.toml` |
238
+ | grok | `~/.grok` | ✓ | — | — | — | — | ✓ `config.toml` |
239
+ | opencode | `$XDG_CONFIG_HOME/opencode`, else `~/.config/opencode` | ✓ | — | ✓ (`command/`) | — | — | ✓ `opencode.json` (flag, *assumed*) |
240
+ | openclaw | `~/.openclaw` | ✓ (dir move, or flag when `skills.entries.<name>` exists, *assumed*) | ✓ | — | — | ✓ `openclaw.json` (flag, *assumed*) | — (sqlite) |
241
+ | copilot | `~/.copilot` | ✓ | ✓ | — | — | — | ✓ `mcp-config.json` |
242
+ | vibe | `~/.vibe` | ✓ | — | — | — | — | — |
243
+ | devin | `~/.devin` | — | — | — | — | — | — |
244
+ | agy | `~/.antigravity` | — | — | — | — | — | — |
245
+
246
+ Copilot's `config.json` is machine-managed and never edited by this tool; Copilot
247
+ plugins are not yet supported. `claude` plugins are toggled through `claude
248
+ plugin enable/disable`; `openclaw` plugins through a config flag (see [Assumed
249
+ formats](#assumed-formats)). Codex has no plugin CLI this tool can drive: `disable
250
+ plugin x --harness codex` exits `4`, which is why the matrix leaves it unchecked.
251
+
252
+ Flag items (openclaw plugins and flagged skills, opencode mcp) are toggled with
253
+ `disable` / `enable`, `undo` and `enable --all`. The picker, `cost` and `profile`
254
+ list them live (read from the config file; only entries that carry a boolean
255
+ `enabled`, since a key is never invented) and parked (from `state.json`); an
256
+ openclaw skill whose flag was switched off by this tool is shown as disabled even
257
+ though its directory is still in place.
258
+
259
+ The grok MCP location (`~/.grok/config.toml`, the same `[mcp_servers.<name>]`
260
+ tables as codex, including a `.headers` sub-table for remote servers) was
261
+ confirmed on a live install. Backups keep every sub-table verbatim.
262
+
263
+ Unsupported pairs fail loudly. OpenClaw keeps MCP servers in
264
+ `state/openclaw.sqlite`, not a file this tool can safely slice, so it refuses
265
+ rather than guessing.
266
+
267
+ ### Shared directories
268
+
269
+ OpenCode may read skills from another harness's directory through
270
+ `opencode.json` → `skills.paths` (absolute, `~/`-prefixed or relative-to-the-
271
+ opencode-dir entries; a bare `~` or `$HOME/...` is not expanded, and
272
+ `opencode.jsonc` is not read). Such a directory is **one** item, filed under
273
+ its owner (the harness whose home really holds it): it is
274
+ parked once, tracked once, and every row reports `shared_with`, the other
275
+ harnesses it also affects. `status` prints `shared dir with: ...`.
276
+
277
+ If the live directory carries `.synced-from-*` markers, a sync job may
278
+ re-create what you parked; `status` warns about it. Park in the source harness
279
+ instead. The warning is printed once per harness that views the directory.
280
+
281
+ ## Companion files
282
+
283
+ A skill or command often calls a helper next to it — `scripts/foo.py`,
284
+ `tg-send.sh`. On `disable`, the item's text is scanned for such references and
285
+ each one is classified:
286
+
287
+ - **exclusive** (nothing else in the harness mentions it) → parked alongside
288
+ the item, restored with it.
289
+ - **shared** (anything else references it) → **left alone**, and reported.
290
+
291
+ Sharing is a veto, not a warning. `tg-send.sh` is referenced by five different
292
+ commands; moving it because one of them got disabled would silently break the
293
+ other four.
294
+
295
+ Detection reads file text, so a path built at runtime cannot be found. Every
296
+ companion decision is printed before anything moves, so a wrong guess is
297
+ visible rather than silent.
298
+
299
+ ## MCP backups are verbatim
300
+
301
+ - **Claude**: the raw entry from `~/.claude.json`. Never `claude mcp get` —
302
+ that prints a human summary and silently drops auth fields (`headers`,
303
+ `headersHelper`), so a server restored from it fails with 401.
304
+ - **Codex**: the exact `[mcp_servers.<name>]` text block including its
305
+ `.tools.*` sub-tables. Python's stdlib reads TOML but cannot write it, and a
306
+ hand-rolled serializer would mangle comments — a text slice is lossless and
307
+ much less code.
308
+
309
+ MCP changes need a **new session** to take effect.
310
+
311
+ ### Claude MCP scopes
312
+
313
+ Both scopes stored in `~/.claude.json` are togglable, and the scope round-trips:
314
+
315
+ | scope | lives in | toggle |
316
+ |---|---|---|
317
+ | user | top-level `mcpServers` | ✓ |
318
+ | local | `projects/<dir>/mcpServers` | ✓ — project path saved with the backup |
319
+ | project | the repo's own `.mcp.json` | ✓ with `--project <dir>` only, see [Project scope](#project-scope) |
320
+ | claude.ai connector | your account | ✓ — recorded in each *existing* project's `disabledMcpServers`; a project first opened later needs the toggle re-run |
321
+
322
+ `claude mcp remove -s local` only sees the project it runs in, so the project
323
+ path is recorded at disable time and the restore runs back in that directory.
324
+ One name that is local-scope in several projects is refused unless your cwd
325
+ picks the winner — guessing would restore it into the wrong project.
326
+
327
+ ## Profiles
328
+
329
+ A profile is a named snapshot of which items are live, for the dotfiles repo
330
+ or a second machine. It holds only `{harness, type, name, live}` per item --
331
+ never a path or a secret.
332
+
333
+ ```sh
334
+ agent-toggle profile save work # -> ~/.agent-toggle/profiles/work.json
335
+ agent-toggle profile save work --out ~/dotfiles/agent-toggle/work.json
336
+ agent-toggle profile diff work # what apply would do; writes nothing
337
+ agent-toggle profile apply ~/dotfiles/agent-toggle/work.json --dry-run
338
+ agent-toggle profile apply work
339
+ agent-toggle profile list
340
+ ```
341
+
342
+ **`apply` toggles only the items the profile mentions.** An item the profile
343
+ lists as live but that is parked now is enabled; one it lists as parked but
344
+ that is live now is disabled; everything else is left alone -- in particular an
345
+ item installed after the save is never touched. An item the profile names that
346
+ this machine does not have gets a `skipped` row (`not on this machine`), not a
347
+ failure. `apply` runs through the same path as `enable` / `disable` (one lock,
348
+ one batch, logged, undoable with `undo`).
349
+
350
+ - An argument ending in `.json` or containing a path separator is a **file
351
+ path** (absolute paths are fine); anything else is a stored profile name under
352
+ `~/.agent-toggle/profiles`. `..` in a path, and names that are not plain file
353
+ names, are refused (exit `2`), as are a bad version, an unknown harness or
354
+ type, an invalid item name, a duplicate item or a file over 1 MiB.
355
+ - `--out` belongs to `save` only; `--dry-run` to `apply` / `diff`. `--harness H`
356
+ narrows `save`, `apply` and `diff` to one harness.
357
+ - `diff` and `apply --dry-run` do not read plugin state (that needs the claude
358
+ CLI, and a dry run never shells out), so profile plugin items show as
359
+ `skipped` there.
360
+ - A user-scope profile never contains project-scope entries; with
361
+ `--project <dir>` the same commands save, diff and apply that project's
362
+ items (claude layout) instead -- the profile holds no directory, so `apply`
363
+ needs `--project` again. A project profile records `"scope": "project"`;
364
+ applying it without `--project` (or a user profile with `--project`) exits `2`,
365
+ so a project profile can never disable the user's items.
366
+ - `save` skips (with a warning) any item whose name `apply` would refuse, such as
367
+ an MCP server called `team/search`, so a saved profile always applies.
368
+
369
+ ## Undo and `enable --all`
370
+
371
+ `undo` reads `~/.agent-toggle/log.jsonl`, takes the batch of the last
372
+ successful `disable` / `enable`, and reverses its rows in reverse order. The
373
+ reversal is logged as its own batch, so `undo` twice puts things back.
374
+ Project rows are replayed in their own project scope, never in user scope.
375
+
376
+ ```sh
377
+ agent-toggle undo --dry-run
378
+ agent-toggle undo
379
+ ```
380
+
381
+ - Nothing logged yet: `nothing to undo` (exit `0`). A log written before
382
+ batch ids existed: `nothing to undo: log predates undo` (exit `1`).
383
+ - `undo` rejects `--harness` (exit `2`): it reverses a whole batch.
384
+ - The log is a plain file you can edit; its names and project dirs are checked
385
+ like command-line input, and a project dir is trusted as written, exactly as
386
+ user-scope replay trusts the log.
387
+
388
+ `enable --all [--harness H] [--project <dir>]` restores every disabled entry in
389
+ scope, each in its own scope (`--project` takes only that project's).
390
+ `enable --all <name>` and `disable --all` are usage errors (exit `2`). Preview
391
+ bulk operations with `--dry-run`.
392
+
393
+ ## Project scope
394
+
395
+ `--project <dir>` (`.` = the current directory) points `disable`, `enable`,
396
+ `enable --all`, `list` and `profile` at one repo: its `.claude/` dir types
397
+ (`skill`, `agent`, `command`, `rule`) and its own `.mcp.json` servers (`mcp`).
398
+ Claude layout only: `--harness codex --project ...` exits `4`, as does a
399
+ missing directory or one with neither `.claude/` nor `.mcp.json`. `$HOME`, its
400
+ ancestors, the tool's own state dir and the harness homes are refused (exit
401
+ `2`) -- that is user scope. `list` and `enable --all` run the same checks. `cost` and
402
+ `ui` are user-scope only.
403
+
404
+ ```sh
405
+ agent-toggle disable skill demo-skill --project .
406
+ agent-toggle disable mcp example-mcp --project ~/work/repo
407
+ agent-toggle list --project .
408
+ agent-toggle enable --all --project .
409
+ ```
410
+
411
+ - Parked items go to `~/.agent-toggle/parked/<sha8>/`, **never inside the
412
+ project**; project and user state entries are separate, so one can never
413
+ restore into the other. Companion files are not moved in project scope.
414
+ - **A tracked file still disappears from the worktree.** Every project-scope
415
+ disable prints a warning like this one, and `git status` shows the deletion:
416
+
417
+ ```
418
+ <repo>/.claude/skills/demo-skill is a tracked deletion in git status; restore with: agent-toggle enable skill demo-skill --project <repo>
419
+ <repo>/.mcp.json is a tracked change in git status; restore with: agent-toggle enable mcp example-mcp --project <repo>
420
+ ```
421
+
422
+ Restore with that command, or `git checkout`; do not commit the deletion if
423
+ the repo is shared.
424
+ - `.mcp.json` is edited directly (no `claude` CLI) and must be **strict JSON**:
425
+ a BOM, comments or trailing commas are refused. `disable` rewrites the file in
426
+ its detected layout (tabs or 2 spaces, LF or CRLF) and saves a verbatim backup
427
+ at `mcp-backups/<sha8>__claude__<name>.json` (mode `0600`; it holds the file
428
+ text, so it may hold auth headers). `enable` restores the file byte for byte
429
+ if it is unchanged since the disable, otherwise merges the entry back in and
430
+ reformats.
431
+ - Moves across filesystems fall back to copy + delete (not atomic). An empty
432
+ `parked/<sha8>/*-disabled` dir may remain after `enable`; `doctor` ignores it.
433
+ - `status` prints one `project <dir>` line per project holding parked items.
434
+ A project with only `.mcp.json` saves only its parked servers in
435
+ `profile save --project`; live ones are not listed (the inventory needs
436
+ `.claude/`).
437
+
438
+ ## Doctor
439
+
440
+ ```sh
441
+ agent-toggle doctor [--harness H] [--json]
442
+ ```
443
+
444
+ Read-only: no lock, no state write-back, no `claude` CLI call. For each
445
+ installed harness it compares the live layout with the table row (expected
446
+ dirs and config keys), then cross-checks `state.json` against disk (parked
447
+ item present, origin dir present, backup present, project dir present, entry
448
+ passes the same tamper checks `enable` runs, modes no looser than `0600` /
449
+ `0700`). Rows (`action: doctor`) carry a status:
450
+
451
+ | status | meaning |
452
+ |---|---|
453
+ | `ok` | matches |
454
+ | `absent` | a dir, config file or key the row expects is not there (an MCP file never created, a missing `mcpServers` key) -- informational, exit `0` |
455
+ | `note` | worth knowing: shared dir, orphan backup, JSONC `openclaw.json` / `opencode.json`, a `--harness` that is not installed |
456
+ | `unverified` | could not be parsed here (an existing codex/grok `config.toml` on Python 3.10, which has no `tomllib`) |
457
+ | `warn` | loose file modes; a parked item with no state entry; a leftover `parked/<sha8>` dir that still holds files (an empty one after `enable` is ignored) |
458
+ | `error` | needs fixing: a config that exists but is unparseable or unsupported (`layout changed`), a state entry whose files are gone or fail the tamper checks, a flag re-enabled outside the tool |
459
+
460
+ Only `error` makes the exit code `1`; each problem row names the command that
461
+ fixes it. `--harness X` for a harness that is not installed is a `note` (exit
462
+ `0`). Companion files are not checked.
463
+
464
+ ## Assumed formats
465
+
466
+ Two config shapes come from the design survey (`docs/DESIGN.md` §4 / §11) and
467
+ were **not verified on a real install**:
468
+
469
+ - `openclaw.json`: `skills.entries.<name>.enabled` and
470
+ `plugins.entries.<name>.enabled`
471
+ - `opencode.json`: `mcp.<name>.enabled`
472
+
473
+ The edit changes one boolean token and nothing else, is verified after writing
474
+ and rolled back on any mismatch (bytes and file mode). Files that are not
475
+ strict JSON (JSONC / JSON5 -- comments, trailing commas) or that repeat a key are
476
+ **refused**, never rewritten; a leading BOM is kept as is; a missing key is
477
+ refused, never invented. An openclaw skill
478
+ uses the flag only when `skills.entries.<name>` already exists, otherwise its
479
+ directory is moved. If a run is killed between the flag write and the state
480
+ save, the flag is `false` with no state entry: `enable` then tells you to set it
481
+ back by hand. Please report a real install that differs.
482
+
483
+ ## Safety checks
484
+
485
+ - `enable` refuses a state entry whose `origin`, `parked_at`, backup or flag
486
+ file is outside its harness home, project or `~/.agent-toggle`, or holds `..`:
487
+ an error row `refused: <reason>`, nothing moved.
488
+ - Every edit of a JSON or TOML config is verified after writing (still parses,
489
+ only the target changed) and rolled back, bytes and mode, on failure. An
490
+ invalid codex `config.toml` makes an MCP edit fail and roll back instead of
491
+ being rewritten. A codex `config.toml` edit also fails (and is left as the other
492
+ tool wrote it) if the file changed between read and write, and CRLF files keep their
493
+ line endings. On Python 3.10 (no `tomllib`) the TOML check is textual only: it checks
494
+ against the original text that only the one block changed, but cannot parse.
495
+ - Profiles and project dirs are validated like command-line input.
496
+
497
+ ## Where state lives
498
+
499
+ All of it in `~/.agent-toggle/` (mode `0700`), never as marker files next to the targets —
500
+ your `git status` in your own project must not change because of our
501
+ bookkeeping.
502
+
503
+ | file | contents |
504
+ |---|---|
505
+ | `state.json` | current disabled list (schema v3; atomic write, mode `0600`) |
506
+ | `lock` | held by `disable` / `enable` / `enable --all` / `undo` / `profile apply` / `migrate` / `ui` for the whole batch; a second run waits 5 s then exits `3` (stale after 10 min *and* its PID is gone; a live batch refreshes it per item) |
507
+ | `log.jsonl` | one line per operation (mode `0600`), see below |
508
+ | `mcp-backups/` | `<harness>__<server>.json`, or `<sha8>__<harness>__<server>.json` for a project `.mcp.json` (mode `0600` -- may hold auth headers) |
509
+ | `companions/` | parked exclusive helper files |
510
+ | `parked/<sha8>/` | items parked by `--project` (`<sha8>` = first 8 hex of the SHA-1 of the resolved project dir) |
511
+ | `profiles/` | `<name>.json` profiles (dir `0700`, files `0600`) |
512
+
513
+ Each `log.jsonl` row is `{ts, harness, type, name, action, result, batch,
514
+ project, scope, detail}`. `batch` is one id per run (what `undo` reverses);
515
+ `project` is `null` and `scope` is `user` outside project scope (a claude
516
+ local-scope MCP row has `scope: local` and its working directory in `project`).
517
+ Older rows without `batch`/`harness` cannot be undone.
518
+
519
+ ## Two guardrails, both earned
520
+
521
+ **1. `safe_move()` never lets the source be renamed into the destination.**
522
+ `mv X dest/` when `dest` does not exist *renames* `X` to `dest` — the first
523
+ item fails, the second "succeeds" by becoming that directory, and `SKILL.md`
524
+ and `.git` end up scattered at the root. `shutil.move()` behaves identically.
525
+ So: create the directory, prove it *is* a directory, prove the target does not
526
+ exist, and only then touch the source.
527
+
528
+ Order matters too: `mkdir(exist_ok=True)` raises `FileExistsError` when the
529
+ path exists as a *file*, so the `is_dir()` check has to come **before** the
530
+ `mkdir` or it is unreachable.
531
+
532
+ **2. Park dirs that are not gitignored get a warning.** Without it, every
533
+ disable leaves dozens of deletion lines in `git status`.
534
+
535
+ ## Paths are resolved before comparison
536
+
537
+ `~/.claude` is often a symlink, and on macOS `/var` is one. Comparing
538
+ unresolved paths makes every companion look like it lives outside the harness.
539
+ Both sides are resolved first.
540
+
541
+ ## Design and roadmap
542
+
543
+ Architecture, harness survey, cost model, known gaps and the phased roadmap
544
+ toward a public multi-OS release live in `docs/DESIGN.md`. Release notes are in
545
+ `CHANGELOG.md`.
546
+
547
+ ## Tests
548
+
549
+ ```sh
550
+ python3 -m unittest discover -s tests -v
551
+ ruff check .
552
+ ```
553
+
554
+ stdlib `unittest`, no fixtures, no network. Every test runs against a
555
+ throwaway temp `HOME` and a stubbed `claude` CLI.
556
+
557
+ ## Releasing
558
+
559
+ A release is a git tag `vX.Y.Z` (where `X.Y.Z` must match `agent_toggle.__version__`).
560
+ Pushing the tag triggers `.github/workflows/release.yml`:
561
+
562
+ 1. **Build** job checks that the tag matches `__version__` and builds a wheel and
563
+ source distribution.
564
+ 2. **Publish** job uploads to PyPI using
565
+ [trusted publishing](https://docs.pypi.org/trusted-publishers/),
566
+ with no stored tokens—only the `pypi` environment and OIDC setup.
567
+ 3. **Smoke** job installs the published version on macOS under a throwaway `HOME`
568
+ and runs a `disable` / `enable` round trip to verify the install.
569
+
570
+ Before the first tag, register a trusted publisher for this repository with
571
+ PyPI: workflow `release.yml`, environment `pypi`. See
572
+ [PyPI trusted publishers documentation](https://docs.pypi.org/trusted-publishers/).
573
+
574
+ ## Cross-machine behaviour
575
+
576
+ Disabling is a **local** decision: park dirs are gitignored and do not sync.
577
+ But a disappearance under `skills/` is itself a tracked change, so committing
578
+ it means other machines lose those skills on pull. The content stays in git
579
+ history — `git checkout <commit> -- skills/<name>` brings it back. Don't
580
+ commit those deletions if you don't want them to travel.
@@ -0,0 +1,28 @@
1
+ agent_toggle/__init__.py,sha256=OuHeFXkh67d9_O-SvZ1LZRsKQXfbTGMO5bDQf9KdPbc,95
2
+ agent_toggle/__main__.py,sha256=w1DvuUhWR3RwPMCb-ov6LUj5hD-tcNLETVj_2zgOfpw,60
3
+ agent_toggle/cli.py,sha256=ttz22KpxrhZE3zaoxThYbndK46mPDPPKFRlORmtm1pU,25501
4
+ agent_toggle/companions.py,sha256=QgQfgm49nvUiAkuSkogmuAbZz4Q-3raxy0vfDoAtfC0,6534
5
+ agent_toggle/cost.py,sha256=IkwTWUxjo7G2qjRsFY0o_eCDJViWdxWaFXfg5mZm-4Q,13540
6
+ agent_toggle/doctor.py,sha256=IleVm0S6XQezSydjtXlv9f-FlJGe6xwIn_uO9o_Ogi0,14138
7
+ agent_toggle/fs.py,sha256=GDvzxxAb02rUX4kPklG7YF5AlItnGJBmfYsaLytm2eg,14504
8
+ agent_toggle/harnesses.py,sha256=rixjeX_-iy5Qx8gVZnLEr0beLO3Lik4nSHgPg1AqKbg,9520
9
+ agent_toggle/mechanisms.py,sha256=xwgM965JAC0YgiHpcs46lXAPt9xsuZhw4btOmp2c38U,45745
10
+ agent_toggle/ops.py,sha256=m1WAxU6a-3Jwt9VmyWpqwKV4IUO43BIbunrVkY2SL8M,4602
11
+ agent_toggle/output.py,sha256=VKMWMN7oMh8_S52xPWsyUez3WlA9EsoqpDK7BtYX8TQ,6200
12
+ agent_toggle/profiles.py,sha256=WnGuvooHSm4fEdFZB3M_yya_-4FAILF6u84HBg7Vhos,9473
13
+ agent_toggle/store.py,sha256=5bgwcDoeImSI6tqt_Jgbh9gAyfj0eX_cWSRcnnuSm3k,11274
14
+ agent_toggle/undo.py,sha256=MI-j4c6wOY-k01BRQjmGvKy5pG1GKDZoMDXL1qbw-xw,5961
15
+ agent_toggle/backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ agent_toggle/backends/flag_json.py,sha256=Q_dViwr3EgB1z20IIeCACHPJclvfwn_Sr4syx6bIUtA,5469
17
+ agent_toggle/backends/mcp_json.py,sha256=C0P9kk8RIs_Gh7OlrunLTpaazMsrUCylZGGxTz2jvQM,7935
18
+ agent_toggle/backends/mcp_toml.py,sha256=kvbWREDIEdKHLxdMRRs-7Lnd7ThBjdFIu5bWbG1ZIAw,3429
19
+ agent_toggle/backends/plugin_cli.py,sha256=SKwUxp-wWDq2ci6QRBBZT33l_0DTYPSKvrq1tMFD79M,1344
20
+ agent_toggle/shims/claude.md.tmpl,sha256=6A50pe0l8or6NDFOsTB0bRDvPcrOGhCXyjvzoy9EXFs,2431
21
+ agent_toggle/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ agent_toggle/ui/picker.py,sha256=nbVXig7kiuqJgzK6V8zSYlhNx9_DoALmhwIuWt5S4LU,10780
23
+ agent_toggle-0.1.0.dist-info/licenses/LICENSE,sha256=E6h_R-geEYPYCoL5hhYMi0GJ084D-_NPi1qPNkCpso4,1082
24
+ agent_toggle-0.1.0.dist-info/METADATA,sha256=Dgw2c2eQYtep11CWg3C4dRDMNELEcTwaQrHyVrUS04o,28756
25
+ agent_toggle-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
26
+ agent_toggle-0.1.0.dist-info/entry_points.txt,sha256=8SvRZQviSNwx9aUtbHeBdAus4lU1surrsuxAW6802T0,55
27
+ agent_toggle-0.1.0.dist-info/top_level.txt,sha256=Fy4GLpAauwylPM5exSHTewM8kceQk0v2ZKE20GYS86c,13
28
+ agent_toggle-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agent-toggle = agent_toggle.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 agent-toggle contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ agent_toggle