opencode-swap 0.4.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 (69) hide show
  1. opencode_swap-0.4.0/.gitignore +230 -0
  2. opencode_swap-0.4.0/LICENSE +21 -0
  3. opencode_swap-0.4.0/PKG-INFO +307 -0
  4. opencode_swap-0.4.0/README.md +280 -0
  5. opencode_swap-0.4.0/docs/architecture.md +327 -0
  6. opencode_swap-0.4.0/docs/opencode-auth.md +176 -0
  7. opencode_swap-0.4.0/docs/provider-research-prompt.md +193 -0
  8. opencode_swap-0.4.0/docs/provider-support.md +76 -0
  9. opencode_swap-0.4.0/docs/releasing.md +93 -0
  10. opencode_swap-0.4.0/docs/roadmap.md +61 -0
  11. opencode_swap-0.4.0/docs/security.md +208 -0
  12. opencode_swap-0.4.0/docs/testing.md +113 -0
  13. opencode_swap-0.4.0/integrations/opencode-tui-plugin/LICENSE +21 -0
  14. opencode_swap-0.4.0/integrations/opencode-tui-plugin/README.md +119 -0
  15. opencode_swap-0.4.0/integrations/opencode-tui-plugin/src/tui.tsx +514 -0
  16. opencode_swap-0.4.0/pyproject.toml +58 -0
  17. opencode_swap-0.4.0/src/opencode_swap/__init__.py +3 -0
  18. opencode_swap-0.4.0/src/opencode_swap/__main__.py +4 -0
  19. opencode_swap-0.4.0/src/opencode_swap/atomic.py +95 -0
  20. opencode_swap-0.4.0/src/opencode_swap/backup.py +128 -0
  21. opencode_swap-0.4.0/src/opencode_swap/cli.py +799 -0
  22. opencode_swap-0.4.0/src/opencode_swap/exceptions.py +60 -0
  23. opencode_swap-0.4.0/src/opencode_swap/locking.py +70 -0
  24. opencode_swap-0.4.0/src/opencode_swap/macos_keychain.py +136 -0
  25. opencode_swap-0.4.0/src/opencode_swap/models.py +190 -0
  26. opencode_swap-0.4.0/src/opencode_swap/oauth_jwt.py +66 -0
  27. opencode_swap-0.4.0/src/opencode_swap/oauth_refresh.py +156 -0
  28. opencode_swap-0.4.0/src/opencode_swap/opencode_auth.py +54 -0
  29. opencode_swap-0.4.0/src/opencode_swap/paths.py +83 -0
  30. opencode_swap-0.4.0/src/opencode_swap/process_detection.py +29 -0
  31. opencode_swap-0.4.0/src/opencode_swap/providers/__init__.py +26 -0
  32. opencode_swap-0.4.0/src/opencode_swap/providers/api.py +50 -0
  33. opencode_swap-0.4.0/src/opencode_swap/providers/base.py +91 -0
  34. opencode_swap-0.4.0/src/opencode_swap/providers/common.py +144 -0
  35. opencode_swap-0.4.0/src/opencode_swap/providers/github_copilot.py +46 -0
  36. opencode_swap-0.4.0/src/opencode_swap/providers/openai.py +186 -0
  37. opencode_swap-0.4.0/src/opencode_swap/providers/poe.py +64 -0
  38. opencode_swap-0.4.0/src/opencode_swap/providers/xai.py +72 -0
  39. opencode_swap-0.4.0/src/opencode_swap/providers/zai.py +31 -0
  40. opencode_swap-0.4.0/src/opencode_swap/sealed.py +83 -0
  41. opencode_swap-0.4.0/src/opencode_swap/store.py +551 -0
  42. opencode_swap-0.4.0/src/opencode_swap/switcher.py +952 -0
  43. opencode_swap-0.4.0/src/opencode_swap/transfer.py +149 -0
  44. opencode_swap-0.4.0/src/opencode_swap/usage.py +304 -0
  45. opencode_swap-0.4.0/tests/__init__.py +0 -0
  46. opencode_swap-0.4.0/tests/helpers.py +12 -0
  47. opencode_swap-0.4.0/tests/test_atomic.py +183 -0
  48. opencode_swap-0.4.0/tests/test_backup.py +75 -0
  49. opencode_swap-0.4.0/tests/test_cli.py +1050 -0
  50. opencode_swap-0.4.0/tests/test_concurrency.py +208 -0
  51. opencode_swap-0.4.0/tests/test_locking.py +77 -0
  52. opencode_swap-0.4.0/tests/test_macos_keychain.py +13 -0
  53. opencode_swap-0.4.0/tests/test_main.py +17 -0
  54. opencode_swap-0.4.0/tests/test_models.py +27 -0
  55. opencode_swap-0.4.0/tests/test_multi_provider.py +122 -0
  56. opencode_swap-0.4.0/tests/test_oauth_jwt.py +42 -0
  57. opencode_swap-0.4.0/tests/test_oauth_refresh.py +214 -0
  58. opencode_swap-0.4.0/tests/test_opencode_auth.py +65 -0
  59. opencode_swap-0.4.0/tests/test_paths.py +48 -0
  60. opencode_swap-0.4.0/tests/test_providers_multi.py +217 -0
  61. opencode_swap-0.4.0/tests/test_providers_openai.py +189 -0
  62. opencode_swap-0.4.0/tests/test_registry.py +205 -0
  63. opencode_swap-0.4.0/tests/test_store.py +430 -0
  64. opencode_swap-0.4.0/tests/test_switcher.py +858 -0
  65. opencode_swap-0.4.0/tests/test_switcher_restore.py +273 -0
  66. opencode_swap-0.4.0/tests/test_switcher_transfer.py +338 -0
  67. opencode_swap-0.4.0/tests/test_switcher_use.py +419 -0
  68. opencode_swap-0.4.0/tests/test_transfer.py +65 -0
  69. opencode_swap-0.4.0/tests/test_usage.py +471 -0
@@ -0,0 +1,230 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib64/
18
+ parts/
19
+ sdist/
20
+ var/
21
+ wheels/
22
+ share/python-wheels/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+ MANIFEST
27
+
28
+ # PyInstaller
29
+ # Usually these files are written by a python script from a template
30
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
31
+ *.manifest
32
+ *.spec
33
+
34
+ # Installer logs
35
+ pip-log.txt
36
+ pip-delete-this-directory.txt
37
+
38
+ # Unit test / coverage reports
39
+ htmlcov/
40
+ .tox/
41
+ .nox/
42
+ .coverage
43
+ .coverage.*
44
+ .cache
45
+ nosetests.xml
46
+ coverage.xml
47
+ *.cover
48
+ *.py.cover
49
+ .hypothesis/
50
+ .pytest_cache/
51
+ cover/
52
+
53
+ # Translations
54
+ *.mo
55
+ *.pot
56
+
57
+ # Django stuff:
58
+ *.log
59
+ local_settings.py
60
+ db.sqlite3
61
+ db.sqlite3-journal
62
+
63
+ # Flask stuff:
64
+ instance/
65
+ .webassets-cache
66
+
67
+ # Scrapy stuff:
68
+ .scrapy
69
+
70
+ # Sphinx documentation
71
+ docs/_build/
72
+
73
+ # PyBuilder
74
+ .pybuilder/
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ # For a library or package, you might want to ignore these files since the code is
86
+ # intended to run in multiple environments; otherwise, check them in:
87
+ # .python-version
88
+
89
+ # pipenv
90
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
91
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
92
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
93
+ # install all needed dependencies.
94
+ # Pipfile.lock
95
+
96
+ # UV
97
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
98
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
99
+ # commonly ignored for libraries.
100
+ # uv.lock
101
+
102
+ # poetry
103
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
104
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
105
+ # commonly ignored for libraries.
106
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
107
+ # poetry.lock
108
+ # poetry.toml
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
113
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
114
+ # pdm.lock
115
+ # pdm.toml
116
+ .pdm-python
117
+ .pdm-build/
118
+
119
+ # pixi
120
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
121
+ # pixi.lock
122
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
123
+ # in the .venv directory. It is recommended not to include this directory in version control.
124
+ .pixi
125
+
126
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
127
+ __pypackages__/
128
+
129
+ # Celery stuff
130
+ celerybeat-schedule
131
+ celerybeat.pid
132
+
133
+ # Redis
134
+ *.rdb
135
+ *.aof
136
+ *.pid
137
+
138
+ # RabbitMQ
139
+ mnesia/
140
+ rabbitmq/
141
+ rabbitmq-data/
142
+
143
+ # ActiveMQ
144
+ activemq-data/
145
+
146
+ # SageMath parsed files
147
+ *.sage.py
148
+
149
+ # Environments
150
+ .env
151
+ !/.gemini/.env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Ruff stuff:
198
+ .ruff_cache/
199
+
200
+ # PyPI configuration file
201
+ .pypirc
202
+
203
+ # Marimo
204
+ marimo/_static/
205
+ marimo/_lsp/
206
+ __marimo__/
207
+
208
+ # Streamlit
209
+ .streamlit/secrets.toml
210
+
211
+ #IDEs
212
+ # IntelliJ IDEA / JetBrains
213
+ .idea/*
214
+ !.idea/codeStyles/
215
+ !.idea/copyright/
216
+ *.iml
217
+ out/
218
+ .gradle/
219
+
220
+ # VS Code
221
+ .vscode/
222
+
223
+ # macOS
224
+ .DS_Store
225
+
226
+ # Password-encrypted opencode-swap account exports
227
+ *.ocs
228
+
229
+ # OpenCode TUI plugin dependencies (bun.lock is committed).
230
+ integrations/opencode-tui-plugin/node_modules/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Roberto Leinardi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,307 @@
1
+ Metadata-Version: 2.5
2
+ Name: opencode-swap
3
+ Version: 0.4.0
4
+ Summary: Multi-account switcher for OpenCode
5
+ Project-URL: Homepage, https://github.com/leinardi/opencode-swap
6
+ Project-URL: Repository, https://github.com/leinardi/opencode-swap
7
+ Project-URL: Issues, https://github.com/leinardi/opencode-swap/issues
8
+ Project-URL: Documentation, https://github.com/leinardi/opencode-swap#readme
9
+ Author-email: Roberto Leinardi <roberto@leinardi.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: account-switcher,cli,opencode
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.12
24
+ Requires-Dist: pycryptodomex>=3.19.0
25
+ Requires-Dist: pyzipper>=0.4.0
26
+ Description-Content-Type: text/markdown
27
+
28
+ # opencode-swap
29
+
30
+ **Multi-account switcher for [OpenCode](https://github.com/anomalyco/opencode).**
31
+
32
+ OpenCode has no built-in way to keep several accounts for one provider around and flip
33
+ between them. `opencode-swap` is a small standalone CLI that does exactly
34
+ that: it keeps a securely-stored copy of each account's OpenCode auth state
35
+ and swaps the one OpenCode currently considers "active."
36
+
37
+ ```bash
38
+ opencode-swap use openai personal && opencode
39
+ ```
40
+
41
+ Core workflow stays standalone: no background process or proxy runs while
42
+ OpenCode is. An optional terminal UI plugin adds account status and commands
43
+ without taking ownership of credentials or swaps.
44
+
45
+ ![Active account and usage in the session prompt](https://raw.githubusercontent.com/leinardi/opencode-swap/main/integrations/opencode-tui-plugin/assets/session_prompt_right.gif)
46
+
47
+ ![Account switching from the TUI command palette](https://raw.githubusercontent.com/leinardi/opencode-swap/main/integrations/opencode-tui-plugin/assets/command.gif)
48
+
49
+ ## Status
50
+
51
+ Early. Core behavior is covered by automated tests and has been exercised
52
+ against a real OpenCode installation. See
53
+ [`docs/roadmap.md`](https://github.com/leinardi/opencode-swap/blob/main/docs/roadmap.md)
54
+ for what's done and what's left.
55
+
56
+ ⚠️ **OpenAI is the only provider tested end to end with real accounts.** Other
57
+ provider implementations were derived from OpenCode source and synthetic tests.
58
+ Maintainer does not have accounts for those services, so testers are wanted for
59
+ every non-OpenAI provider.
60
+
61
+ > 🧪 **Testers wanted.** If you use any non-OpenAI provider, please open an
62
+ > issue with the provider, auth method, OpenCode version, and sanitized failure
63
+ > details. **Never include credentials.**
64
+ > [`docs/provider-research-prompt.md`](https://github.com/leinardi/opencode-swap/blob/main/docs/provider-research-prompt.md)
65
+ > has a copy-paste OpenCode prompt that gathers safe source evidence for one provider.
66
+
67
+ 🤖 This project is built largely with agentic AI coding tools, under human
68
+ review. Read the code before trusting it with your credentials.
69
+
70
+ ## Looking for load balancing?
71
+
72
+ `opencode-swap` activates one account at a time. If you instead want requests
73
+ spread across several accounts at runtime, that's a different problem, and
74
+ [`opencode-balancer`](https://github.com/thelioo/opencode-balancer) is the
75
+ project for it.
76
+ [`docs/security.md`](https://github.com/leinardi/opencode-swap/blob/main/docs/security.md)
77
+ covers the storage trade-offs each approach makes.
78
+
79
+ ## Install
80
+
81
+ ```bash
82
+ uv tool install opencode-swap # or: pipx install opencode-swap
83
+ opencode-swap --help
84
+ ```
85
+
86
+ `uvx opencode-swap --help` works too, without installing anything permanently.
87
+
88
+ ### From source
89
+
90
+ ```bash
91
+ git clone https://github.com/leinardi/opencode-swap.git
92
+ cd opencode-swap
93
+ uv sync
94
+ uv run opencode-swap --help
95
+ ```
96
+
97
+ `uv tool install .` from the checkout also works if you want `opencode-swap`
98
+ and `ocs` on your `PATH` from a local checkout.
99
+
100
+ ## Quickstart
101
+
102
+ ```bash
103
+ # 1. Log into OpenCode normally with your first account.
104
+ opencode auth login
105
+
106
+ # 2. Import whichever account is currently active into opencode-swap.
107
+ opencode-swap add openai personal
108
+
109
+ # 3. Log into a second account.
110
+ opencode auth login # choose/authenticate a different ChatGPT account
111
+ opencode-swap add openai work
112
+
113
+ # 4. See what's saved.
114
+ opencode-swap list
115
+ # personal ...abcd -
116
+ # * work ...ef01 -
117
+
118
+ # 5. Switch whenever you like.
119
+ opencode-swap use openai personal
120
+ opencode
121
+ ```
122
+
123
+ Names are yours to choose (lowercase letters/digits/`-`/`_`/`.`). Re-running
124
+ `add` with the provider and name of an already-imported account refreshes its stored
125
+ tokens in place; it won't create a duplicate.
126
+
127
+ ## Commands
128
+
129
+ | Command | Description |
130
+ | --- | --- |
131
+ | `opencode-swap add <provider> <name>` | Import the provider's currently-active account under `<name>`. |
132
+ | `opencode-swap list [provider] [--usage]` | List every saved account, or filter by provider; `--usage` adds a live quota line where supported (OpenAI ChatGPT OAuth, Z.AI `zai-coding-plan`; network, opt-in). |
133
+ | `opencode-swap current [provider]` | Show active managed accounts for every provider, or one provider. |
134
+ | `opencode-swap status [provider] [--json] [--usage]` | Show integration status; `--json` emits versioned secret-safe data; `--usage` as for `list`. |
135
+ | `opencode-swap use <provider> <name> [-y]` | Activate one saved provider account. |
136
+ | `opencode-swap switch <provider> [-y]` | Switch to next saved account for provider. |
137
+ | `opencode-swap refresh <provider> [name]` | Ensure a saved account's OAuth token is valid, refreshing over the network if expired (no-op if already valid); every saved account for the provider if `name` is omitted. Never refreshes whichever account is currently active in OpenCode — that account's own next request handles its refresh. |
138
+ | `opencode-swap remove <provider> <name> [-y]` | Delete a saved provider account. |
139
+ | `opencode-swap rename <provider> <old> <new>` | Rename a saved provider account. |
140
+ | `opencode-swap export <path>` | Export all saved accounts to a new password-encrypted `.ocs` archive. |
141
+ | `opencode-swap import <path>` | Import accounts from an encrypted archive; prompts to skip or overwrite existing names. |
142
+ | `opencode-swap restore [--pristine] [-y]` | Recover `auth.json` from the most recent pre-switch backup, or from the very first snapshot ever taken (`--pristine`). |
143
+ | `opencode-swap doctor` | Diagnose paths, schema compatibility, secret backend, and backup state. |
144
+
145
+ `use`, `switch`, `remove`, and `restore` ask for confirmation unless you pass `-y`/`--yes`
146
+ (and refuse to prompt at all on a non-interactive terminal, so pass `-y` in
147
+ scripts). `use` also warns if it detects a running `opencode` process, since
148
+ switching while OpenCode might be mid-token-refresh is the one real race
149
+ condition this tool can't fully close (see
150
+ [`docs/security.md`](https://github.com/leinardi/opencode-swap/blob/main/docs/security.md)).
151
+
152
+ No command ever prints an access token, refresh token, or API key. Account
153
+ ids are shown truncated to their last four characters.
154
+
155
+ ## OpenCode TUI integration
156
+
157
+ ```bash
158
+ opencode plugin @leinardi/opencode-swap --global
159
+ ```
160
+
161
+ [`integrations/opencode-tui-plugin`](https://github.com/leinardi/opencode-swap/blob/main/integrations/opencode-tui-plugin)
162
+ uses OpenCode's supported TUI-plugin API to render active account and
163
+ active-only usage at right side of session prompt metadata:
164
+
165
+ ```text
166
+ Plan · GPT-5.6 Sol OpenAI · medium work · 17%
167
+ ```
168
+
169
+ It only appears after session has sent a request through provider managed by
170
+ `opencode-swap`; it stays hidden for unrelated providers. `/swap` opens a
171
+ safe account picker, `/swap-next` rotates current provider, and `/swap-refresh`
172
+ refreshes visible status. See the
173
+ [plugin README](https://github.com/leinardi/opencode-swap/blob/main/integrations/opencode-tui-plugin/README.md)
174
+ for install and concurrency limits.
175
+
176
+ Core CLI does not require Bun. TUI plugin development and root `make verify`
177
+ require Bun 1.3.14; `make check` remains Python-only and offline.
178
+
179
+ ### Moving accounts to another computer
180
+
181
+ ```bash
182
+ # Source computer
183
+ opencode-swap export ~/opencode-accounts.ocs
184
+
185
+ # Transfer the archive, then on the destination computer
186
+ opencode-swap import ~/opencode-accounts.ocs
187
+ opencode-swap use openai personal
188
+ ```
189
+
190
+ `export` asks for a password twice; `import` asks for it once. Password input
191
+ requires an interactive terminal and is never placed in command arguments or
192
+ printed. The archive is AES-256 encrypted and created with `0600` permissions.
193
+ Delete the transfer archive after a successful import. See
194
+ [`docs/architecture.md`](https://github.com/leinardi/opencode-swap/blob/main/docs/architecture.md)
195
+ for how import handles naming conflicts and validates accounts before writing
196
+ anything.
197
+
198
+ ## How it works
199
+
200
+ The short version: OpenCode keeps all of its provider credentials in a
201
+ single JSON file, `~/.local/share/opencode/auth.json`, and re-reads it fresh
202
+ on every request; no restart is required to pick up a change. The entire unit
203
+ of "which account is active" is one top-level provider key in that file.
204
+ `opencode-swap` keeps a copy of each account's record in private storage, and
205
+ `use <provider> <name>` atomically replaces that one key.
206
+
207
+ The interesting part is what happens *between* switches: OpenCode rotates
208
+ the access and refresh token in place whenever it refreshes, so a naively
209
+ cached copy would go stale. `opencode-swap` captures that rotation back into
210
+ its own storage every time you switch *away* from an account, before
211
+ overwriting it with another. Providers using static API keys do not rotate
212
+ credentials, but use the same atomic switching path.
213
+
214
+ ### Provider support
215
+
216
+ | Provider/auth class | Status |
217
+ | --- | --- |
218
+ | OpenAI API key and ChatGPT OAuth | ✅ Supported; only end-to-end tested provider |
219
+ | Any provider using OpenCode's canonical `{"type":"api","key":"..."}` record | ⚠️ Supported generically; testers wanted |
220
+ | GitHub Copilot OAuth | ⚠️ Supported from OpenCode source analysis; testers wanted |
221
+ | Poe API/OAuth | ⚠️ Supported from pinned plugin source analysis; testers wanted |
222
+ | xAI API/OAuth | ⚠️ API supported; OAuth requires access JWT with stable `iss`/`sub`; testers wanted |
223
+ | Z.AI GLM Coding Plan (`zai-coding-plan`) | ⚠️ Supported through generic API handling; `--usage` reads the GLM Coding Plan quota; testers wanted |
224
+ | GitLab and Snowflake API/PAT | ⚠️ Supported through generic API handling; testers wanted |
225
+ | GitLab and Snowflake OAuth | ❌ Not supported: OpenCode does not persist a provably stable per-user identity |
226
+ | Well-known URL auth and unknown OAuth plugins | ❌ Not supported |
227
+
228
+ Same account name may exist under different providers. `openai:work` and
229
+ `anthropic:work` are separate accounts. Provider IDs must match keys in
230
+ OpenCode's `auth.json` exactly.
231
+
232
+ Full details, including the exact OpenCode internals this was reverse
233
+ engineered from and the switch algorithm's failure-recovery guarantees, are
234
+ in [`docs/opencode-auth.md`](https://github.com/leinardi/opencode-swap/blob/main/docs/opencode-auth.md)
235
+ and [`docs/architecture.md`](https://github.com/leinardi/opencode-swap/blob/main/docs/architecture.md).
236
+
237
+ ## Security
238
+
239
+ Credentials are stored via:
240
+
241
+ - **macOS**: the system Keychain, through the pinned `/usr/bin/security` CLI.
242
+ - **Linux**: atomic `chmod 0600` files under a `0700` directory, obfuscated
243
+ (base64) but not encrypted. This matches OpenCode's filesystem trust boundary
244
+ without interactive Secret Service unlock prompts. The optional TUI status
245
+ widget polls account usage, so every secret read must be non-interactive and
246
+ complete in bounded time. Linux Secret Service cannot guarantee this: an
247
+ unlocked keyring may prompt again, while a locked or unhealthy D-Bus service
248
+ can block the CLI indefinitely.
249
+ - **macOS fallback**: the same private-file backend when Keychain is
250
+ unavailable.
251
+
252
+ No custom cryptographic protocol. Portable exports use standard WinZip AES-256
253
+ implemented by `pyzipper`; plaintext credentials never touch a temporary file.
254
+ No plaintext database. Full threat model in
255
+ [`docs/security.md`](https://github.com/leinardi/opencode-swap/blob/main/docs/security.md).
256
+ See [`SECURITY.md`](https://github.com/leinardi/opencode-swap/blob/main/SECURITY.md)
257
+ to report a vulnerability.
258
+
259
+ ## Scope
260
+
261
+ **In scope:** standalone CLI, Linux + macOS, OpenCode provider accounts with
262
+ known safe identity semantics, secure multi-account storage, transactional switching with
263
+ rollback, backup/recovery, an architecture that doesn't need a rewrite to
264
+ add provider-specific behavior without changing safe I/O machinery.
265
+
266
+ **Out of scope:** load balancing / round-robin, runtime request interception,
267
+ a local proxy, Windows, unsupported/ambiguous OAuth providers, reimplementing
268
+ or modifying OpenCode, cloud sync. The core CLI stays standalone and owns
269
+ credentials and swaps; the optional TUI plugin is a read-only companion that
270
+ shells out to it rather than a separate GUI.
271
+
272
+ ## Documentation
273
+
274
+ - [`docs/architecture.md`](https://github.com/leinardi/opencode-swap/blob/main/docs/architecture.md): module map, data flow, the switch algorithm, transaction/rollback design.
275
+ - [`docs/opencode-auth.md`](https://github.com/leinardi/opencode-swap/blob/main/docs/opencode-auth.md): how OpenCode stores provider credentials and refreshes OpenAI credentials.
276
+ - [`docs/provider-support.md`](https://github.com/leinardi/opencode-swap/blob/main/docs/provider-support.md): provider matrix, source evidence, live-testing status, and deferred OAuth cases.
277
+ - [`docs/provider-research-prompt.md`](https://github.com/leinardi/opencode-swap/blob/main/docs/provider-research-prompt.md): safe copy-paste prompt for provider-research issues.
278
+ - [`docs/security.md`](https://github.com/leinardi/opencode-swap/blob/main/docs/security.md): threat model, storage mechanism comparison, what's explicitly out of scope.
279
+ - [`docs/testing.md`](https://github.com/leinardi/opencode-swap/blob/main/docs/testing.md): testing strategy and how to run the suite.
280
+ - [`docs/roadmap.md`](https://github.com/leinardi/opencode-swap/blob/main/docs/roadmap.md): what v1 ships and the known gaps.
281
+ - [`docs/releasing.md`](https://github.com/leinardi/opencode-swap/blob/main/docs/releasing.md): how to cut a CLI (PyPI) or TUI plugin (npm) release.
282
+ - [`AGENTS.md`](https://github.com/leinardi/opencode-swap/blob/main/AGENTS.md): project guide for AI coding agents working in this repo.
283
+ - [`CONTRIBUTING.md`](https://github.com/leinardi/opencode-swap/blob/main/CONTRIBUTING.md): how to set up, test, and submit changes.
284
+ - [`SECURITY.md`](https://github.com/leinardi/opencode-swap/blob/main/SECURITY.md): how to report a vulnerability.
285
+
286
+ ## Development
287
+
288
+ ```bash
289
+ make python-sync
290
+ make python-test # No network/keychain access required
291
+ make doctor
292
+ ```
293
+
294
+ Run `make help` for all development targets. Direct `uv` commands remain
295
+ available when Make is not installed.
296
+
297
+ See [`docs/testing.md`](https://github.com/leinardi/opencode-swap/blob/main/docs/testing.md)
298
+ for what the suite covers and how it keeps real macOS Keychain data and your
299
+ real `auth.json` out of the loop,
300
+ [`CONTRIBUTING.md`](https://github.com/leinardi/opencode-swap/blob/main/CONTRIBUTING.md)
301
+ for the PR workflow, and
302
+ [`AGENTS.md`](https://github.com/leinardi/opencode-swap/blob/main/AGENTS.md)
303
+ for conventions to follow when changing code here.
304
+
305
+ ## License
306
+
307
+ MIT