agent-toggle 0.2.0__tar.gz → 0.5.2__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 (93) hide show
  1. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/PKG-INFO +291 -90
  2. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/README.md +289 -88
  3. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/__init__.py +1 -1
  4. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/backends/flag_json.py +42 -12
  5. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/backends/mcp_json.py +154 -18
  6. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/backends/mcp_toml.py +1 -2
  7. agent_toggle-0.5.2/agent_toggle/backends/plugin_cli.py +90 -0
  8. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/cli.py +257 -55
  9. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/companions.py +6 -1
  10. agent_toggle-0.5.2/agent_toggle/config.py +136 -0
  11. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/cost.py +46 -27
  12. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/doctor.py +265 -55
  13. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/fs.py +128 -9
  14. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/harnesses.py +44 -18
  15. agent_toggle-0.5.2/agent_toggle/help.py +224 -0
  16. agent_toggle-0.5.2/agent_toggle/i18n.py +69 -0
  17. agent_toggle-0.5.2/agent_toggle/locales/__init__.py +5 -0
  18. agent_toggle-0.5.2/agent_toggle/locales/zh_tw_cli.py +85 -0
  19. agent_toggle-0.5.2/agent_toggle/locales/zh_tw_common.py +7 -0
  20. agent_toggle-0.5.2/agent_toggle/locales/zh_tw_config.py +89 -0
  21. agent_toggle-0.5.2/agent_toggle/locales/zh_tw_picker.py +80 -0
  22. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/mechanisms.py +315 -68
  23. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/ops.py +12 -4
  24. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/output.py +13 -4
  25. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/profiles.py +25 -13
  26. agent_toggle-0.5.2/agent_toggle/settings.py +184 -0
  27. agent_toggle-0.5.2/agent_toggle/spinner.py +54 -0
  28. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/store.py +41 -0
  29. agent_toggle-0.5.2/agent_toggle/toml_check.py +273 -0
  30. agent_toggle-0.5.2/agent_toggle/ui/config_menu.py +622 -0
  31. agent_toggle-0.5.2/agent_toggle/ui/menu.py +134 -0
  32. agent_toggle-0.5.2/agent_toggle/ui/model.py +245 -0
  33. agent_toggle-0.5.2/agent_toggle/ui/picker.py +503 -0
  34. agent_toggle-0.5.2/agent_toggle/ui/theme.py +365 -0
  35. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/undo.py +6 -1
  36. agent_toggle-0.5.2/agent_toggle/update_check.py +359 -0
  37. agent_toggle-0.5.2/agent_toggle/update_prompt.py +201 -0
  38. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle.egg-info/PKG-INFO +291 -90
  39. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle.egg-info/SOURCES.txt +23 -1
  40. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle.egg-info/requires.txt +1 -1
  41. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/pyproject.toml +2 -2
  42. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_alias.py +66 -8
  43. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_backends.py +81 -6
  44. agent_toggle-0.5.2/tests/test_cli.py +393 -0
  45. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_cli_surface.py +42 -2
  46. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_color.py +2 -0
  47. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_config.py +43 -10
  48. agent_toggle-0.5.2/tests/test_config_menu.py +420 -0
  49. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_conformance.py +11 -2
  50. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_cost.py +109 -0
  51. agent_toggle-0.5.2/tests/test_crash.py +379 -0
  52. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_doctor.py +168 -7
  53. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_fs.py +160 -4
  54. agent_toggle-0.5.2/tests/test_help.py +138 -0
  55. agent_toggle-0.5.2/tests/test_i18n.py +172 -0
  56. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_install.py +96 -0
  57. agent_toggle-0.5.2/tests/test_menu.py +243 -0
  58. agent_toggle-0.5.2/tests/test_picker.py +691 -0
  59. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_profiles.py +16 -0
  60. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_project.py +190 -0
  61. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_safety.py +4 -6
  62. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_secret_audit.py +23 -0
  63. agent_toggle-0.5.2/tests/test_settings.py +166 -0
  64. agent_toggle-0.5.2/tests/test_theme.py +318 -0
  65. agent_toggle-0.5.2/tests/test_update_check.py +421 -0
  66. agent_toggle-0.5.2/tests/test_update_prompt.py +41 -0
  67. agent_toggle-0.2.0/agent_toggle/backends/plugin_cli.py +0 -41
  68. agent_toggle-0.2.0/agent_toggle/config.py +0 -143
  69. agent_toggle-0.2.0/agent_toggle/ui/menu.py +0 -93
  70. agent_toggle-0.2.0/agent_toggle/ui/model.py +0 -72
  71. agent_toggle-0.2.0/agent_toggle/ui/picker.py +0 -213
  72. agent_toggle-0.2.0/tests/test_cli.py +0 -109
  73. agent_toggle-0.2.0/tests/test_menu.py +0 -86
  74. agent_toggle-0.2.0/tests/test_picker.py +0 -195
  75. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/LICENSE +0 -0
  76. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/__main__.py +0 -0
  77. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/backends/__init__.py +0 -0
  78. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/shims/claude.md.tmpl +0 -0
  79. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/shims/generic.md.tmpl +0 -0
  80. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle/ui/__init__.py +0 -0
  81. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle.egg-info/dependency_links.txt +0 -0
  82. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle.egg-info/entry_points.txt +0 -0
  83. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/agent_toggle.egg-info/top_level.txt +0 -0
  84. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/setup.cfg +0 -0
  85. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_companions.py +0 -0
  86. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_completions.py +0 -0
  87. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_containment.py +0 -0
  88. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_encoding.py +0 -0
  89. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_harnesses.py +0 -0
  90. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_mechanisms.py +0 -0
  91. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_platform.py +0 -0
  92. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_store.py +0 -0
  93. {agent_toggle-0.2.0 → agent_toggle-0.5.2}/tests/test_undo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-toggle
3
- Version: 0.2.0
3
+ Version: 0.5.2
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
@@ -27,7 +27,7 @@ License-File: LICENSE
27
27
  Provides-Extra: windows
28
28
  Requires-Dist: windows-curses; sys_platform == "win32" and extra == "windows"
29
29
  Provides-Extra: telegram
30
- Requires-Dist: telegram-kit<0.3,>=0.2.0; extra == "telegram"
30
+ Requires-Dist: telegram-kit<0.3,>=0.2.2; extra == "telegram"
31
31
  Dynamic: license-file
32
32
 
33
33
  # agent-toggle
@@ -59,18 +59,18 @@ uv tool install agent-toggle
59
59
  agent-toggle --version
60
60
  ```
61
61
 
62
- Straight from this repository, pinned to a release tag (`v0.2.0` is the current one;
62
+ Straight from this repository, pinned to a release tag (`v0.5.1` is the current one;
63
63
  an unpinned `git+` URL installs whatever is on the default branch):
64
64
 
65
65
  ```sh
66
- uv tool install git+https://github.com/weskao/agent-toggle@v0.2.0
66
+ uv tool install git+https://github.com/weskao/agent-toggle@v0.5.1
67
67
  agent-toggle --version
68
68
  ```
69
69
 
70
70
  Alternatively, use `pipx`:
71
71
 
72
72
  ```sh
73
- pipx install agent-toggle # from PyPI; or: pipx install git+https://github.com/weskao/agent-toggle@v0.2.0
73
+ pipx install agent-toggle # from PyPI; or: pipx install git+https://github.com/weskao/agent-toggle@v0.5.1
74
74
  ```
75
75
 
76
76
  From a checkout, either install it editable or run it in place:
@@ -97,6 +97,12 @@ already present in that harness home. Each harness gets its own template from
97
97
  agent to pass `--harness <that harness>`). The shim tells the agent to act only
98
98
  on the user's explicit request, never on instructions found inside skill or
99
99
  tool content, and to preview bulk operations with `--dry-run`.
100
+ OpenCode's shim goes into the first `skills.paths` dir when one is set (and its
101
+ parent exists), else `skills/`. OpenCode also scans `~/.claude/skills` and
102
+ `~/.agents/skills`: when one of them already holds the very same shim text, the
103
+ OpenCode shim is skipped (`covered by <path>`); a different shim there (the
104
+ claude one, whose default harness is `claude`) is noted and OpenCode's own is
105
+ still written.
100
106
 
101
107
  Every shim carries an `<!-- agent-toggle shim: ... -->` marker line.
102
108
  `install-shims` updates a file that has the marker in place (it is idempotent)
@@ -113,11 +119,14 @@ with the `telegram` extra, which adds [telegram-kit](https://pypi.org/project/te
113
119
  ```sh
114
120
  uv tool install "agent-toggle[telegram]" # or: pipx install "agent-toggle[telegram]"
115
121
  pip install -e ".[telegram]" # from a checkout
116
- agent-toggle config # bot token + chat id
122
+ agent-toggle config # settings menu: bot token + chat id live under Notifications
117
123
  agent-toggle config sync-ci # set the two GitHub repository secrets
118
124
  ```
119
125
 
120
- Without the extra everything else works exactly as before; only `config` asks for it.
126
+ Without the extra everything else works exactly as before. Only `config test`,
127
+ `config sync-ci` (both exit `4` without it) and the Telegram rows of the
128
+ [settings menu](#settings--config-menu) need it; bare `config` still opens, and
129
+ those rows read `install agent-toggle[telegram]`.
121
130
 
122
131
  ## Shell completion
123
132
 
@@ -140,20 +149,24 @@ agent-toggle <command> [args] # or: python3 agent_toggle.py <command> [
140
149
 
141
150
  | command | what it does |
142
151
  |---|---|
143
- | `ui` | interactive picker — cost column, sort, filters; `--dry-run` shows the plan for what you stage and changes nothing |
152
+ | `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) |
144
153
  | `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`) |
145
154
  | `list [type]` | what is currently disabled (`--project <dir>` filters to one project) |
146
- | `cost [--type T]` | estimated startup tokens per item, biggest first (read-only; `--harness H` filters) |
147
- | `install-shims` | write the skill shim into every installed harness; refuses to overwrite a file it did not write (`--dry-run` shows the plan) |
155
+ | `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) |
156
+ | `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); on a terminal, then asks y/n for each problem `doctor` can fix (see [Fixing](#fixing)) |
148
157
  | `disable <type> <name>...` | park one or more items (`--dry-run` shows the plan; `--project <dir>` for a repo's own `.claude/` and `.mcp.json`) |
149
158
  | `enable <type> <name>...` | put them back (`--dry-run` shows the plan; `--project <dir>` likewise) |
150
159
  | `enable --all` | put back **every** disabled item (`--harness H` narrows it, `--project <dir>` takes only that project's) |
151
160
  | `undo` | reverse the last logged batch (`--dry-run` shows the plan) |
152
161
  | `profile save\|apply\|diff\|list` | named sets of live items; see [Profiles](#profiles) |
153
- | `config [test\|sync-ci]` | Telegram settings for the CI failure alerts; see [CI notifications](#ci-notifications) |
154
- | `doctor` | read-only check of each harness layout and of `state.json` against disk; exit `1` only on an `error` row |
162
+ | `config` | settings menu (curses, else a numbered list); `config --json` lists every setting; see [Settings & config menu](#settings--config-menu) |
163
+ | `config test\|sync-ci` | send one Telegram test message / set the GitHub CI secrets; needs the `[telegram]` extra; see [CI notifications](#ci-notifications) |
164
+ | `help [command]` | styled help; `help ui` = `--help ui` = `ui --help`; see [Help](#help) |
165
+ | `doctor` | check each harness layout and `state.json` against disk; exit `1` only on an `error` row; on a terminal, asks y/n per fix it can run itself |
155
166
  | `migrate` | import an older `~/.claude-toggle/` state |
156
167
 
168
+ Every command also works with a leading `--` (`agent-toggle --status`, `--help`, `--config`); this README uses the plain form.
169
+
157
170
  `<type>` = `skill` / `agent` / `command` / `rule` / `plugin` / `mcp`.
158
171
  `rule` is claude-only (`~/.claude/rules/*.md`, parked in `rules-disabled/`).
159
172
  Disabling a rule prints a warning (also in `--json` `warnings`, dry run included)
@@ -169,9 +182,12 @@ Flags accepted by every command, before or after the subcommand:
169
182
  `{ExceptionType}: {message}` -- always emit it, with `"ok": false`.
170
183
  Exception: `--help` / `--version` print plain text even with `--json`, and
171
184
  `ui` is interactive so it rejects `--json` (exit 2).
172
- - `--project <dir>` (`disable` / `enable` / `enable --all` / `list` / `profile
185
+ - `--project <dir>` (`disable` / `enable` / `enable --all` / `list` / `ui` / `cost` / `profile
173
186
  save|apply|diff`) switches to project scope; see [Project scope](#project-scope).
174
187
  - `--version` prints the version.
188
+ - `AGENT_TOGGLE_CLI_TIMEOUT=<seconds>` overrides the timeout of every `claude` CLI call
189
+ (default 30 s for read-only `plugin list`, 120 s for every other call); a
190
+ non-positive or non-numeric value is ignored with a warning.
175
191
  - `-v` / `--verbose` (or `AGENT_TOGGLE_DEBUG=1`) adds a traceback on stderr for
176
192
  unexpected errors; otherwise they are a single `error:` line.
177
193
  - `--color auto|always|never` sets ANSI color for human output: green ok, red errors,
@@ -180,6 +196,8 @@ Flags accepted by every command, before or after the subcommand:
180
196
  and stays monochrome when the terminal has no colors). `--json` is never colored.
181
197
  `never` turns it off and `always` forces it even when piped; `auto` (default) checks
182
198
  `NO_COLOR`, then `FORCE_COLOR`, then `TERM=dumb`, and otherwise colors only on a TTY.
199
+ Precedence: `--color` flag > `AGENT_TOGGLE_COLOR` > the `color` [setting](#settings--config-menu)
200
+ > `auto`; `NO_COLOR` still wins over `auto`.
183
201
  `always` still stays plain on a Windows console that cannot do ANSI.
184
202
 
185
203
  Extra row fields: `list` rows carry `at`, `mechanism`, `companions`; `cost` rows
@@ -224,57 +242,184 @@ A colon addresses nesting: `demo:batch` is `commands/demo/batch.md`.
224
242
  ## Interactive picker
225
243
 
226
244
  ```sh
227
- agent-toggle ui
245
+ agent-toggle ui # alias: agent-toggle pick
246
+ agent-toggle # same, on a terminal; elsewhere prints help and exits 2
228
247
  ```
229
248
 
230
249
  ```
231
- filter: telegram█
232
- *[x] (92) claude command telegram-summary
233
- [ ] (61) claude skill telegram-display
234
- [x] 48 claude skill telegram-group-send
235
- [x] 20 claude mcp telegram-example
236
-
237
- 4 shown | ~68 tok | harness:all type:all sort:name | 1 staged -- Enter to apply
238
- Tab tick Enter apply Esc cancel s sort h/t filter ? keys / type to filter
250
+ agent-toggle v0.5.1 5 resources · ~6.3k tok live · 1 staged (+300 tok)
251
+ All 5 │ claude 4 │ codex 2
252
+ 🔍 Type to search (name, path, fuzzy) type: All sort: name
253
+ Skills 3 ─────────────────────────────────────────────────────────────│ beta
254
+ ● alpha claude 1.2k █▎ │
255
+ › ● + beta claude (300) ▎ │ harness claude
256
+ ● zeta claude 5.0k █████ │ type skill
257
+ Agents 1 ─────────────────────────────────────────────────────────────│ state ○ parked
258
+ ● gamma claude 50 ▏ +shared │ staged → live
259
+ Commands 1 ───────────────────────────────────────────────────────────│ path ~/.claude/skills-disabled/beta
260
+ ● delta codex 10 ▏ │ cost 0 now; ~300 tok if restored
261
+ │ since 2026-09-19 10:00:00
262
+ 5 of 5 shown · 2/5
263
+ Space toggle Enter apply Esc cancel ? help / search ←→ harness t type s sort p profile a all
239
264
  ```
240
265
 
241
- The number column is the estimated startup tokens (chars / 4, about +-25 %);
242
- a parked row shows `(N)`, what restoring it would load. Plugins appear as rows
243
- too (via `claude plugin list --json`; skipped under `ui --dry-run`, which never
244
- shells out).
266
+ What is on screen:
267
+
268
+ - **Title bar**: version and a summary (resources, live tokens, staged changes and their token effect).
269
+ - **Harness tab bar**: `All` plus each enabled harness, with counts. Harnesses switched off in
270
+ the [settings](#settings--config-menu) are hidden.
271
+ - **Rows grouped by type** (Skills, Agents, Commands, ...) under a heading with a count.
272
+ `●` is live, `○` parked (`*` / `o` in ASCII); a `+` / `-` before the name marks a staged change,
273
+ and `+shared` marks a directory another harness also reads.
274
+ - **Cost column**: estimated startup tokens (chars / 4, about +-25 %) with a colored bar. A parked
275
+ row shows `(N)` in brackets, what restoring it would load. Plugins appear as rows too (via
276
+ `claude plugin list --json`; skipped under `ui --dry-run`, which never shells out).
277
+ - **Detail pane** at 100 columns or wider: harness, type, state, staged, path, cost, since,
278
+ mechanism, and what it is shared with.
279
+ - **Search bar** (🔍; plain `/` on terminals that cannot draw emoji: the Linux console, the legacy
280
+ Windows console, non-UTF-8 locales) under the tabs, always visible.
281
+ - **Per-harness colors** on the harness column, matching `ai-accounts list` (claude orange on
282
+ 256-color terminals, codex cyan, agy blue, grok yellow, vibe green, copilot red, opencode magenta).
283
+ - **Key-chip footer** and a `?` help overlay. A spinner shows on stderr while the list loads
284
+ (only on a TTY; off with `NO_COLOR`). The mouse wheel scrolls (xterm alternate-scroll mode).
245
285
 
246
286
  | key | action |
247
287
  |---|---|
248
- | `s` | cycle sort: name, cost (biggest first) |
249
- | `h` | cycle the harness filter |
288
+ | `Space` | toggle the highlighted row (live / parked) and stay on it (while filtering, `Space` types a space) |
289
+ | `Tab` | toggle the highlighted row and advance to the next |
290
+ | `Enter` | apply every staged change (with `--dry-run`: show the plan) |
291
+ | `Esc` / `Ctrl-C` | cancel; nothing is applied |
292
+ | `↑` `↓` `PgUp` `PgDn` `Home` `End`, `Ctrl-P` / `Ctrl-N` | move |
293
+ | `/` | 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` |
294
+ | `Backspace` / `Ctrl-U` | delete one character / clear the filter |
295
+ | `←` `→` | switch harness tab (also while typing a filter); `0` = All, `1`-`9` = that tab, `h` = next tab |
250
296
  | `t` | cycle the type filter |
297
+ | `s` | cycle sort: name, cost (biggest first); applies within each type group |
298
+ | `p` | profiles: stage a saved one, or save the live state (see below) |
251
299
  | `?` | show the key list |
252
- | `/` | start typing a filter |
253
- | any other printable character | appends to the filter (terms are ANDed, case-insensitive) |
254
- | `Backspace` / `Ctrl-U` | delete one character / clear the filter |
255
- | `↑` `↓` / `Ctrl-P` `Ctrl-N` | move; `PgUp`/`PgDn` jump a screen |
256
- | `Tab` | tick / untick the highlighted row |
257
- | `Enter` | apply every staged change (with `--dry-run`: show the plan) |
258
- | `Esc` / `Ctrl-C` | cancel — nothing is applied |
300
+ | `a` / `Ctrl-A` | toggle every visible row |
301
+
302
+ The command keys (`t`, `s`, `p`, `h`, `a`, `0`-`9`, `?`) act only while no filter is active.
303
+ Press `/` first to type a filter that begins with one of them; once the filter is non-empty,
304
+ every letter just types.
259
305
 
260
- `s`, `h`, `t` and `?` are commands while the filter is empty. Press `/` first
261
- to type a filter that begins with one of them (the example above is typed
262
- `/telegram`); once the filter is non-empty, every letter just types.
306
+ The start view (sort, harness tab, type filter) comes from the `picker_sort`, `picker_harness`
307
+ and `picker_type` [settings](#settings--config-menu). A profile skips harnesses that are hidden
308
+ and counts those items as `hidden by settings`; `ui --harness <name>` shows that harness even
309
+ when it is switched off.
263
310
 
264
- The checkbox shows the **enabled** state: `[x]` is live, `[ ]` is parked. A
265
- `*` marks a row you changed.
311
+ `p` opens a prompt over the list of saved profiles: type a number or name (or
312
+ `apply <name>`) and Enter to **stage** that profile's changes (only the items it
313
+ mentions; ones this machine lacks are skipped), then Enter in the picker applies
314
+ them like any other change, so `ui --dry-run` previews a profile and a stray
315
+ `p` changes nothing. `save <name>` writes the live state (not your staged changes) as
316
+ a profile, with the same name rules and project scope as `profile save`; it is
317
+ off under `--dry-run`. Esc closes the prompt.
266
318
 
267
319
  Nothing happens while the picker is open. Changes are staged, the screen is
268
- torn down, and only then do the real operations run — so their output (which
320
+ torn down, and only then do the real operations run, so their output (which
269
321
  companion files moved, which were kept because they are shared) is readable
270
322
  instead of fighting curses for the terminal.
271
323
 
272
324
  Built on stdlib `curses`, so there is nothing to install on macOS and Linux
273
325
  (on Windows, `pip install "agent-toggle[windows]"` pulls `windows-curses`).
274
326
  Without curses, `ui` falls back to a numbered menu with the same staging and the
275
- same result: type row numbers (`1 3 5-7`) to tick or untick, `/text` to filter,
276
- `s` / `h` / `t` to sort and cycle the harness and type filters, `a` to apply, `q`
277
- (or end of input) to cancel.
327
+ same result. It is grouped by type, with the same glyphs and color: type row
328
+ numbers (`1 3 5-7`) to tick or untick, `/text` to filter (`/` alone clears it),
329
+ `s` / `h` / `t` to sort and cycle the harness and type filters, `p` to list profiles,
330
+ `p <number|name>` to stage one and `p save <name>` to save the live state, `a` to
331
+ apply, `q` (or end of input) to cancel.
332
+
333
+ The chrome (title, tabs, footer, help, detail labels) is translated when `language` is
334
+ `zh-TW`; item names and paths are never translated.
335
+
336
+ ## Settings & config menu
337
+
338
+ ```sh
339
+ agent-toggle config # or: agent-toggle --config
340
+ agent-toggle config --json # every setting with its value and source; the token is masked
341
+ ```
342
+
343
+ `config` opens the settings menu: curses on a terminal, a numbered list otherwise. Each change
344
+ is saved the moment you make it. On a numbered list, end of input, `q` or an empty line exits `0`
345
+ (safe in CI; note it reads piped digits, so an open stdin pipe waits).
346
+
347
+ | group | rows |
348
+ |---|---|
349
+ | General | Check for updates, Color, Language, Default harness |
350
+ | Harnesses | one On/Off per harness, with `found ~/.x` or `not on this machine` |
351
+ | Picker | default sort, harness filter, type filter |
352
+ | Notifications | Telegram bot token (masked), Telegram chat ID |
353
+ | Tools | Health check (`doctor`; then asks y/n per fix it can run), Install shims, Undo last change (asks y/n), Send test message, Sync CI secrets (asks y/n); their output shows in an overlay |
354
+
355
+ Keys: `↑` `↓` move (wraps, skips headings), `←` `→` cycle a value, `Enter` / `Space` change or run
356
+ the row, `r` reset the row, `R` reset everything (asks y/n), `q` / `Esc` / `Ctrl-C` quit. In an
357
+ inline text field `Enter` commits, `Esc` cancels and `-` then `Enter` clears. A dim `env` tag
358
+ marks a row whose value is overridden by an environment variable. The Language row switches the
359
+ UI live.
360
+
361
+ Settings live in `~/.agent-toggle/config.json` (mode `0600`, written atomically; keys the
362
+ tool does not know are kept):
363
+
364
+ | key | values | default | env override |
365
+ |---|---|---|---|
366
+ | `update_check` | `true` / `false` | `true` | `AGENT_TOGGLE_UPDATE_CHECK` |
367
+ | `color` | `auto` / `always` / `never` | `auto` | `AGENT_TOGGLE_COLOR` |
368
+ | `language` | `en` / `zh-TW` | `en` | `AGENT_TOGGLE_LANG` |
369
+ | `default_harness` | a harness name | `claude` | `AGENT_TOGGLE_DEFAULT_HARNESS` |
370
+ | `harness.<name>` | `true` / `false`, for `claude` `codex` `grok` `opencode` `openclaw` `copilot` `vibe` `devin` `agy` | `true` | |
371
+ | `picker_sort` | `name` / `cost` | `name` | |
372
+ | `picker_harness` | `all` or a harness name | `all` | |
373
+ | `picker_type` | `all` or a type | `all` | |
374
+ | `telegram_chat_id` | a number, `-100...` or `@channel` | unset | `TG_CHAT_ID` |
375
+
376
+ The bot token is not in this file: it is in the OS keystore through telegram-kit
377
+ (`TG_BOT_TOKEN` overrides it). An environment variable always beats the file; an
378
+ invalid value in either is ignored with one warning on stderr.
379
+
380
+ - `harness.<name>` off hides that harness from `ui`, `list`, `status` and `cost` (`list`, `status` and
381
+ `cost` print a dim `N hidden by settings` note, and `--json` carries it in `warnings`; an item shared with a harness that is still
382
+ on stays listed) and `install-shims` skips it; an explicit
383
+ `--harness <name>` still reaches it, `ui --harness <name>` included.
384
+ - `default_harness` is what `disable` / `enable` act on without `--harness`, even when that
385
+ harness is switched off (with one warning on stderr);
386
+ `disable` / `enable --project` without `--harness` still default to `claude`.
387
+ - `language` translates the config menu, help, update prompt and picker chrome; command output stays English.
388
+ - Bare `config` works without the `[telegram]` extra; `config test` and `config sync-ci` are
389
+ unchanged and need it (exit `4` without it).
390
+
391
+ ## Update check
392
+
393
+ On every invocation (unless disabled) agent-toggle checks PyPI for a newer release. It starts
394
+ before the command and is offered after it, on every exit path; it overlaps the command and waits at most 0.8 s afterwards.
395
+
396
+ - **On a terminal** (stdin and stderr are TTYs, no `--json`): a panel on stderr, "agent-toggle X
397
+ is available (you have Y)", with `Update now`, `Skip` and `Skip until next version`, a release
398
+ notes link, and `↑` `↓` `Enter` `q`. `Update now` runs `uv tool upgrade agent-toggle`; if that
399
+ fails you get a yellow warning with the command to run yourself.
400
+ - **Off a terminal**: two stderr lines, `agent-toggle X is available (you have Y)` and
401
+ ` uv tool upgrade agent-toggle`.
402
+ - It never changes the exit code or stdout, so `--json` output stays one document.
403
+
404
+ It is one `GET https://pypi.org/pypi/agent-toggle/json` on a background thread with a 0.8 s
405
+ timeout, cached for 10 minutes in `~/.agent-toggle/update-check.json`. Nothing is sent beyond a
406
+ normal HTTP request with the User-Agent `agent-toggle-update-check`. See `SECURITY.md`
407
+ for the hardening. To turn it off, set `AGENT_TOGGLE_UPDATE_CHECK=0` or the `update_check` setting
408
+ to off (the settings menu's "Check for updates").
409
+
410
+ ## Help
411
+
412
+ ```sh
413
+ agent-toggle help # grouped overview
414
+ agent-toggle help config # one command; same as: --help config, config --help
415
+ ```
416
+
417
+ Help is grouped as Toggle (`ui`, `disable`, `enable`, `undo`, `profile`), Inspect (`status`,
418
+ `list`, `cost`, `doctor`) and Setup (`config`, `install-shims`, `migrate`), with the global
419
+ options, examples and exit codes. `help <command>` shows that command's usage, arguments,
420
+ options and examples; `help help` is the overview, `help version` shows how to print the version,
421
+ and an unknown command exits `2`. It honours `--color` and `NO_COLOR`, and is
422
+ translated when `language` is `zh-TW`.
278
423
 
279
424
  ## What each harness supports
280
425
 
@@ -330,16 +475,19 @@ rather than guessing.
330
475
  ### Shared directories
331
476
 
332
477
  OpenCode may read skills from another harness's directory through
333
- `opencode.json` → `skills.paths` (absolute, `~/`-prefixed or relative-to-the-
334
- opencode-dir entries; a bare `~` or `$HOME/...` is not expanded, and
335
- `opencode.jsonc` is not read). Such a directory is **one** item, filed under
478
+ `opencode.json` → `skills.paths` (absolute, `~`, `$HOME/...`
479
+ and `${HOME}/...` entries; read from `opencode.json`, else `opencode.jsonc`;
480
+ relative entries are skipped, since OpenCode resolves them against the session
481
+ directory). OpenCode also always scans `~/.claude/skills` and `~/.agents/skills`,
482
+ so claude's skills count as shared with it. Such a directory is **one** item, filed under
336
483
  its owner (the harness whose home really holds it): it is
337
484
  parked once, tracked once, and every row reports `shared_with`, the other
338
485
  harnesses it also affects. `status` prints `shared dir with: ...`.
339
486
 
340
487
  If the live directory carries `.synced-from-*` markers, a sync job may
341
- re-create what you parked; `status` warns about it. Park in the source harness
342
- instead. The warning is printed once per harness that views the directory.
488
+ re-create what you parked; `status` and `disable` warn about it. Park in the
489
+ source harness instead. The warning is printed once per real directory, naming
490
+ every harness that views it.
343
491
 
344
492
  ## Companion files
345
493
 
@@ -461,8 +609,10 @@ bulk operations with `--dry-run`.
461
609
  Claude layout only: `--harness codex --project ...` exits `4`, as does a
462
610
  missing directory or one with neither `.claude/` nor `.mcp.json`. `$HOME`, its
463
611
  ancestors, the tool's own state dir and the harness homes are refused (exit
464
- `2`) -- that is user scope. `list` and `enable --all` run the same checks. `cost` and
465
- `ui` are user-scope only.
612
+ `2`) -- that is user scope. `list`, `enable --all`, `cost` and `ui` run the same
613
+ checks. `cost --project` prices that project's items only (no plugins), and
614
+ `ui --project` stages and applies in project scope (its `p` key saves and applies
615
+ project profiles).
466
616
 
467
617
  ```sh
468
618
  agent-toggle disable skill demo-skill --project .
@@ -485,18 +635,21 @@ agent-toggle enable --all --project .
485
635
  Restore with that command, or `git checkout`; do not commit the deletion if
486
636
  the repo is shared.
487
637
  - `.mcp.json` is edited directly (no `claude` CLI) and must be **strict JSON**:
488
- a BOM, comments or trailing commas are refused. `disable` rewrites the file in
489
- its detected layout (tabs or 2 spaces, LF or CRLF) and saves a verbatim backup
490
- at `mcp-backups/<sha8>__claude__<name>.json` (mode `0600`; it holds the file
491
- text, so it may hold auth headers). `enable` restores the file byte for byte
492
- if it is unchanged since the disable, otherwise merges the entry back in and
493
- reformats.
494
- - Moves across filesystems fall back to copy + delete (not atomic). An empty
495
- `parked/<sha8>/*-disabled` dir may remain after `enable`; `doctor` ignores it.
638
+ a BOM, comments or trailing commas are refused. `disable` cuts only that
639
+ server's entry, leaving every other byte as it was, and saves a backup at
640
+ `mcp-backups/<sha8>__claude__<name>.json` (mode `0600`) holding only that entry
641
+ -- its own headers, never another server's. `enable` puts the entry's bytes
642
+ back into the current file (an unchanged file comes back byte for byte; after
643
+ other edits it goes after its old neighbour, else at the end, indented like the
644
+ file) and checks that nothing else changed.
645
+ - A move across filesystems copies into a temp dir beside the target, renames it
646
+ into place, then deletes the source, so a killed run leaves either the source
647
+ intact or the target complete, and the next run settles it (see "Where state
648
+ lives"). `enable` removes the emptied `parked/<sha8>/*-disabled` and
649
+ `parked/<sha8>` dirs; a dir that still holds anything is kept.
496
650
  - `status` prints one `project <dir>` line per project holding parked items.
497
- A project with only `.mcp.json` saves only its parked servers in
498
- `profile save --project`; live ones are not listed (the inventory needs
499
- `.claude/`).
651
+ A project with only `.mcp.json` has its live servers listed too in
652
+ `profile save --project`, next to its parked ones.
500
653
 
501
654
  ## Doctor
502
655
 
@@ -504,31 +657,56 @@ agent-toggle enable --all --project .
504
657
  agent-toggle doctor [--harness H] [--json]
505
658
  ```
506
659
 
507
- Read-only: no lock, no state write-back, no `claude` CLI call. For each
660
+ The check is read-only: no lock, no state write-back, no `claude` CLI call. For each
508
661
  installed harness it compares the live layout with the table row (expected
509
662
  dirs and config keys), then cross-checks `state.json` against disk (parked
510
663
  item present, origin dir present, backup present, project dir present, entry
511
- passes the same tamper checks `enable` runs, modes no looser than `0600` /
512
- `0700`). Rows (`action: doctor`) carry a status:
664
+ passes the same tamper checks `enable` runs, companion files present and not also
665
+ live, modes no looser than `0600` / `0700`). Rows (`action: doctor`) carry a status:
513
666
 
514
667
  | status | meaning |
515
668
  |---|---|
516
669
  | `ok` | matches |
517
670
  | `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` |
518
671
  | `note` | worth knowing: shared dir, orphan backup, JSONC `openclaw.json` / `opencode.json`, a `--harness` that is not installed |
519
- | `unverified` | could not be parsed here (an existing codex/grok `config.toml` on Python 3.10, which has no `tomllib`) |
520
- | `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) |
521
- | `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 |
672
+ | `unverified` | no version could be read, or the row has nothing to check |
673
+ | `warn` | loose file modes; a parked item or companion file with no state entry; a leftover `parked/<sha8>` dir that still holds files (an empty one is ignored); an op a killed run left in flight that the next change settles (`pending: done` / `undone`) |
674
+ | `error` | needs fixing: a config that exists but is unparseable or unsupported (`layout changed`), a state entry whose files (companions included) are gone or fail the tamper checks, a companion both live and parked, a flag re-enabled outside the tool, an op a killed run left in flight that needs you (`pending: stuck`, with the exact fix) |
522
675
 
523
676
  Only `error` makes the exit code `1`; each problem row names the command that
524
677
  fixes it. `--harness X` for a harness that is not installed is a `note` (exit
525
- `0`). Companion files are not checked.
678
+ `0`).
526
679
 
527
680
  A harness item parked with no state entry also carries `orphan`, and its fix
528
681
  follows from it: `identical` (the live copy has the same content: delete the
529
682
  parked copy), `differs` (a different live copy exists: compare, keep one), or
530
683
  `parked-only` (move it back, then `disable` it so the state records it).
531
684
 
685
+ ### Fixing
686
+
687
+ `install-shims` (and so `./install.sh`) runs the same check silently after
688
+ writing the shims and asks only about these fixable problems, each shown first.
689
+
690
+ On a terminal (stdin and stdout both a TTY, no `--json`) doctor then asks
691
+ `(y/n)` for each problem it can fix without a judgment call, and runs each yes
692
+ under the run lock; anything else keeps its `fix:` hint for you. A fixed row's
693
+ status becomes `fixed` and no longer counts toward the exit code; a failed fix
694
+ adds an `error` row. Piped, `--json`, or run by an agent through a shim, doctor
695
+ stays read-only.
696
+
697
+ | problem | fix it offers |
698
+ |---|---|
699
+ | loose mode on the state dir, `state.json` or a backup | `chmod 700` / `chmod 600` |
700
+ | backup with no state entry, server configured again in its harness | delete the backup (not offered while the server is missing, nor for project backups) |
701
+ | flag re-enabled outside the tool | `enable` the item, which clears the stale entry |
702
+ | origin dir gone | recreate it, then `enable` the item |
703
+ | op a killed run left in flight (`pending: done` / `undone`) | settle it now, as the next change would |
704
+ | orphan parked item, `identical` | delete the parked copy |
705
+ | orphan parked item, `parked-only` | move it back (restores it) |
706
+
707
+ Not offered: `differs` orphans, missing parked items or backups, a gone project
708
+ dir, tamper refusals, `pending: stuck`, companion conflicts, unparseable configs.
709
+
532
710
  ## Assumed formats
533
711
 
534
712
  Two config shapes came from the design survey (`docs/DESIGN.md` §4 / §11). Both
@@ -540,14 +718,16 @@ were since seen on one real install each (openclaw 2026.7.1-2, opencode 2.0.22;
540
718
  - `opencode.json`: `mcp.<name>.enabled`
541
719
 
542
720
  The edit changes one boolean token and nothing else, is verified after writing
543
- and rolled back on any mismatch (bytes and file mode). Files that are not
544
- strict JSON (JSONC / JSON5 -- comments, trailing commas) or that repeat a key are
545
- **refused**, never rewritten; a leading BOM is kept as is; a missing key is
546
- refused, never invented. An openclaw skill
721
+ and rolled back on any mismatch (bytes and file mode). JSONC (`//` and `/* */`
722
+ comments, trailing commas) is edited the same way: only the flag token changes,
723
+ comments and commas are kept. JSON5 (unquoted keys, single quotes, hex) and files
724
+ that repeat a key are **refused**, never rewritten; a leading BOM is kept as is; a
725
+ missing key is refused, never invented. An openclaw skill
547
726
  uses the flag only when `skills.entries.<name>` already exists, otherwise its
548
- directory is moved. If a run is killed between the flag write and the state
549
- save, the flag is `false` with no state entry: `enable` then tells you to set it
550
- back by hand. Please report a real install that differs.
727
+ directory is moved. The state entry (with the flag's previous value) is saved
728
+ before the flag is written, so a run killed in between is settled by the next
729
+ run, never guessed (see "Where state lives"). Please report a real install that
730
+ differs.
551
731
 
552
732
  ## Safety checks
553
733
 
@@ -559,8 +739,9 @@ back by hand. Please report a real install that differs.
559
739
  invalid codex `config.toml` makes an MCP edit fail and roll back instead of
560
740
  being rewritten. A codex `config.toml` edit also fails (and is left as the other
561
741
  tool wrote it) if the file changed between read and write, and CRLF files keep their
562
- line endings. On Python 3.10 (no `tomllib`) the TOML check is textual only: it checks
563
- against the original text that only the one block changed, but cannot parse.
742
+ line endings. On Python 3.10 (no `tomllib`) the edit is parse-checked by a stdlib structural
743
+ validator (`agent_toggle/toml_check.py`, differentially tested against `tomllib`),
744
+ on top of the textual check that only the one block changed.
564
745
  - Profiles and project dirs are validated like command-line input.
565
746
 
566
747
  ## Where state lives
@@ -571,13 +752,29 @@ bookkeeping.
571
752
 
572
753
  | file | contents |
573
754
  |---|---|
574
- | `state.json` | current disabled list (schema v3; atomic write, mode `0600`) |
575
- | `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) |
755
+ | `state.json` | current disabled list (schema v3; atomic write, mode `0600`), saved before each flag write or dir move with the op in flight under an optional `pending` key, and after each MCP / plugin item |
756
+ | `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; the error says when its PID is gone, i.e. a killed run left it) |
576
757
  | `log.jsonl` | one line per operation (mode `0600`), see below |
577
758
  | `mcp-backups/` | `<harness>__<server>.json`, or `<sha8>__<harness>__<server>.json` for a project `.mcp.json` (mode `0600` -- may hold auth headers) |
578
759
  | `companions/` | parked exclusive helper files |
579
760
  | `parked/<sha8>/` | items parked by `--project` (`<sha8>` = first 8 hex of the SHA-1 of the resolved project dir) |
580
761
  | `profiles/` | `<name>.json` profiles (dir `0700`, files `0600`) |
762
+ | `config.json` | [settings](#settings--config-menu) and the Telegram chat id (mode `0600`, atomic write, unknown keys kept) |
763
+ | `update-check.json` | the [update check](#update-check)'s 10-minute cache (mode `0600`, atomic write) |
764
+
765
+ A killed run loses at most the one item in flight. If that was a flag write or a
766
+ dir move (with its companions), its `pending` record (the full entry, a flag's
767
+ previous value included) lets the next `disable` / `enable` / `enable --all` /
768
+ `undo` / ... finish it (it reached disk: recorded) or drop it (it did not), with
769
+ a warning and a `recovered` log row (which `undo` does not reverse). `status` and
770
+ `doctor` report it, with `agent-toggle enable ...` when the item ends up
771
+ disabled. The one case left to you is a cross-filesystem move killed between its
772
+ two renames: both copies are complete, and the report names the `diff -r` to
773
+ check and the `rm -rf` that keeps either one. MCP and plugin items are saved
774
+ per item (when the next one starts), without a `pending` record (a project `.mcp.json`
775
+ edit has one): a kill inside that window
776
+ leaves the change unrecorded (an MCP server's backup stays in `mcp-backups/`).
777
+ A killed run also leaves its `lock`; the next run's error says so.
581
778
 
582
779
  Each `log.jsonl` row is `{ts, harness, type, name, action, result, batch,
583
780
  project, scope, detail}`. `batch` is one id per run (what `undo` reverses);
@@ -599,7 +796,9 @@ path exists as a *file*, so the `is_dir()` check has to come **before** the
599
796
  `mkdir` or it is unreachable.
600
797
 
601
798
  **2. Park dirs that are not gitignored get a warning.** Without it, every
602
- disable leaves dozens of deletion lines in `git status`.
799
+ disable leaves dozens of deletion lines in `git status`. The warning is shown once
800
+ per park dir, and only for a dir inside a git work tree (elsewhere `git status`
801
+ cannot be dirtied and the `.gitignore` fix would not apply).
603
802
 
604
803
  ## Paths are resolved before comparison
605
804
 
@@ -632,19 +831,21 @@ runs. The message names the repository, branch, 7-character commit and a link to
632
831
  Set it up once, with the `telegram` extra installed (see [Install](#install)):
633
832
 
634
833
  ```sh
635
- agent-toggle config # prompts for the bot token (hidden) and the chat id
834
+ agent-toggle config # settings menu: Notifications has the bot token (hidden) and chat id
636
835
  agent-toggle config test # send one test message
637
836
  agent-toggle config sync-ci # set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID on the repo
638
837
  ```
639
838
 
640
- `config` works like aicp's `--config`: Enter keeps the current value, `-` clears it.
641
- The bot token is kept only in the OS credential store (macOS Keychain, Linux Secret
642
- Service, Windows DPAPI) through telegram-kit, never in a file or on a command line, and
643
- is shown masked. With no credential store it refuses to store the token; set
644
- `TG_BOT_TOKEN` in the environment instead (`TG_CHAT_ID` likewise for the chat id). The
645
- chat id is ordinary configuration in `~/.agent-toggle/config.json`: a number, `-100...`
646
- for a group, or an `@channel`. `config sync-ci` needs the GitHub CLI (`gh`) signed in; it
647
- passes both values on stdin, and `--repo OWNER/REPO` / `--dry-run` work as elsewhere.
839
+ In the [settings menu](#settings--config-menu) an inline field commits on `Enter`, cancels on
840
+ `Esc`, and clears on `-` then `Enter`; the Tools group also has Send test message and Sync CI
841
+ secrets (the same as `config test` / `config sync-ci`, and they need the extra). The bot token
842
+ is kept only in the OS credential store (macOS Keychain, Linux Secret Service, Windows DPAPI)
843
+ through telegram-kit, never in a file or on a command line, and is shown masked. With no
844
+ credential store it refuses to store the token; set `TG_BOT_TOKEN` in the environment instead
845
+ (`TG_CHAT_ID` likewise for the chat id). The chat id (`telegram_chat_id`) is ordinary
846
+ configuration in `~/.agent-toggle/config.json`: a number, `-100...` for a group, or an
847
+ `@channel`. `config sync-ci` needs the GitHub CLI (`gh`) signed in; it passes both values on
848
+ stdin, and `--repo OWNER/REPO` / `--dry-run` work as elsewhere.
648
849
 
649
850
  To skip the tool, set the secrets by hand (each command prompts for the value):
650
851