kyora-switch 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,211 @@
1
+ kyora-switch
2
+
3
+ Copyright 2026 Elia Hilse
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Apache License
12
+ Version 2.0, January 2004
13
+ http://www.apache.org/licenses/
14
+
15
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
16
+
17
+ 1. Definitions.
18
+
19
+ "License" shall mean the terms and conditions for use, reproduction,
20
+ and distribution as defined by Sections 1 through 9 of this document.
21
+
22
+ "Licensor" shall mean the copyright owner or entity authorized by
23
+ the copyright owner that is granting the License.
24
+
25
+ "Legal Entity" shall mean the union of the acting entity and all
26
+ other entities that control, are controlled by, or are under common
27
+ control with that entity. For the purposes of this definition,
28
+ "control" means (i) the power, direct or indirect, to cause the
29
+ direction or management of such entity, whether by contract or
30
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
31
+ outstanding shares, or (iii) beneficial ownership of such entity.
32
+
33
+ "You" (or "Your") shall mean an individual or Legal Entity
34
+ exercising permissions granted by this License.
35
+
36
+ "Source" form shall mean the preferred form for making modifications,
37
+ including but not limited to software source code, documentation
38
+ source, and configuration files.
39
+
40
+ "Object" form shall mean any form resulting from mechanical
41
+ transformation or translation of a Source form, including but
42
+ not limited to compiled object code, generated documentation,
43
+ and conversions to other media types.
44
+
45
+ "Work" shall mean the work of authorship, whether in Source or
46
+ Object form, made available under the License, as indicated by a
47
+ copyright notice that is included in or attached to the work
48
+ (an example is provided in the Appendix below).
49
+
50
+ "Derivative Works" shall mean any work, whether in Source or Object
51
+ form, that is based on (or derived from) the Work and for which the
52
+ editorial revisions, annotations, elaborations, or other modifications
53
+ represent, as a whole, an original work of authorship. For the purposes
54
+ of this License, Derivative Works shall not include works that remain
55
+ separable from, or merely link (or bind by name) to the interfaces of,
56
+ the Work and Derivative Works thereof.
57
+
58
+ "Contribution" shall mean any work of authorship, including
59
+ the original version of the Work and any modifications or additions
60
+ to that Work or Derivative Works thereof, that is intentionally
61
+ submitted to Licensor for inclusion in the Work by the copyright owner
62
+ or by an individual or Legal Entity authorized to submit on behalf of
63
+ the copyright owner. For the purposes of this definition, "submitted"
64
+ means any form of electronic, verbal, or written communication sent
65
+ to the Licensor or its representatives, including but not limited to
66
+ communication on electronic mailing lists, source code control systems,
67
+ and issue tracking systems that are managed by, or on behalf of, the
68
+ Licensor for the purpose of discussing and improving the Work, but
69
+ excluding communication that is conspicuously marked or otherwise
70
+ designated in writing by the copyright owner as "Not a Contribution."
71
+
72
+ "Contributor" shall mean Licensor and any individual or Legal Entity
73
+ on behalf of whom a Contribution has been received by Licensor and
74
+ subsequently incorporated within the Work.
75
+
76
+ 2. Grant of Copyright License. Subject to the terms and conditions of
77
+ this License, each Contributor hereby grants to You a perpetual,
78
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
79
+ copyright license to reproduce, prepare Derivative Works of,
80
+ publicly display, publicly perform, sublicense, and distribute the
81
+ Work and such Derivative Works in Source or Object form.
82
+
83
+ 3. Grant of Patent License. Subject to the terms and conditions of
84
+ this License, each Contributor hereby grants to You a perpetual,
85
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
86
+ (except as stated in this section) patent license to make, have made,
87
+ use, offer to sell, sell, import, and otherwise transfer the Work,
88
+ where such license applies only to those patent claims licensable
89
+ by such Contributor that are necessarily infringed by their
90
+ Contribution(s) alone or by combination of their Contribution(s)
91
+ with the Work to which such Contribution(s) was submitted. If You
92
+ institute patent litigation against any entity (including a
93
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
94
+ or a Contribution incorporated within the Work constitutes direct
95
+ or contributory patent infringement, then any patent licenses
96
+ granted to You under this License for that Work shall terminate
97
+ as of the date such litigation is filed.
98
+
99
+ 4. Redistribution. You may reproduce and distribute copies of the
100
+ Work or Derivative Works thereof in any medium, with or without
101
+ modifications, and in Source or Object form, provided that You
102
+ meet the following conditions:
103
+
104
+ (a) You must give any other recipients of the Work or
105
+ Derivative Works a copy of this License; and
106
+
107
+ (b) You must cause any modified files to carry prominent notices
108
+ stating that You changed the files; and
109
+
110
+ (c) You must retain, in the Source form of any Derivative Works
111
+ that You distribute, all copyright, patent, trademark, and
112
+ attribution notices from the Source form of the Work,
113
+ excluding those notices that do not pertain to any part of
114
+ the Derivative Works; and
115
+
116
+ (d) If the Work includes a "NOTICE" text file as part of its
117
+ distribution, then any Derivative Works that You distribute must
118
+ include a readable copy of the attribution notices contained
119
+ within such NOTICE file, excluding those notices that do not
120
+ pertain to any part of the Derivative Works, in at least one
121
+ of the following places: within a NOTICE text file distributed
122
+ as part of the Derivative Works; within the Source form or
123
+ documentation, if provided along with the Derivative Works; or,
124
+ within a display generated by the Derivative Works, if and
125
+ wherever such third-party notices normally appear. The contents
126
+ of the NOTICE file are for informational purposes only and
127
+ do not modify the License. You may add Your own attribution
128
+ notices within Derivative Works that You distribute, alongside
129
+ or as an addendum to the NOTICE text from the Work, provided
130
+ that such additional attribution notices cannot be construed
131
+ as modifying the License.
132
+
133
+ You may add Your own copyright statement to Your modifications and
134
+ may provide additional or different license terms and conditions
135
+ for use, reproduction, or distribution of Your modifications, or
136
+ for any such Derivative Works as a whole, provided Your use,
137
+ reproduction, and distribution of the Work otherwise complies with
138
+ the conditions stated in this License.
139
+
140
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
141
+ any Contribution intentionally submitted for inclusion in the Work
142
+ by You to the Licensor shall be under the terms and conditions of
143
+ this License, without any additional terms or conditions.
144
+ Notwithstanding the above, nothing herein shall supersede or modify
145
+ the terms of any separate license agreement you may have executed
146
+ with Licensor regarding such Contributions.
147
+
148
+ 6. Trademarks. This License does not grant permission to use the trade
149
+ names, trademarks, service marks, or product names of the Licensor,
150
+ except as required for reasonable and customary use in describing the
151
+ origin of the Work and reproducing the content of the NOTICE file.
152
+
153
+ 7. Disclaimer of Warranty. Unless required by applicable law or
154
+ agreed to in writing, Licensor provides the Work (and each
155
+ Contributor provides its Contributions) on an "AS IS" BASIS,
156
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
157
+ implied, including, without limitation, any warranties or conditions
158
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
159
+ PARTICULAR PURPOSE. You are solely responsible for determining the
160
+ appropriateness of using or redistributing the Work and assume any
161
+ risks associated with Your exercise of permissions under this License.
162
+
163
+ 8. Limitation of Liability. In no event and under no legal theory,
164
+ whether in tort (including negligence), contract, or otherwise,
165
+ unless required by applicable law (such as deliberate and grossly
166
+ negligent acts) or agreed to in writing, shall any Contributor be
167
+ liable to You for damages, including any direct, indirect, special,
168
+ incidental, or consequential damages of any character arising as a
169
+ result of this License or out of the use or inability to use the
170
+ Work (including but not limited to damages for loss of goodwill,
171
+ work stoppage, computer failure or malfunction, or any and all
172
+ other commercial damages or losses), even if such Contributor
173
+ has been advised of the possibility of such damages.
174
+
175
+ 9. Accepting Warranty or Additional Liability. While redistributing
176
+ the Work or Derivative Works thereof, You may choose to offer,
177
+ and charge a fee for, acceptance of support, warranty, indemnity,
178
+ or other liability obligations and/or rights consistent with this
179
+ License. However, in accepting such obligations, You may act only
180
+ on Your own behalf and on Your sole responsibility, not on behalf
181
+ of any other Contributor, and only if You agree to indemnify,
182
+ defend, and hold each Contributor harmless for any liability
183
+ incurred by, or claims asserted against, such Contributor by reason
184
+ of your accepting any such warranty or additional liability.
185
+
186
+ END OF TERMS AND CONDITIONS
187
+
188
+ APPENDIX: How to apply the Apache License to your work.
189
+
190
+ To apply the Apache License to your work, attach the following
191
+ boilerplate notice, with the fields enclosed by brackets "[]"
192
+ replaced with your own identifying information. (Don't include
193
+ the brackets!) The text should be enclosed in the appropriate
194
+ comment syntax for the file format. We also recommend that a
195
+ file or class name and description of purpose be included on the
196
+ same "printed page" as the copyright notice for easier
197
+ identification within third-party archives.
198
+
199
+ Copyright 2026 Elia Hilse
200
+
201
+ Licensed under the Apache License, Version 2.0 (the "License");
202
+ you may not use this file except in compliance with the License.
203
+ You may obtain a copy of the License at
204
+
205
+ http://www.apache.org/licenses/LICENSE-2.0
206
+
207
+ Unless required by applicable law or agreed to in writing, software
208
+ distributed under the License is distributed on an "AS IS" BASIS,
209
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
210
+ See the License for the specific language governing permissions and
211
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,8 @@
1
+ kyora-switch
2
+ Copyright 2026 Elia Hilse
3
+
4
+ This product includes software developed by Elia Hilse
5
+ (https://github.com/eliahilse).
6
+
7
+ Licensed under the Apache License, Version 2.0. See the LICENSE file
8
+ distributed with this work for the full terms.
package/README.md ADDED
@@ -0,0 +1,199 @@
1
+ # kyora switch
2
+
3
+ Hot-swap Claude Code and Codex logins. Save the account you are logged into as a named slot, then load it back later — no logging out, no re-auth, no browser round trip.
4
+
5
+ Built for the work-account / personal-account shuffle: two subscriptions, one laptop.
6
+
7
+ ```bash
8
+ claude # /login as account 1
9
+ kyora-switch claude save work
10
+
11
+ claude # /login as account 2
12
+ kyora-switch claude save private
13
+
14
+ kyora-switch claude load work # back to account 1
15
+ kyora-switch claude load private # and back again
16
+ ```
17
+
18
+ Codex works the same way, with its own slots:
19
+
20
+ ```bash
21
+ codex login
22
+ kyora-switch codex save work
23
+ kyora-switch codex load private
24
+ ```
25
+
26
+ ## Install
27
+
28
+ Requires [Bun](https://bun.sh). The tool runs on Bun's shell and file APIs, so `bun` has to be on your PATH. `npx` will not work.
29
+
30
+ ```bash
31
+ bun add -g kyora-switch
32
+ kyora-switch status
33
+ ```
34
+
35
+ Or run it without installing anything:
36
+
37
+ ```bash
38
+ bunx kyora-switch status
39
+ ```
40
+
41
+ macOS is the primary target: Claude Code keeps its OAuth blob in the login keychain there, and switch reads and writes it the same way. On Linux both CLIs keep credentials in files, which switch handles natively. Windows is untested.
42
+
43
+ From a checkout, the entry point carries a `#!/usr/bin/env bun` shebang, so a symlink from anywhere on your PATH is the whole install:
44
+
45
+ ```bash
46
+ ln -s "$PWD/packages/switch/cli/src/index.ts" ~/.local/bin/kyora-switch
47
+ ```
48
+
49
+ The link points at the checkout, so the tool runs whatever branch you have out. Or skip the install and run `bun packages/switch/cli/src/index.ts`.
50
+
51
+ ## Commands
52
+
53
+ Every provider takes the same verbs, under `kyora-switch claude …` or `kyora-switch codex …`:
54
+
55
+ | command | what it does |
56
+ | --- | --- |
57
+ | `save <slot>` | store the account that provider is logged into right now |
58
+ | `load <slot>` | log that provider back into a stored account |
59
+ | `list` | slots for that provider, with the live one marked |
60
+ | `usage` | how much quota each stored account has left |
61
+ | `sync` | copy the live login back into its slot |
62
+ | `clear` | sign out locally, without revoking the account |
63
+ | `rm <slot>` | delete a slot |
64
+ | `rename <old> <new>` | rename a slot |
65
+
66
+ Two commands span both:
67
+
68
+ | command | what it does |
69
+ | --- | --- |
70
+ | `status` | which account each CLI is logged into, and which slot it came from |
71
+ | `usage` | remaining quota across every stored account, both providers |
72
+ | `doctor` | where each CLI keeps its auth on this machine, and what it reads back |
73
+
74
+ Options: `--json` for `list`, `status` and `usage`, `-y` to skip the `rm` confirmation.
75
+
76
+ Slots are namespaced per provider, so `claude/work` and `codex/work` are independent — you can save one without touching the other.
77
+
78
+ ## Which account has room left
79
+
80
+ `usage` answers the question you actually have before switching. It probes each stored account with that slot's own token, so you see every account at once rather than only the one you are logged into:
81
+
82
+ ```
83
+ $ kyora-switch usage
84
+ claude — Claude Code
85
+ * work you@work.dev · Acme · max
86
+ 38% left session 50% used, resets in 3h 33m · weekly 32% used, resets in 4d · weekly Fable 62% used, resets in 4d
87
+ private you@home.dev · max
88
+ 9% left session 91% used, resets in 38m · weekly 44% used, resets in 3d
89
+
90
+ codex — Codex
91
+ * work you@work.dev · pro
92
+ 26% left 7d 74% used, resets in 6d · GPT-5.3-Codex-Spark 5h 0% used, resets in 5h
93
+ ```
94
+
95
+ `*` marks the account that is live, and the percentage is what is left on the tightest window — including the per-model ones, which are often the binding limit long before the plan-wide window is.
96
+
97
+ | provider | source |
98
+ | --- | --- |
99
+ | Claude Code | `api.anthropic.com/api/oauth/usage`, the endpoint `/usage` reads |
100
+ | Codex | `chatgpt.com/backend-api/codex/usage`, the endpoint `/status` reads |
101
+
102
+ Claude's payload carries a `limits` array covering the session window, the plan-wide weekly window, and a weekly window per model — that last one is where a `weekly Fable` or `weekly Opus` limit shows up, and it is easy to be near it while the plan-wide number still looks comfortable. Codex reports its plan window plus any model-scoped limits the account has.
103
+
104
+ The account that is live is always probed with its live credentials, which the CLI keeps refreshed. Other slots are probed with the access token frozen into them at save time, and Claude's expire after about eight hours, so those go quiet by the next day. `refresh` fixes that:
105
+
106
+ ```bash
107
+ kyora-switch claude refresh # renew stale slots, then show usage
108
+ kyora-switch usage --refresh # same, as part of a normal usage read
109
+ ```
110
+
111
+ It makes the call the CLI itself makes when its token ages out: `POST /v1/oauth/token` with `grant_type=refresh_token`. The refresh token rotates, so the new credentials are written straight back into the slot — a spent token is never left behind. Only slots that are *not* live are touched: the live account belongs to the CLI, which refreshes it under its own lock.
112
+
113
+ A refresh does not extend the session. The refresh token expiry is absolute, roughly four weeks from the login that created it, and comes back unchanged. Once past that, the slot needs a real login.
114
+
115
+ Codex access tokens last around ten days, so its slots keep reporting without any of this.
116
+
117
+ The probing and cooldown logic is [`@kyora-sh/usage`](../../packages/shared/usage), shared with kyora review and council so all three read quota the same way.
118
+
119
+ ## Slots never fall behind the CLI
120
+
121
+ Claude Code rotates its own tokens every few hours and writes them straight to the keychain. A slot saved in the morning holds the morning's token by the evening — still loadable, since the refresh token survives rotation, but stale enough that `usage` had nothing to read and a switch away froze the slot on an old credential.
122
+
123
+ So every `status`, `list`, `usage` and `load` first copies the live login back into whichever slot holds the same account. `load` does it before switching away, which is the moment that matters: the account you are leaving gets its freshest token saved before its keychain entry is overwritten. `sync` runs that step on its own.
124
+
125
+ It is a local file copy, nothing more — no network, no keychain write, and a slot that already matches is left untouched so its timestamps do not churn. Slots for other accounts are never involved.
126
+
127
+ ## Signing out without losing the account
128
+
129
+ `codex logout` and Claude's `/logout` end the account's session server side, which also invalidates the credentials sitting in your saved slots. That is the wrong tool when all you want is a free slot to log a second account into.
130
+
131
+ ```bash
132
+ kyora-switch codex clear # remove the local credentials, nothing else
133
+ codex login # now log in as someone else
134
+ ```
135
+
136
+ `clear` removes only what is on this machine: `~/.codex/auth.json` for Codex, and for Claude Code the OAuth blob plus the `oauthAccount` key. It makes no network call, so every slot you saved earlier still loads. The outgoing login is backed up first, exactly like `load` does.
137
+
138
+ `config.toml`, project history, settings and MCP config are untouched.
139
+
140
+ ## What actually gets swapped
141
+
142
+ Only the credentials and the account they belong to. Session history, project settings, and MCP config stay where they are.
143
+
144
+ **Claude Code**
145
+
146
+ - the OAuth blob, from the macOS login keychain (service `Claude Code-credentials`) or `~/.claude/.credentials.json` where there is no keychain
147
+ - `oauthAccount` in `~/.claude.json`, and nothing else in that file — `userID` and `machineID` identify the install, not the account, so they stay put
148
+
149
+ Nothing else is touched, which is the same surface `/login` changes when you sign in as a different account. In particular `policy-limits.json` and `remote-settings.json` are left alone: the second one carries your org's plugin and marketplace config, and an earlier version of this tool deleted it on every switch. The entitlement caches in `~/.claude.json` are left alone too — the CLI refetches them for whoever is logged in.
150
+
151
+ **Codex**
152
+
153
+ - `~/.codex/auth.json`. `config.toml` is configuration, not auth, so it stays.
154
+
155
+ ## Verified against the real CLIs
156
+
157
+ The write paths were read out of the shipped `claude` and `codex` binaries rather than guessed:
158
+
159
+ - Claude Code reads with `security find-generic-password -a <user> -w -s "Claude Code-credentials"` and writes with `add-generic-password -U -a <user> -s <service> -X <hex>`, piped through `security -i` so the secret never lands in process arguments. `security -i` silently truncates past 4096 characters, so both it and Claude Code fall back to plain argv for larger blobs. This does the same.
160
+ - `security -w` returns the value as hex whenever it holds a non-ASCII or control byte, which reads decode.
161
+ - The plaintext fallback is `<config dir>/.credentials.json` at mode `0600`, matching the `384` the binary chmods to.
162
+ - Codex keeps everything in `auth.json`; `id_token`, `access_token`, `refresh_token`, `account_id`, `last_refresh` and `auth_mode` all live there and nowhere else.
163
+
164
+ A save-then-load round trip on a real login returns the keychain blob byte for byte, with `~/.claude.json` keeping its projects, counters and install IDs.
165
+
166
+ ## How hot is "hot"
167
+
168
+ The swap itself is instant, but a CLI already running has its token in memory. Restart `claude` or `codex` after loading a slot — `load` tells you when it finds one running.
169
+
170
+ ## Safety
171
+
172
+ - every `load` copies the outgoing login to `~/.kyora/switch/backups/<provider>/<timestamp>/` first, so a switch made before you saved the current account is still recoverable — the last 10 per provider are kept
173
+ - `load` warns when the login it just replaced was in no slot
174
+ - slots and backups are written `0600` under `0700` directories, and restoring a credentials file tightens its mode to `0600` even if it was laxer
175
+ - files are replaced atomically, so a running CLI never reads a half-written config
176
+
177
+ Slots are plaintext credentials on disk, exactly like the files they came from. They are as sensitive as the logins themselves — do not sync `~/.kyora/switch` anywhere.
178
+
179
+ ## Environment
180
+
181
+ | variable | effect |
182
+ | --- | --- |
183
+ | `KYORA_SWITCH_DIR` | where slots and backups live (default `~/.kyora/switch`) |
184
+ | `KYORA_SWITCH_NO_KEYCHAIN=1` | keep Claude credentials in files only, never the keychain |
185
+ | `CLAUDE_CONFIG_DIR` | respected, same as Claude Code reads it |
186
+ | `CODEX_HOME` | respected, same as Codex reads it |
187
+
188
+ ## Development
189
+
190
+ ```bash
191
+ bun test
192
+ bun run check-types
193
+ ```
194
+
195
+ Tests never touch the real keychain or your real logins — they run against temp directories, with keychain access disabled by a preload (`src/test-setup.ts`).
196
+
197
+ ## License
198
+
199
+ [Apache-2.0](https://github.com/eliahilse/kyora/blob/main/packages/switch/cli/LICENSE). The rest of the kyora repo is [Elastic-2.0](https://github.com/eliahilse/kyora/blob/main/LICENSE); this package is the exception.
package/dist/index.js ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ import{parseArgs as $1}from"util";function R0($){if($===null||typeof $!=="object")return null;let X=$,Z=Array.isArray(X.limits)?_0(X.limits):[];return t(Z.length>0?Z:N0(X))}function t($){if($.length===0)return null;let X=Math.max(...$.map((Z)=>Z.usedPct));return{remainingPct:Math.max(0,Math.round(100-X)),detail:$.map((Z)=>`${Z.label} ${Z.usedPct}% used`).join(" \xB7 "),windows:$}}function _0($){let X=[];for(let Z of $){if(Z===null||typeof Z!=="object")continue;let G=Z;if(typeof G.percent!=="number")continue;X.push({label:D0(G),usedPct:Math.round(G.percent),resetsAt:v(G.resets_at),active:G.is_active===!0})}return X}function D0($){let X=typeof $.kind==="string"?$.kind:"limit",G=$.scope?.model,H=typeof G?.display_name==="string"?G.display_name:void 0;if(X==="session")return"session";if(X==="weekly_all")return"weekly";if(X==="weekly_scoped")return H?`weekly ${H}`:"weekly scoped";return H?`${X} ${H}`:X}function N0($){let X=[];for(let[Z,G]of[["five_hour","5h"],["seven_day","7d"]]){let H=$[Z];if(H===null||typeof H!=="object")continue;let Q=S0(H);if(Q===null)continue;X.push({label:G,usedPct:Math.round(Q),resetsAt:L0(H)})}return X}function v($){if(typeof $==="number")return $>100000000000?$:$*1000;if(typeof $!=="string")return;let X=Date.parse($);return Number.isNaN(X)?void 0:X}function L0($){return v($.resets_at)}function S0($){for(let[X,Z]of Object.entries($)){if(typeof Z!=="number")continue;if(/util|percent|pct/i.test(X))return Z<1?Z*100:Z}return null}async function i($,X){try{let Z=await fetch($,{headers:X,redirect:"error",signal:AbortSignal.timeout(5000)});if(!Z.ok)return null;return await Z.json()}catch{return null}}function r($){try{let X=JSON.parse($)?.claudeAiOauth?.accessToken;return typeof X==="string"&&X.length>0?X:void 0}catch{return}}async function o($){let X=await i(process.env.KYORA_CLAUDE_USAGE_URL??"https://api.anthropic.com/api/oauth/usage",{authorization:`Bearer ${$}`,"anthropic-beta":"oauth-2025-04-20"});return X?R0(X):null}function T0($){if(typeof $!=="number"||$<=0)return"window";let X=Math.round($/3600);return X<24?`${X}h`:`${Math.round(X/24)}d`}function a($,X=""){if($===null||typeof $!=="object")return[];let Z=$,G=[];for(let H of["primary_window","secondary_window"]){let Q=Z[H];if(Q===null||typeof Q!=="object")continue;let z=Q;if(typeof z.used_percent!=="number")continue;G.push({label:`${X}${T0(z.limit_window_seconds)}`,usedPct:Math.round(z.used_percent),resetsAt:v(z.reset_at)})}return G}function b0($){if($===null||typeof $!=="object")return null;let X=$,Z=a(X.rate_limit);if(Array.isArray(X.additional_rate_limits))for(let G of X.additional_rate_limits){if(G===null||typeof G!=="object")continue;let H=G,Q=typeof H.limit_name==="string"?`${H.limit_name} `:"";Z.push(...a(H.rate_limit,Q))}return t(Z)}function l($){try{let X=JSON.parse($)?.tokens,Z=X?.access_token,G=X?.account_id;return typeof Z==="string"&&typeof G==="string"?{token:Z,account:G}:null}catch{return null}}async function e($,X){let Z=await i(process.env.KYORA_CODEX_USAGE_URL??"https://chatgpt.com/backend-api/codex/usage",{authorization:`Bearer ${$}`,"chatgpt-account-id":X,accept:"application/json"});return Z?b0(Z):null}import{rm as u0}from"fs/promises";import{homedir as B0}from"os";import{join as h}from"path";import{chmod as j0,mkdir as A0,rename as C0,rm as E0,stat as I0}from"fs/promises";import{dirname as f0}from"path";async function K($){let X=Bun.file($);return await X.exists()?await X.text():null}async function D($){let X=await K($);if(X===null)return null;try{let Z=JSON.parse(X);return Z!==null&&typeof Z==="object"?Z:null}catch{throw Error(`${$} is not valid JSON`)}}async function k0($){try{return(await I0($)).mode&511}catch{return null}}async function V($,X,Z){await A0(f0($),{recursive:!0,mode:448});let G=`${$}.kyora-switch.${process.pid}.tmp`;try{await Bun.write(G,X),await j0(G,Z??await k0($)??384),await C0(G,$)}catch(H){throw await E0(G,{force:!0}),H}}function p($){let X=$.split(".")[1];if(!X)return null;try{let Z=Buffer.from(X.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8"),G=JSON.parse(Z);return G!==null&&typeof G==="object"?G:null}catch{return null}}function W($){return typeof $==="string"&&$.length>0?$:void 0}function A($){return $!==null&&typeof $==="object"?$:void 0}function $0($){let X=A($)??{},Z=A(X.tokens)??{},G=W(Z.id_token),H=G?p(G)??{}:{},Q=A(H["https://api.openai.com/auth"])??{},z=W(H.email)??W(Q.chatgpt_account_id)??W(Z.account_id),M=W(Q.chatgpt_plan_type)??(W(X.OPENAI_API_KEY)?"api key":void 0);return{account:z,plan:M}}function X0($){let X=A(A($)?.oauthAccount)??{};return{account:W(X.emailAddress)??W(X.accountUuid),org:W(X.organizationName),plan:W(X.seatTier)??W(X.billingType)}}import{userInfo as g0}from"os";var C="Claude Code-credentials",y0=4096;function E(){return g0().username}function I(){if(process.env.KYORA_SWITCH_NO_KEYCHAIN==="1")return!1;return process.platform==="darwin"}async function G0($=C){let X=await Bun.$`security find-generic-password -a ${E()} -w -s ${$}`.quiet().nothrow();if(X.exitCode!==0)return null;let Z=X.stdout.toString().trim();if(Z.length===0)return null;if(Z.length%2===0&&/^[0-9a-f]+$/.test(Z))return Buffer.from(Z,"hex").toString("utf8");return Z}async function H0($=C){let X=await Bun.$`security delete-generic-password -a ${E()} -s ${$}`.quiet().nothrow();if(X.exitCode!==0&&!/could not be found/i.test(X.stderr.toString()))throw Error(`keychain delete failed: ${X.stderr.toString().trim()||`exit ${X.exitCode}`}`)}async function Z0($,X){let Z=Bun.spawn($,{stdin:X===void 0?"ignore":new TextEncoder().encode(X),stdout:"ignore",stderr:"pipe"}),[G,H]=await Promise.all([Z.exited,new Response(Z.stderr).text()]);return{ok:G===0,stderr:H.trim()||`exit ${G}`}}async function Q0($,X=C){let Z=E();if(/["\\]/.test(Z)||/["\\]/.test(X))throw Error(`cannot quote keychain account "${Z}" or service "${X}"`);let G=Buffer.from($,"utf8").toString("hex"),H=`add-generic-password -U -a "${Z}" -s "${X}" -X ${G}
4
+ `;if(H.length<=y0&&(await Z0(["security","-i"],H)).ok)return;let Q=await Z0(["security","add-generic-password","-U","-a",Z,"-s",X,"-X",G]);if(!Q.ok)throw Error(`keychain write failed: ${Q.stderr}`)}var N="credentials.json",z0="account.json",P="oauthAccount",h0=()=>process.env.KYORA_CLAUDE_TOKEN_URL??"https://platform.claude.com/v1/oauth/token",m0=()=>process.env.KYORA_CLAUDE_CLIENT_ID??"9d1c250a-e61b-44d9-88ed-5944d1962f5e";function w0(){return process.env.CLAUDE_CONFIG_DIR??h(B0(),".claude")}function L(){let $=process.env.CLAUDE_CONFIG_DIR;return $?h($,".claude.json"):h(B0(),".claude.json")}function S(){return h(w0(),".credentials.json")}async function c0(){let $=await K(S());if($!==null)return $;return I()?await G0():null}async function v0($){if(await K(S())!==null){await V(S(),$,384);return}if(I()){await Q0($);return}await V(S(),$,384)}function J0($,X){let Z={...$};if(P in X)Z[P]=X[P];else delete Z[P];return Z}async function l0($){let X;try{X=JSON.parse($)}catch{return null}let Z=X.claudeAiOauth;if(!Z?.refreshToken)return null;let G=await fetch(h0(),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({grant_type:"refresh_token",refresh_token:Z.refreshToken,client_id:m0(),scope:(Z.scopes??[]).join(" ")}),signal:AbortSignal.timeout(30000)}).catch(()=>null);if(!G?.ok)return null;let H=await G.json().catch(()=>null);if(typeof H?.access_token!=="string")return null;let Q=Date.now(),z={...Z,accessToken:H.access_token,refreshToken:typeof H.refresh_token==="string"?H.refresh_token:Z.refreshToken,expiresAt:typeof H.expires_in==="number"?Q+H.expires_in*1000:Z.expiresAt,refreshTokenExpiresAt:typeof H.refresh_token_expires_in==="number"?Q+H.refresh_token_expires_in*1000:Z.refreshTokenExpiresAt,scopes:typeof H.scope==="string"?H.scope.split(" "):Z.scopes};return JSON.stringify({...X,claudeAiOauth:z})}function p0($){return $&&P in $?{[P]:$[P]}:{}}var q0={id:"claude",label:"Claude Code",processName:"claude",loginHint:"run `claude` and use /login",locations(){return[I()?`macOS keychain: ${C} (${E()})`:S(),L()]},async capture(){let $=await c0();if($===null)return null;let X=p0(await D(L())),Z={[N]:$,[z0]:`${JSON.stringify(X,null,2)}
5
+ `};return{provider:"claude",identity:X0(X),files:Z,capturedAt:Date.now()}},async forget(){if(await u0(S(),{force:!0}),I())await H0();let $=await D(L());if($)await V(L(),`${JSON.stringify(J0($,{}),null,2)}
6
+ `)},credentialExpiry($){try{let X=JSON.parse($.files[N]??"{}")?.claudeAiOauth?.expiresAt;return typeof X==="number"?X:void 0}catch{return}},async refresh($){let X=$.files[N];if(X===void 0)return null;let Z=await l0(X);if(Z===null)return null;return{...$,files:{...$.files,[N]:Z}}},async quota($){let X=r($.files[N]??"");return X?await o(X):null},async restore($){let X=$.files[N];if(X===void 0)throw Error("slot has no Claude credentials");await v0(X);let Z=JSON.parse($.files[z0]??"{}"),G=await D(L())??{};await V(L(),`${JSON.stringify(J0(G,Z),null,2)}
7
+ `)}};import{homedir as n0}from"os";import{join as V0}from"path";import{rm as d0}from"fs/promises";var k="auth.json";function s0(){return process.env.CODEX_HOME??V0(n0(),".codex")}function f(){return V0(s0(),k)}var W0={id:"codex",label:"Codex",processName:"codex",loginHint:"run `codex login`",locations(){return[f()]},async capture(){let $=await K(f());if($===null)return null;let X;try{X=JSON.parse($)}catch{throw Error(`${f()} is not valid JSON`)}return{provider:"codex",identity:$0(X),files:{[k]:$},capturedAt:Date.now()}},async forget(){await d0(f(),{force:!0})},credentialExpiry($){let X=l($.files[k]??"")?.token,Z=X?p(X)?.exp:void 0;return typeof Z==="number"?Z*1000:void 0},async quota($){let X=l($.files[k]??"");return X?await e(X.token,X.account):null},async restore($){let X=$.files[k];if(X===void 0)throw Error("slot has no Codex credentials");await V(f(),X,384)}};var x=[q0,W0];function n($){let X=x.find((Z)=>Z.id===$);if(!X)throw Error(`unknown provider "${$}"`);return X}async function m($){let X=await Bun.$`pgrep -x ${$.processName}`.quiet().nothrow();if(X.exitCode!==0)return 0;return X.stdout.toString().split(`
8
+ `).filter((Z)=>Z.trim().length>0).length}import{mkdir as M0,readdir as d,rename as a0,rm as s}from"fs/promises";import{homedir as t0}from"os";import{join as B}from"path";var Y0=/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/,g="slot.json",i0=10;function F(){return process.env.KYORA_SWITCH_DIR??B(t0(),".kyora","switch")}function Y($,X){return B(F(),$,R(X))}function R($){if(!Y0.test($))throw Error(`invalid slot name "${$}" \u2014 use letters, digits, dot, dash or underscore`);return $}async function T($){let X;try{X=await d(B(F(),$))}catch{return[]}let Z=[];for(let G of X.sort()){if(!Y0.test(G))continue;let H=await O($,G);if(H)Z.push(H)}return Z}async function O($,X){let Z=await D(B(Y($,X),g));if(!Z)return null;return{name:X,provider:$,identity:Z.identity??{},createdAt:typeof Z.createdAt==="number"?Z.createdAt:0,updatedAt:typeof Z.updatedAt==="number"?Z.updatedAt:0}}async function y($,X){let Z=await O($,X);if(!Z)return null;let G=Y($,X),H={};for(let Q of await d(G)){if(Q===g)continue;let z=await K(B(G,Q));if(z!==null)H[Q]=z}return{provider:$,identity:Z.identity,files:H,capturedAt:Z.updatedAt}}async function u($,X){let Z=Y(X.provider,$),G=await O(X.provider,$);await s(Z,{recursive:!0,force:!0}),await M0(Z,{recursive:!0,mode:448});for(let[Q,z]of Object.entries(X.files))await V(B(Z,Q),z,384);let H={name:$,provider:X.provider,identity:X.identity,createdAt:G?.createdAt||X.capturedAt,updatedAt:X.capturedAt};return await V(B(Z,g),`${JSON.stringify(H,null,2)}
9
+ `,384),H}async function K0($,X){await s(Y($,X),{recursive:!0,force:!0})}async function O0($,X,Z){await M0(B(F(),$),{recursive:!0,mode:448}),await a0(Y($,X),Y($,Z));let G=await O($,Z);if(G)await V(B(Y($,Z),g),`${JSON.stringify({...G,name:Z},null,2)}
10
+ `,384)}async function w($){let X=new Date().toISOString().replace(/[:.]/g,"-"),Z=B(F(),"backups",$.provider,X);for(let[G,H]of Object.entries($.files))await V(B(Z,G),H,384);return await V(B(Z,g),`${JSON.stringify($.identity,null,2)}
11
+ `,384),await r0($.provider),Z}async function r0($){let X=B(F(),"backups",$),Z;try{Z=await d(X)}catch{return}for(let G of Z.sort().slice(0,Math.max(0,Z.length-i0)))await s(B(X,G),{recursive:!0,force:!0})}function o0($,X){let Z=Object.keys($.files).sort();if(Z.join(`
12
+ `)!==Object.keys(X.files).sort().join(`
13
+ `))return!1;return Z.every((G)=>$.files[G]===X.files[G])}async function b($){let X=await $.capture(),Z=await T($.id),G=X?Z.find((Q)=>Q.identity.account===X.identity.account):void 0;if(!X||!G)return{active:X,slots:Z,synced:null};let H=await y($.id,G.name);if(H&&o0(H,X))return{active:X,slots:Z,synced:null};return await u(G.name,X),Z=await T($.id),{active:X,slots:Z,synced:G.name}}var e0=["claude","codex"];function U0($){return e0.includes($??"")}function q($){let X=[$.account??"unknown account"];if($.org)X.push($.org);if($.plan)X.push($.plan);return X.join(" \xB7 ")}var P0=`kyora-switch \u2014 hot-swap Claude Code and Codex logins
14
+
15
+ usage:
16
+ kyora-switch claude save <slot> store the Claude account you are logged into
17
+ kyora-switch claude load <slot> log Claude Code back into a stored account
18
+ kyora-switch claude list slots for Claude Code
19
+ kyora-switch claude usage how much quota each stored account has left
20
+ kyora-switch claude refresh renew the tokens of stale slots, then show usage
21
+ kyora-switch claude sync copy the live login back into its slot
22
+ kyora-switch claude clear sign out locally, without revoking the account
23
+ kyora-switch claude rm <slot> delete a slot
24
+ kyora-switch claude rename <a> <b> rename a slot
25
+
26
+ kyora-switch codex save <slot> same commands for Codex
27
+ kyora-switch codex load <slot>
28
+
29
+ kyora-switch status which account each CLI is logged into
30
+ kyora-switch usage remaining quota across every stored account
31
+ kyora-switch refresh renew stale slot tokens, then show usage
32
+ kyora-switch sync copy each live login back into its slot
33
+ kyora-switch doctor where each CLI keeps its auth on this machine
34
+
35
+ Every read (status, list, usage) and every load syncs the live login into its
36
+ slot first, so the CLI's own token refreshes never leave a slot behind.
37
+
38
+ options:
39
+ --json machine-readable output (list, status, usage)
40
+ --refresh renew a stale slot's token before reading its quota
41
+ -y, --yes skip the confirmation on rm
42
+ -h, --help this help
43
+
44
+ Slots live in ~/.kyora/switch/<provider> (override with KYORA_SWITCH_DIR). Every
45
+ load backs the outgoing login up first, so a switch made before saving is still
46
+ recoverable.`;function J($){console.error(`[kyora-switch] ${$}`),process.exit(2)}var j=($)=>console.error(`[kyora-switch] ${$}`),_=($)=>$.padEnd(7);async function X1($){process.stdout.write($);for await(let X of console)return X.trim();return""}async function Z1($,X){let Z=await $.capture();if(!Z)J(`${$.label} is not logged in \u2014 ${$.loginHint}`);await u(R(X),Z),console.log(`saved ${$.id} "${X}" \u2014 ${q(Z.identity)}`),console.log(` ${Y($.id,X)}`)}async function G1($,X){let Z=await y($.id,R(X))??J(`no ${$.id} slot "${X}"`),{active:G,slots:H,synced:Q}=await b($);if(G){let z=await w(G);if(Q)j(`synced ${$.id} "${Q}" from the live login before switching away`);else if(!H.some((M)=>M.identity.account===G.identity.account))j(`the login you just replaced (${q(G.identity)}) was in no slot \u2014 it is in ${z}`)}if(await $.restore(Z),console.log(`${$.id} \u2192 "${X}" \u2014 ${q(Z.identity)}`),await m($)>0)j(`${$.label} is running \u2014 restart it, the old token is already in memory`)}async function H1($,X){let{active:Z,slots:G}=await b($);if(X)return console.log(JSON.stringify(G,null,2));if(G.length===0)return console.log(`no ${$.id} slots yet \u2014 run \`kyora-switch ${$.id} save <slot>\` while logged in`);for(let H of G){let Q=Z&&H.identity.account===Z.identity.account?" (active)":"";console.log(`${_(H.name)} ${q(H.identity)}${Q}`)}}function x0($){let X=Math.max(1,Math.round($/60000));if(X<60)return`${X}m`;let Z=Math.floor(X/60);if(Z<24)return X%60===0?`${Z}h`:`${Z}h ${X%60}m`;return`${Math.round(Z/24)}d`}function Q1($){let X=$-Date.now();return X<=0?"now":x0(X)}function z1($){let X=$.windows??[];if(X.length===0)return`${`${$.remainingPct}% left`.padEnd(9)} ${$.detail}`;let Z=X.map((G)=>`${G.label} ${G.usedPct}% used${G.resetsAt?`, resets in ${Q1(G.resetsAt)}`:""}`).join(" \xB7 ");return`${`${$.remainingPct}% left`.padEnd(9)} ${Z}`}async function J1($,X,Z){let G=$.credentialExpiry?.(Z);if(!$.refresh||G===void 0||G>Date.now())return Z;let H=await $.refresh(Z);if(!H)return j(`${$.id} "${X}": refresh was refused, its stored token is spent \u2014 load the slot and log in again`),Z;return await w(H),await u(X,H),H}async function B1($,X){let{active:Z,slots:G}=await b($),H=[];for(let Q of G){let z=await y($.id,Q.name),M=Boolean(Z&&Z.identity.account===Q.identity.account),U=M?Z:z;if(!M&&X&&U)U=await J1($,Q.name,U);H.push({provider:$.id,slot:Q.name,account:q(Q.identity),live:M,quota:U&&$.quota?await $.quota(U):null,expiresAt:U?$.credentialExpiry?.(U):void 0})}if(Z&&!H.some((Q)=>Q.live))H.unshift({provider:$.id,slot:null,account:q(Z.identity),live:!0,quota:$.quota?await $.quota(Z):null,expiresAt:$.credentialExpiry?.(Z),note:"not saved to a slot"});return H}function q1($,X){if($.quotaHint)return $.quotaHint;if(X.expiresAt!==void 0&&X.expiresAt<=Date.now())return`stored access token expired ${x0(Date.now()-X.expiresAt)} ago \u2014 load this slot to refresh it`;return"no quota reported"}function V1($,X){if(X.length===0){console.log(`${$.id}: nothing logged in and no slots saved`);return}let Z=Math.max(...X.map((G)=>(G.slot??"live").length));for(let G of X){let H=(G.slot??"live").padEnd(Z),Q=G.live?"*":" ",z=G.quota?z1(G.quota):q1($,G);if(console.log(`${Q} ${H} ${G.account}`),console.log(` ${" ".repeat(Z)} ${z}`),G.note)console.log(` ${" ".repeat(Z)} ${G.note}`)}}async function c($,X,Z){let G=await Promise.all($.map(async(H)=>[H,await B1(H,Z)]));if(X)return console.log(JSON.stringify(Object.fromEntries(G.map(([H,Q])=>[H.id,Q])),null,2));G.forEach(([H,Q],z)=>{if(z>0)console.log();if($.length>1)console.log(`${H.id} \u2014 ${H.label}`);V1(H,Q)})}async function W1($){let X=await $.capture();if(!X){console.log(`${$.id} is already signed out \u2014 ${$.loginHint}`);return}let Z=await w(X);if(!(await T($.id)).some((H)=>H.identity.account===X.identity.account))j(`${q(X.identity)} was in no slot \u2014 it is in ${Z}`);if(await $.forget(),console.log(`${$.id} signed out locally \u2014 ${q(X.identity)}`),console.log(` the account keeps its session, so a saved slot still loads; ${$.loginHint} for another one`),await m($)>0)j(`${$.label} is running \u2014 restart it, the old token is already in memory`)}async function M1($,X,Z){if(!await O($.id,R(X)))J(`no ${$.id} slot "${X}"`);if(!Z){if(!process.stdin.isTTY)J(`refusing to delete ${$.id} "${X}" without --yes`);if((await X1(`delete ${$.id} slot "${X}"? [y/N] `)).toLowerCase()!=="y")return console.log("cancelled")}await K0($.id,X),console.log(`deleted ${$.id} "${X}"`)}async function Y1($,X,Z){if(!await O($.id,R(X)))J(`no ${$.id} slot "${X}"`);if(await O($.id,R(Z)))J(`${$.id} slot "${Z}" already exists`);await O0($.id,X,Z),console.log(`renamed ${$.id} "${X}" to "${Z}"`)}async function F0($){for(let X of $){let{active:Z,slots:G,synced:H}=await b(X);if(!Z)console.log(`${_(X.id)} not logged in \u2014 nothing to sync`);else if(H)console.log(`${_(X.id)} synced "${H}" \u2014 ${q(Z.identity)}`);else if(G.some((Q)=>Q.identity.account===Z.identity.account))console.log(`${_(X.id)} already current \u2014 ${q(Z.identity)}`);else console.log(`${_(X.id)} ${q(Z.identity)} is in no slot \u2014 save it first`)}}async function K1($){let X=[];for(let Z of x){let{active:G,slots:H}=await b(Z),Q=H.find((z)=>G&&z.identity.account===G.identity.account);X.push({provider:Z.id,identity:G?.identity??null,slot:Q?.name??null})}if($)return console.log(JSON.stringify(X,null,2));for(let Z of X){let G=n(Z.provider);if(!Z.identity){console.log(`${_(Z.provider)} not logged in \u2014 ${G.loginHint}`);continue}console.log(`${_(Z.provider)} ${q(Z.identity)} (${Z.slot?`slot "${Z.slot}"`:"no slot"})`)}}async function O1(){console.log(`slots: ${F()}`);for(let $ of x){console.log(`
47
+ ${$.id} \u2014 ${$.label}`);for(let G of $.locations())console.log(` ${G}`);let X=await $.capture().catch((G)=>G);if(X instanceof Error)console.log(` status: unreadable \u2014 ${X.message}`);else if(!X)console.log(` status: not logged in \u2014 ${$.loginHint}`);else console.log(` status: ${q(X.identity)}`);let Z=await m($);console.log(` running: ${Z>0?`yes (${Z} process${Z===1?"":"es"})`:"no"}`),console.log(` slots: ${(await T($.id)).length}`)}}async function U1($,X,Z,G,H){let[Q="list",z,M]=X;switch(Q){case"save":return await Z1($,z??J("save needs a slot name"));case"load":case"use":return await G1($,z??J("load needs a slot name"));case"list":case"ls":return await H1($,Z);case"usage":return await c([$],Z,H);case"refresh":return await c([$],Z,!0);case"sync":return await F0([$]);case"rm":case"remove":case"delete":return await M1($,z??J("rm needs a slot name"),G);case"rename":return await Y1($,z??J("rename needs the current name"),M??J("rename needs the new name"));case"clear":case"signout":return await W1($);default:J(`unknown ${$.id} command "${Q}" \u2014 run \`kyora-switch --help\``)}}async function P1(){let $;try{$=$1({args:Bun.argv.slice(2),options:{json:{type:"boolean"},refresh:{type:"boolean"},yes:{type:"boolean",short:"y"},help:{type:"boolean",short:"h"}},allowPositionals:!0,strict:!0})}catch(Q){J(Q.message)}let{values:X,positionals:Z}=$;if(X.help)return console.log(P0);let[G="status",...H]=Z;if(U0(G))return await U1(n(G),H,Boolean(X.json),Boolean(X.yes),Boolean(X.refresh));switch(G){case"status":return await K1(Boolean(X.json));case"usage":return await c(x,Boolean(X.json),Boolean(X.refresh));case"refresh":return await c(x,Boolean(X.json),!0);case"sync":return await F0(x);case"doctor":return await O1();case"help":return console.log(P0);default:J(`unknown command "${G}" \u2014 run \`kyora-switch --help\``)}}P1().catch(($)=>J($.message));
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "kyora-switch",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Hot-swap Claude Code and Codex logins between accounts, with live quota for every stored account",
6
+ "keywords": [
7
+ "claude-code",
8
+ "codex",
9
+ "cli",
10
+ "account-switcher",
11
+ "oauth",
12
+ "keychain"
13
+ ],
14
+ "license": "Apache-2.0",
15
+ "author": "Elia Hilse",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/eliahilse/kyora",
19
+ "directory": "packages/switch/cli"
20
+ },
21
+ "homepage": "https://kyora.sh",
22
+ "bugs": "https://github.com/eliahilse/kyora/issues",
23
+ "main": "./dist/index.js",
24
+ "bin": {
25
+ "kyora-switch": "./dist/index.js"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE",
31
+ "NOTICE"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "engines": {
37
+ "bun": ">=1.2.0"
38
+ },
39
+ "scripts": {
40
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun --minify && chmod +x ./dist/index.js",
41
+ "check-types": "tsc --noEmit",
42
+ "test": "bun test",
43
+ "dev": "bun --watch src/index.ts",
44
+ "switch": "bun src/index.ts",
45
+ "prepublishOnly": "bun run build"
46
+ },
47
+ "devDependencies": {
48
+ "@kyora-sh/usage": "0.1.0",
49
+ "@repo/typescript-config": "0.0.0",
50
+ "@types/bun": "latest",
51
+ "typescript": "^5.9.2"
52
+ }
53
+ }