agent-toggle 0.2.0__tar.gz → 0.6.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 (93) hide show
  1. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/PKG-INFO +293 -90
  2. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/README.md +291 -88
  3. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/__init__.py +1 -1
  4. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/backends/flag_json.py +42 -12
  5. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/backends/mcp_json.py +154 -18
  6. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/backends/mcp_toml.py +1 -2
  7. agent_toggle-0.6.0/agent_toggle/backends/plugin_cli.py +90 -0
  8. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/cli.py +257 -55
  9. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/companions.py +6 -1
  10. agent_toggle-0.6.0/agent_toggle/config.py +137 -0
  11. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/cost.py +63 -33
  12. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/doctor.py +265 -55
  13. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/fs.py +128 -9
  14. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/harnesses.py +44 -18
  15. agent_toggle-0.6.0/agent_toggle/help.py +224 -0
  16. agent_toggle-0.6.0/agent_toggle/i18n.py +69 -0
  17. agent_toggle-0.6.0/agent_toggle/locales/__init__.py +5 -0
  18. agent_toggle-0.6.0/agent_toggle/locales/zh_tw_cli.py +85 -0
  19. agent_toggle-0.6.0/agent_toggle/locales/zh_tw_common.py +7 -0
  20. agent_toggle-0.6.0/agent_toggle/locales/zh_tw_config.py +90 -0
  21. agent_toggle-0.6.0/agent_toggle/locales/zh_tw_picker.py +82 -0
  22. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/mechanisms.py +315 -68
  23. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/ops.py +12 -4
  24. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/output.py +13 -4
  25. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/profiles.py +25 -13
  26. agent_toggle-0.6.0/agent_toggle/settings.py +184 -0
  27. agent_toggle-0.6.0/agent_toggle/spinner.py +54 -0
  28. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/store.py +41 -0
  29. agent_toggle-0.6.0/agent_toggle/toml_check.py +273 -0
  30. agent_toggle-0.6.0/agent_toggle/ui/config_menu.py +624 -0
  31. agent_toggle-0.6.0/agent_toggle/ui/menu.py +134 -0
  32. agent_toggle-0.6.0/agent_toggle/ui/model.py +248 -0
  33. agent_toggle-0.6.0/agent_toggle/ui/picker.py +544 -0
  34. agent_toggle-0.6.0/agent_toggle/ui/theme.py +365 -0
  35. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/undo.py +6 -1
  36. agent_toggle-0.6.0/agent_toggle/update_check.py +359 -0
  37. agent_toggle-0.6.0/agent_toggle/update_prompt.py +201 -0
  38. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle.egg-info/PKG-INFO +293 -90
  39. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle.egg-info/SOURCES.txt +23 -1
  40. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle.egg-info/requires.txt +1 -1
  41. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/pyproject.toml +2 -2
  42. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_alias.py +66 -8
  43. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_backends.py +81 -6
  44. agent_toggle-0.6.0/tests/test_cli.py +393 -0
  45. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_cli_surface.py +42 -2
  46. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_color.py +2 -0
  47. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_config.py +51 -10
  48. agent_toggle-0.6.0/tests/test_config_menu.py +437 -0
  49. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_conformance.py +11 -2
  50. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_cost.py +112 -2
  51. agent_toggle-0.6.0/tests/test_crash.py +379 -0
  52. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_doctor.py +168 -7
  53. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_fs.py +160 -4
  54. agent_toggle-0.6.0/tests/test_help.py +138 -0
  55. agent_toggle-0.6.0/tests/test_i18n.py +172 -0
  56. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_install.py +96 -0
  57. agent_toggle-0.6.0/tests/test_menu.py +243 -0
  58. agent_toggle-0.6.0/tests/test_picker.py +746 -0
  59. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_profiles.py +16 -0
  60. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_project.py +190 -0
  61. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_safety.py +4 -6
  62. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_secret_audit.py +23 -0
  63. agent_toggle-0.6.0/tests/test_settings.py +166 -0
  64. agent_toggle-0.6.0/tests/test_theme.py +318 -0
  65. agent_toggle-0.6.0/tests/test_update_check.py +421 -0
  66. agent_toggle-0.6.0/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.6.0}/LICENSE +0 -0
  76. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/__main__.py +0 -0
  77. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/backends/__init__.py +0 -0
  78. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/shims/claude.md.tmpl +0 -0
  79. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/shims/generic.md.tmpl +0 -0
  80. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle/ui/__init__.py +0 -0
  81. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle.egg-info/dependency_links.txt +0 -0
  82. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle.egg-info/entry_points.txt +0 -0
  83. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/agent_toggle.egg-info/top_level.txt +0 -0
  84. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/setup.cfg +0 -0
  85. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_companions.py +0 -0
  86. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_completions.py +0 -0
  87. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_containment.py +0 -0
  88. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_encoding.py +0 -0
  89. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_harnesses.py +0 -0
  90. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_mechanisms.py +0 -0
  91. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_platform.py +0 -0
  92. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/tests/test_store.py +0 -0
  93. {agent_toggle-0.2.0 → agent_toggle-0.6.0}/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.6.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
@@ -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 and the package version; 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,186 @@ 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: the file's frontmatter `description` (skills,
278
+ agents, commands, rules), then harness, type, state, staged, path, cost, since,
279
+ mechanism, and what it is shared with.
280
+ - **Search bar** (🔍; plain `/` on terminals that cannot draw emoji: the Linux console, the legacy
281
+ Windows console, non-UTF-8 locales) under the tabs, always visible.
282
+ - **Per-harness colors** on the harness column, matching `ai-accounts list` (claude orange on
283
+ 256-color terminals, codex cyan, agy blue, grok yellow, vibe green, copilot red, opencode magenta).
284
+ - **Key-chip footer** and a `?` help overlay. A spinner shows on stderr while the list loads
285
+ (only on a TTY; off with `NO_COLOR`). The mouse wheel scrolls (xterm alternate-scroll mode).
245
286
 
246
287
  | key | action |
247
288
  |---|---|
248
- | `s` | cycle sort: name, cost (biggest first) |
249
- | `h` | cycle the harness filter |
289
+ | `Space` | toggle the highlighted row (live / parked) and stay on it (while filtering, `Space` types a space) |
290
+ | `Tab` | toggle the highlighted row and advance to the next |
291
+ | `Enter` | apply every staged change (with `--dry-run`: show the plan) |
292
+ | `Esc` / `Ctrl-C` | cancel; nothing is applied |
293
+ | `↑` `↓` `PgUp` `PgDn` `Home` `End`, `Ctrl-P` / `Ctrl-N` | move |
294
+ | `/` | 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` |
295
+ | `Backspace` / `Ctrl-U` | delete one character / clear the filter |
296
+ | `←` `→` | switch harness tab (also while typing a filter); `0` = All, `1`-`9` = that tab, `h` = next tab |
250
297
  | `t` | cycle the type filter |
298
+ | `s` | cycle sort: name, cost (biggest first); applies within each type group |
299
+ | `p` | profiles: stage a saved one, or save the live state (see below) |
251
300
  | `?` | 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 |
301
+ | `a` / `Ctrl-A` | toggle every visible row |
302
+
303
+ The command keys (`t`, `s`, `p`, `h`, `a`, `0`-`9`, `?`) act only while no filter is active.
304
+ Press `/` first to type a filter that begins with one of them; once the filter is non-empty,
305
+ every letter just types.
259
306
 
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.
307
+ The start view (sort, harness tab, type filter) comes from the `picker_sort`, `picker_harness`
308
+ and `picker_type` [settings](#settings--config-menu). A profile skips harnesses that are hidden
309
+ and counts those items as `hidden by settings`; `ui --harness <name>` shows that harness even
310
+ when it is switched off.
263
311
 
264
- The checkbox shows the **enabled** state: `[x]` is live, `[ ]` is parked. A
265
- `*` marks a row you changed.
312
+ `p` opens a prompt over the list of saved profiles: type a number or name (or
313
+ `apply <name>`) and Enter to **stage** that profile's changes (only the items it
314
+ mentions; ones this machine lacks are skipped), then Enter in the picker applies
315
+ them like any other change, so `ui --dry-run` previews a profile and a stray
316
+ `p` changes nothing. `save <name>` writes the live state (not your staged changes) as
317
+ a profile, with the same name rules and project scope as `profile save`; it is
318
+ off under `--dry-run`. Esc closes the prompt.
266
319
 
267
320
  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
321
+ torn down, and only then do the real operations run, so their output (which
269
322
  companion files moved, which were kept because they are shared) is readable
270
323
  instead of fighting curses for the terminal.
271
324
 
272
325
  Built on stdlib `curses`, so there is nothing to install on macOS and Linux
273
326
  (on Windows, `pip install "agent-toggle[windows]"` pulls `windows-curses`).
274
327
  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.
328
+ same result. It is grouped by type, with the same glyphs and color: type row
329
+ numbers (`1 3 5-7`) to tick or untick, `/text` to filter (`/` alone clears it),
330
+ `s` / `h` / `t` to sort and cycle the harness and type filters, `p` to list profiles,
331
+ `p <number|name>` to stage one and `p save <name>` to save the live state, `a` to
332
+ apply, `q` (or end of input) to cancel.
333
+
334
+ The chrome (title, tabs, footer, help, detail labels) is translated when `language` is
335
+ `zh-TW`; item names and paths are never translated.
336
+
337
+ ## Settings & config menu
338
+
339
+ ```sh
340
+ agent-toggle config # or: agent-toggle --config
341
+ agent-toggle config --json # every setting with its value and source, plus the package version; the token is masked
342
+ ```
343
+
344
+ `config` opens the settings menu: curses on a terminal, a numbered list otherwise. The title
345
+ shows the package version. Each change is saved the moment you make it. On a numbered list,
346
+ end of input, `q` or an empty line exits `0`
347
+ (safe in CI; note it reads piped digits, so an open stdin pipe waits).
348
+
349
+ | group | rows |
350
+ |---|---|
351
+ | General | Check for updates, Color, Language, Default harness |
352
+ | Harnesses | one On/Off per harness, with `found ~/.x` or `not on this machine` |
353
+ | Picker | default sort, harness filter, type filter |
354
+ | Notifications | Telegram bot token (masked), Telegram chat ID |
355
+ | 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 |
356
+
357
+ Keys: `↑` `↓` move (wraps, skips headings), `←` `→` cycle a value, `Enter` / `Space` change or run
358
+ the row, `r` reset the row, `R` reset everything (asks y/n), `q` / `Esc` / `Ctrl-C` quit. In an
359
+ inline text field `Enter` commits, `Esc` cancels and `-` then `Enter` clears. A dim `env` tag
360
+ marks a row whose value is overridden by an environment variable. The Language row switches the
361
+ UI live.
362
+
363
+ Settings live in `~/.agent-toggle/config.json` (mode `0600`, written atomically; keys the
364
+ tool does not know are kept):
365
+
366
+ | key | values | default | env override |
367
+ |---|---|---|---|
368
+ | `update_check` | `true` / `false` | `true` | `AGENT_TOGGLE_UPDATE_CHECK` |
369
+ | `color` | `auto` / `always` / `never` | `auto` | `AGENT_TOGGLE_COLOR` |
370
+ | `language` | `en` / `zh-TW` | `en` | `AGENT_TOGGLE_LANG` |
371
+ | `default_harness` | a harness name | `claude` | `AGENT_TOGGLE_DEFAULT_HARNESS` |
372
+ | `harness.<name>` | `true` / `false`, for `claude` `codex` `grok` `opencode` `openclaw` `copilot` `vibe` `devin` `agy` | `true` | |
373
+ | `picker_sort` | `name` / `cost` | `name` | |
374
+ | `picker_harness` | `all` or a harness name | `all` | |
375
+ | `picker_type` | `all` or a type | `all` | |
376
+ | `telegram_chat_id` | a number, `-100...` or `@channel` | unset | `TG_CHAT_ID` |
377
+
378
+ The bot token is not in this file: it is in the OS keystore through telegram-kit
379
+ (`TG_BOT_TOKEN` overrides it). An environment variable always beats the file; an
380
+ invalid value in either is ignored with one warning on stderr.
381
+
382
+ - `harness.<name>` off hides that harness from `ui`, `list`, `status` and `cost` (`list`, `status` and
383
+ `cost` print a dim `N hidden by settings` note, and `--json` carries it in `warnings`; an item shared with a harness that is still
384
+ on stays listed) and `install-shims` skips it; an explicit
385
+ `--harness <name>` still reaches it, `ui --harness <name>` included.
386
+ - `default_harness` is what `disable` / `enable` act on without `--harness`, even when that
387
+ harness is switched off (with one warning on stderr);
388
+ `disable` / `enable --project` without `--harness` still default to `claude`.
389
+ - `language` translates the config menu, help, update prompt and picker chrome; command output stays English.
390
+ - Bare `config` works without the `[telegram]` extra; `config test` and `config sync-ci` are
391
+ unchanged and need it (exit `4` without it).
392
+
393
+ ## Update check
394
+
395
+ On every invocation (unless disabled) agent-toggle checks PyPI for a newer release. It starts
396
+ before the command and is offered after it, on every exit path; it overlaps the command and waits at most 0.8 s afterwards.
397
+
398
+ - **On a terminal** (stdin and stderr are TTYs, no `--json`): a panel on stderr, "agent-toggle X
399
+ is available (you have Y)", with `Update now`, `Skip` and `Skip until next version`, a release
400
+ notes link, and `↑` `↓` `Enter` `q`. `Update now` runs `uv tool upgrade agent-toggle`; if that
401
+ fails you get a yellow warning with the command to run yourself.
402
+ - **Off a terminal**: two stderr lines, `agent-toggle X is available (you have Y)` and
403
+ ` uv tool upgrade agent-toggle`.
404
+ - It never changes the exit code or stdout, so `--json` output stays one document.
405
+
406
+ It is one `GET https://pypi.org/pypi/agent-toggle/json` on a background thread with a 0.8 s
407
+ timeout, cached for 10 minutes in `~/.agent-toggle/update-check.json`. Nothing is sent beyond a
408
+ normal HTTP request with the User-Agent `agent-toggle-update-check`. See `SECURITY.md`
409
+ for the hardening. To turn it off, set `AGENT_TOGGLE_UPDATE_CHECK=0` or the `update_check` setting
410
+ to off (the settings menu's "Check for updates").
411
+
412
+ ## Help
413
+
414
+ ```sh
415
+ agent-toggle help # grouped overview
416
+ agent-toggle help config # one command; same as: --help config, config --help
417
+ ```
418
+
419
+ Help is grouped as Toggle (`ui`, `disable`, `enable`, `undo`, `profile`), Inspect (`status`,
420
+ `list`, `cost`, `doctor`) and Setup (`config`, `install-shims`, `migrate`), with the global
421
+ options, examples and exit codes. `help <command>` shows that command's usage, arguments,
422
+ options and examples; `help help` is the overview, `help version` shows how to print the version,
423
+ and an unknown command exits `2`. It honours `--color` and `NO_COLOR`, and is
424
+ translated when `language` is `zh-TW`.
278
425
 
279
426
  ## What each harness supports
280
427
 
@@ -330,16 +477,19 @@ rather than guessing.
330
477
  ### Shared directories
331
478
 
332
479
  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
480
+ `opencode.json` → `skills.paths` (absolute, `~`, `$HOME/...`
481
+ and `${HOME}/...` entries; read from `opencode.json`, else `opencode.jsonc`;
482
+ relative entries are skipped, since OpenCode resolves them against the session
483
+ directory). OpenCode also always scans `~/.claude/skills` and `~/.agents/skills`,
484
+ so claude's skills count as shared with it. Such a directory is **one** item, filed under
336
485
  its owner (the harness whose home really holds it): it is
337
486
  parked once, tracked once, and every row reports `shared_with`, the other
338
487
  harnesses it also affects. `status` prints `shared dir with: ...`.
339
488
 
340
489
  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.
490
+ re-create what you parked; `status` and `disable` warn about it. Park in the
491
+ source harness instead. The warning is printed once per real directory, naming
492
+ every harness that views it.
343
493
 
344
494
  ## Companion files
345
495
 
@@ -461,8 +611,10 @@ bulk operations with `--dry-run`.
461
611
  Claude layout only: `--harness codex --project ...` exits `4`, as does a
462
612
  missing directory or one with neither `.claude/` nor `.mcp.json`. `$HOME`, its
463
613
  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.
614
+ `2`) -- that is user scope. `list`, `enable --all`, `cost` and `ui` run the same
615
+ checks. `cost --project` prices that project's items only (no plugins), and
616
+ `ui --project` stages and applies in project scope (its `p` key saves and applies
617
+ project profiles).
466
618
 
467
619
  ```sh
468
620
  agent-toggle disable skill demo-skill --project .
@@ -485,18 +637,21 @@ agent-toggle enable --all --project .
485
637
  Restore with that command, or `git checkout`; do not commit the deletion if
486
638
  the repo is shared.
487
639
  - `.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.
640
+ a BOM, comments or trailing commas are refused. `disable` cuts only that
641
+ server's entry, leaving every other byte as it was, and saves a backup at
642
+ `mcp-backups/<sha8>__claude__<name>.json` (mode `0600`) holding only that entry
643
+ -- its own headers, never another server's. `enable` puts the entry's bytes
644
+ back into the current file (an unchanged file comes back byte for byte; after
645
+ other edits it goes after its old neighbour, else at the end, indented like the
646
+ file) and checks that nothing else changed.
647
+ - A move across filesystems copies into a temp dir beside the target, renames it
648
+ into place, then deletes the source, so a killed run leaves either the source
649
+ intact or the target complete, and the next run settles it (see "Where state
650
+ lives"). `enable` removes the emptied `parked/<sha8>/*-disabled` and
651
+ `parked/<sha8>` dirs; a dir that still holds anything is kept.
496
652
  - `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/`).
653
+ A project with only `.mcp.json` has its live servers listed too in
654
+ `profile save --project`, next to its parked ones.
500
655
 
501
656
  ## Doctor
502
657
 
@@ -504,31 +659,56 @@ agent-toggle enable --all --project .
504
659
  agent-toggle doctor [--harness H] [--json]
505
660
  ```
506
661
 
507
- Read-only: no lock, no state write-back, no `claude` CLI call. For each
662
+ The check is read-only: no lock, no state write-back, no `claude` CLI call. For each
508
663
  installed harness it compares the live layout with the table row (expected
509
664
  dirs and config keys), then cross-checks `state.json` against disk (parked
510
665
  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:
666
+ passes the same tamper checks `enable` runs, companion files present and not also
667
+ live, modes no looser than `0600` / `0700`). Rows (`action: doctor`) carry a status:
513
668
 
514
669
  | status | meaning |
515
670
  |---|---|
516
671
  | `ok` | matches |
517
672
  | `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
673
  | `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 |
674
+ | `unverified` | no version could be read, or the row has nothing to check |
675
+ | `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`) |
676
+ | `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
677
 
523
678
  Only `error` makes the exit code `1`; each problem row names the command that
524
679
  fixes it. `--harness X` for a harness that is not installed is a `note` (exit
525
- `0`). Companion files are not checked.
680
+ `0`).
526
681
 
527
682
  A harness item parked with no state entry also carries `orphan`, and its fix
528
683
  follows from it: `identical` (the live copy has the same content: delete the
529
684
  parked copy), `differs` (a different live copy exists: compare, keep one), or
530
685
  `parked-only` (move it back, then `disable` it so the state records it).
531
686
 
687
+ ### Fixing
688
+
689
+ `install-shims` (and so `./install.sh`) runs the same check silently after
690
+ writing the shims and asks only about these fixable problems, each shown first.
691
+
692
+ On a terminal (stdin and stdout both a TTY, no `--json`) doctor then asks
693
+ `(y/n)` for each problem it can fix without a judgment call, and runs each yes
694
+ under the run lock; anything else keeps its `fix:` hint for you. A fixed row's
695
+ status becomes `fixed` and no longer counts toward the exit code; a failed fix
696
+ adds an `error` row. Piped, `--json`, or run by an agent through a shim, doctor
697
+ stays read-only.
698
+
699
+ | problem | fix it offers |
700
+ |---|---|
701
+ | loose mode on the state dir, `state.json` or a backup | `chmod 700` / `chmod 600` |
702
+ | 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) |
703
+ | flag re-enabled outside the tool | `enable` the item, which clears the stale entry |
704
+ | origin dir gone | recreate it, then `enable` the item |
705
+ | op a killed run left in flight (`pending: done` / `undone`) | settle it now, as the next change would |
706
+ | orphan parked item, `identical` | delete the parked copy |
707
+ | orphan parked item, `parked-only` | move it back (restores it) |
708
+
709
+ Not offered: `differs` orphans, missing parked items or backups, a gone project
710
+ dir, tamper refusals, `pending: stuck`, companion conflicts, unparseable configs.
711
+
532
712
  ## Assumed formats
533
713
 
534
714
  Two config shapes came from the design survey (`docs/DESIGN.md` §4 / §11). Both
@@ -540,14 +720,16 @@ were since seen on one real install each (openclaw 2026.7.1-2, opencode 2.0.22;
540
720
  - `opencode.json`: `mcp.<name>.enabled`
541
721
 
542
722
  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
723
+ and rolled back on any mismatch (bytes and file mode). JSONC (`//` and `/* */`
724
+ comments, trailing commas) is edited the same way: only the flag token changes,
725
+ comments and commas are kept. JSON5 (unquoted keys, single quotes, hex) and files
726
+ that repeat a key are **refused**, never rewritten; a leading BOM is kept as is; a
727
+ missing key is refused, never invented. An openclaw skill
547
728
  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.
729
+ directory is moved. The state entry (with the flag's previous value) is saved
730
+ before the flag is written, so a run killed in between is settled by the next
731
+ run, never guessed (see "Where state lives"). Please report a real install that
732
+ differs.
551
733
 
552
734
  ## Safety checks
553
735
 
@@ -559,8 +741,9 @@ back by hand. Please report a real install that differs.
559
741
  invalid codex `config.toml` makes an MCP edit fail and roll back instead of
560
742
  being rewritten. A codex `config.toml` edit also fails (and is left as the other
561
743
  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.
744
+ line endings. On Python 3.10 (no `tomllib`) the edit is parse-checked by a stdlib structural
745
+ validator (`agent_toggle/toml_check.py`, differentially tested against `tomllib`),
746
+ on top of the textual check that only the one block changed.
564
747
  - Profiles and project dirs are validated like command-line input.
565
748
 
566
749
  ## Where state lives
@@ -571,13 +754,29 @@ bookkeeping.
571
754
 
572
755
  | file | contents |
573
756
  |---|---|
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) |
757
+ | `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 |
758
+ | `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
759
  | `log.jsonl` | one line per operation (mode `0600`), see below |
577
760
  | `mcp-backups/` | `<harness>__<server>.json`, or `<sha8>__<harness>__<server>.json` for a project `.mcp.json` (mode `0600` -- may hold auth headers) |
578
761
  | `companions/` | parked exclusive helper files |
579
762
  | `parked/<sha8>/` | items parked by `--project` (`<sha8>` = first 8 hex of the SHA-1 of the resolved project dir) |
580
763
  | `profiles/` | `<name>.json` profiles (dir `0700`, files `0600`) |
764
+ | `config.json` | [settings](#settings--config-menu) and the Telegram chat id (mode `0600`, atomic write, unknown keys kept) |
765
+ | `update-check.json` | the [update check](#update-check)'s 10-minute cache (mode `0600`, atomic write) |
766
+
767
+ A killed run loses at most the one item in flight. If that was a flag write or a
768
+ dir move (with its companions), its `pending` record (the full entry, a flag's
769
+ previous value included) lets the next `disable` / `enable` / `enable --all` /
770
+ `undo` / ... finish it (it reached disk: recorded) or drop it (it did not), with
771
+ a warning and a `recovered` log row (which `undo` does not reverse). `status` and
772
+ `doctor` report it, with `agent-toggle enable ...` when the item ends up
773
+ disabled. The one case left to you is a cross-filesystem move killed between its
774
+ two renames: both copies are complete, and the report names the `diff -r` to
775
+ check and the `rm -rf` that keeps either one. MCP and plugin items are saved
776
+ per item (when the next one starts), without a `pending` record (a project `.mcp.json`
777
+ edit has one): a kill inside that window
778
+ leaves the change unrecorded (an MCP server's backup stays in `mcp-backups/`).
779
+ A killed run also leaves its `lock`; the next run's error says so.
581
780
 
582
781
  Each `log.jsonl` row is `{ts, harness, type, name, action, result, batch,
583
782
  project, scope, detail}`. `batch` is one id per run (what `undo` reverses);
@@ -599,7 +798,9 @@ path exists as a *file*, so the `is_dir()` check has to come **before** the
599
798
  `mkdir` or it is unreachable.
600
799
 
601
800
  **2. Park dirs that are not gitignored get a warning.** Without it, every
602
- disable leaves dozens of deletion lines in `git status`.
801
+ disable leaves dozens of deletion lines in `git status`. The warning is shown once
802
+ per park dir, and only for a dir inside a git work tree (elsewhere `git status`
803
+ cannot be dirtied and the `.gitignore` fix would not apply).
603
804
 
604
805
  ## Paths are resolved before comparison
605
806
 
@@ -632,19 +833,21 @@ runs. The message names the repository, branch, 7-character commit and a link to
632
833
  Set it up once, with the `telegram` extra installed (see [Install](#install)):
633
834
 
634
835
  ```sh
635
- agent-toggle config # prompts for the bot token (hidden) and the chat id
836
+ agent-toggle config # settings menu: Notifications has the bot token (hidden) and chat id
636
837
  agent-toggle config test # send one test message
637
838
  agent-toggle config sync-ci # set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID on the repo
638
839
  ```
639
840
 
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.
841
+ In the [settings menu](#settings--config-menu) an inline field commits on `Enter`, cancels on
842
+ `Esc`, and clears on `-` then `Enter`; the Tools group also has Send test message and Sync CI
843
+ secrets (the same as `config test` / `config sync-ci`, and they need the extra). The bot token
844
+ is kept only in the OS credential store (macOS Keychain, Linux Secret Service, Windows DPAPI)
845
+ through telegram-kit, never in a file or on a command line, and is shown masked. With no
846
+ credential store it refuses to store the token; set `TG_BOT_TOKEN` in the environment instead
847
+ (`TG_CHAT_ID` likewise for the chat id). The chat id (`telegram_chat_id`) is ordinary
848
+ configuration in `~/.agent-toggle/config.json`: a number, `-100...` for a group, or an
849
+ `@channel`. `config sync-ci` needs the GitHub CLI (`gh`) signed in; it passes both values on
850
+ stdin, and `--repo OWNER/REPO` / `--dry-run` work as elsewhere.
648
851
 
649
852
  To skip the tool, set the secrets by hand (each command prompts for the value):
650
853