claude-profile-switch 1.0.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 +21 -0
- package/README.md +152 -0
- package/claude-switch.js +618 -0
- package/package.json +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ashutosh Adhao
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# claude-switch
|
|
2
|
+
|
|
3
|
+
> npm package: **`claude-profile-switch`** · command: **`claude-switch`**
|
|
4
|
+
|
|
5
|
+
Switch between multiple saved **Claude Code** accounts/sessions from one terminal —
|
|
6
|
+
macOS, Linux, and Windows. Zero dependencies (just Node, which Claude Code already needs).
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
Claude Code keeps a single active session. If you use more than one account
|
|
11
|
+
(personal / team / client), you'd normally have to re-`/login` every time you
|
|
12
|
+
switch. `claude-switch` snapshots a session into a named profile and swaps it
|
|
13
|
+
back in instantly.
|
|
14
|
+
|
|
15
|
+
## What it saves
|
|
16
|
+
|
|
17
|
+
A profile snapshots **both** halves of an account so the switch is clean:
|
|
18
|
+
|
|
19
|
+
| Piece | Location (Linux/Windows) | Location (macOS) |
|
|
20
|
+
|-------|--------------------------|------------------|
|
|
21
|
+
| Session token | `~/.claude/.credentials.json` | login Keychain → service `Claude Code-credentials` |
|
|
22
|
+
| Account identity (email, account/org UUID) | `~/.claude.json` → `oauthAccount` | same |
|
|
23
|
+
|
|
24
|
+
Profiles live in `~/.claude-switch/profiles/<name>/` with `0600` permissions.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
From npm (once published):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g claude-profile-switch
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or from a local clone of this directory:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install -g .
|
|
38
|
+
# or, for a live-editable dev install:
|
|
39
|
+
npm link
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Either way you get the `claude-switch` command on your PATH on all three OSes.
|
|
43
|
+
Verify with `claude-switch help`. (Requires Node ≥ 16.)
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
claude-switch save <profile> # save the current session as <profile>
|
|
49
|
+
claude-switch update [profile] # re-sync a profile from the live session (default: active)
|
|
50
|
+
claude-switch use <profile> # switch to a saved profile
|
|
51
|
+
claude-switch list # list profiles; ● marks the active one
|
|
52
|
+
claude-switch list --usage # ...also show live quota for the active session
|
|
53
|
+
claude-switch usage # detailed usage limits for the current session
|
|
54
|
+
claude-switch delete <profile> # remove a saved profile (live login untouched)
|
|
55
|
+
claude-switch whoami # show current account + profile + token status
|
|
56
|
+
claude-switch help
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage limits (`usage` / `list --usage`)
|
|
60
|
+
|
|
61
|
+
Two different "time left" numbers exist — don't confuse them:
|
|
62
|
+
|
|
63
|
+
- **Token expiry** — when the OAuth access token needs refreshing. Shown by
|
|
64
|
+
`list`/`whoami`, read locally from the credentials (free, instant).
|
|
65
|
+
- **Usage window** — your plan's 5-hour and weekly quotas, and when they reset.
|
|
66
|
+
This is **server-side only** (not stored on disk), so it requires one tiny API
|
|
67
|
+
call (Haiku, `max_tokens:1` — negligible quota).
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
$ claude-switch usage
|
|
71
|
+
Current session: you@example.com
|
|
72
|
+
Token: valid, 6h 6m left
|
|
73
|
+
Usage limits:
|
|
74
|
+
5-hour 40% left (60% used) resets in 2h 13m (6/22/2026, 8:30:00 PM)
|
|
75
|
+
weekly 93% left (7% used) resets in 3d 2h (6/25/2026, 8:30:00 PM)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Plain `list` stays offline/free; add `--usage` only when you want live quota, so
|
|
79
|
+
listing never silently spends quota. Usage is fetched for the **active** session
|
|
80
|
+
only (its token is the current one).
|
|
81
|
+
|
|
82
|
+
### Typical flow
|
|
83
|
+
|
|
84
|
+
> **Logging in:** there is no `claude login` command. Start Claude Code with
|
|
85
|
+
> `claude`, then type the `/login` slash command inside the session, complete the
|
|
86
|
+
> browser flow, and `/exit`. (`claude auth` is the CLI entry point for managing
|
|
87
|
+
> authentication if you prefer staying in the shell.)
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Log in to account A: run `claude`, type `/login`, then `/exit`. Snapshot it:
|
|
91
|
+
claude-switch save work
|
|
92
|
+
|
|
93
|
+
# Log in to account B (run `claude` → `/login` → `/exit`), then snapshot it:
|
|
94
|
+
claude-switch save personal
|
|
95
|
+
|
|
96
|
+
# Jump between them
|
|
97
|
+
claude-switch use work
|
|
98
|
+
claude-switch use personal
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Refreshing a token
|
|
102
|
+
|
|
103
|
+
Tokens rotate / eventually expire. To refresh the one stored in a profile,
|
|
104
|
+
re-authenticate inside Claude Code first, then re-sync:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
claude # then type `/login`, complete the flow, and `/exit`
|
|
108
|
+
claude-switch update # re-sync the active profile from that live session
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`update` with no argument targets whichever profile matches the current account,
|
|
112
|
+
so you don't have to remember its name. Pass a name (`claude-switch update work`)
|
|
113
|
+
to update a specific existing profile. (`save <name>` also overwrites — `update`
|
|
114
|
+
is just the convenient "same account, refresh it" path.)
|
|
115
|
+
|
|
116
|
+
## Safety
|
|
117
|
+
|
|
118
|
+
- `save`, `list`, `whoami` are **read-only** — they never modify your live login.
|
|
119
|
+
- `delete` removes only the saved *copy*; your live session is untouched.
|
|
120
|
+
- `use` is the only command that writes the live token, and it **backs up** the
|
|
121
|
+
current one to `~/.claude-switch/backups/credentials.last.json` first.
|
|
122
|
+
|
|
123
|
+
## Verification after switching
|
|
124
|
+
|
|
125
|
+
After `use`, the tool:
|
|
126
|
+
|
|
127
|
+
1. Checks the token's `expiresAt` locally (instant, offline). If expired, it tells
|
|
128
|
+
you to re-`/login` and re-`save`.
|
|
129
|
+
2. Runs a quick live `claude -p` call to confirm the session actually works.
|
|
130
|
+
Skip it with `--no-check`:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
claude-switch use work --no-check
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
If a token is expired:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
⚠ This session's token expired 2h ago.
|
|
140
|
+
⚠ Run `/login` inside `claude` to refresh it, then `claude-switch save work` to update this profile.
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Notes
|
|
144
|
+
|
|
145
|
+
- **Token refresh:** Claude Code rotates tokens periodically. If a profile's token
|
|
146
|
+
has aged out, run `claude` → `/login` on that account, then `claude-switch update`
|
|
147
|
+
(or `save <name>`) to refresh the snapshot.
|
|
148
|
+
- **macOS Keychain:** writing the credential may prompt for Keychain access the
|
|
149
|
+
first time — that's expected.
|
|
150
|
+
- Uninstall with `npm uninstall -g claude-switch`. Remove data with
|
|
151
|
+
`rm -rf ~/.claude-switch`.
|
|
152
|
+
```
|
package/claude-switch.js
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* claude-switch — switch between saved Claude Code accounts/sessions.
|
|
6
|
+
*
|
|
7
|
+
* Cross-platform (macOS / Linux / Windows). Zero dependencies.
|
|
8
|
+
*
|
|
9
|
+
* Where Claude Code keeps things:
|
|
10
|
+
* - Session token : ~/.claude/.credentials.json (macOS: login Keychain, service "Claude Code-credentials")
|
|
11
|
+
* - Account info : ~/.claude.json -> oauthAccount { emailAddress, accountUuid, organizationUuid, ... }
|
|
12
|
+
*
|
|
13
|
+
* A "profile" snapshots BOTH so an account swap is clean (token + identity stay in sync).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const https = require('https');
|
|
20
|
+
const { spawnSync } = require('child_process');
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Paths & constants
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const HOME = os.homedir();
|
|
27
|
+
const IS_MAC = process.platform === 'darwin';
|
|
28
|
+
const IS_WIN = process.platform === 'win32';
|
|
29
|
+
|
|
30
|
+
const CLAUDE_DIR = path.join(HOME, '.claude');
|
|
31
|
+
const CREDS_FILE = path.join(CLAUDE_DIR, '.credentials.json');
|
|
32
|
+
const CLAUDE_JSON = path.join(HOME, '.claude.json');
|
|
33
|
+
|
|
34
|
+
const KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
35
|
+
const KEYCHAIN_ACCOUNT = (os.userInfo().username || 'claude');
|
|
36
|
+
|
|
37
|
+
const STORE_DIR = path.join(HOME, '.claude-switch');
|
|
38
|
+
const PROFILES_DIR = path.join(STORE_DIR, 'profiles');
|
|
39
|
+
const BACKUP_DIR = path.join(STORE_DIR, 'backups');
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Tiny output helpers
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
46
|
+
const c = (code, s) => (useColor ? `[${code}m${s}[0m` : s);
|
|
47
|
+
const bold = (s) => c('1', s);
|
|
48
|
+
const green = (s) => c('32', s);
|
|
49
|
+
const yellow = (s) => c('33', s);
|
|
50
|
+
const red = (s) => c('31', s);
|
|
51
|
+
const dim = (s) => c('2', s);
|
|
52
|
+
const cyan = (s) => c('36', s);
|
|
53
|
+
|
|
54
|
+
function die(msg) {
|
|
55
|
+
console.error(red('✖ ') + msg);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
function info(msg) { console.log(msg); }
|
|
59
|
+
|
|
60
|
+
/** Transient status line on a TTY; no-op when output is piped. Returns a clear fn. */
|
|
61
|
+
function spinner(text) {
|
|
62
|
+
if (!process.stdout.isTTY) return () => {};
|
|
63
|
+
process.stdout.write(dim(text));
|
|
64
|
+
return () => process.stdout.write('\r' + ' '.repeat(text.length) + '\r');
|
|
65
|
+
}
|
|
66
|
+
function ok(msg) { console.log(green('✔ ') + msg); }
|
|
67
|
+
function warn(msg) { console.log(yellow('⚠ ') + msg); }
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Profile-name validation (it becomes a directory name)
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
function validName(name) {
|
|
74
|
+
if (!name) return false;
|
|
75
|
+
return /^[A-Za-z0-9._@-]+$/.test(name) && name !== '.' && name !== '..';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Credential read/write (abstracts macOS Keychain vs. plain file)
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/** Returns the raw credentials JSON string, or null if none present. */
|
|
83
|
+
function readLiveCredentials() {
|
|
84
|
+
if (IS_MAC) {
|
|
85
|
+
const r = spawnSync('security', [
|
|
86
|
+
'find-generic-password', '-s', KEYCHAIN_SERVICE, '-a', KEYCHAIN_ACCOUNT, '-w',
|
|
87
|
+
], { encoding: 'utf8' });
|
|
88
|
+
if (r.status === 0 && r.stdout) return r.stdout.replace(/\n$/, '');
|
|
89
|
+
// Fall through to file in case this machine uses a file even on macOS.
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return fs.readFileSync(CREDS_FILE, 'utf8');
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Writes the raw credentials JSON string back to wherever Claude Code reads it. */
|
|
99
|
+
function writeLiveCredentials(raw) {
|
|
100
|
+
if (IS_MAC) {
|
|
101
|
+
// Update (or create) the keychain item. -U overwrites an existing item.
|
|
102
|
+
const r = spawnSync('security', [
|
|
103
|
+
'add-generic-password', '-U',
|
|
104
|
+
'-s', KEYCHAIN_SERVICE, '-a', KEYCHAIN_ACCOUNT,
|
|
105
|
+
'-w', raw,
|
|
106
|
+
], { encoding: 'utf8' });
|
|
107
|
+
if (r.status !== 0) {
|
|
108
|
+
die('Failed to write to macOS Keychain: ' + (r.stderr || r.error || 'unknown error'));
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
113
|
+
fs.writeFileSync(CREDS_FILE, raw, { mode: 0o600 });
|
|
114
|
+
try { fs.chmodSync(CREDS_FILE, 0o600); } catch { /* best effort on Windows */ }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// ~/.claude.json account block (read + patch in place, preserving everything)
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
function readClaudeJson() {
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(fs.readFileSync(CLAUDE_JSON, 'utf8'));
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Extract the identity bits we need to keep in sync with the token. */
|
|
130
|
+
function extractAccount(claudeJson) {
|
|
131
|
+
if (!claudeJson) return null;
|
|
132
|
+
return {
|
|
133
|
+
oauthAccount: claudeJson.oauthAccount || null,
|
|
134
|
+
userID: claudeJson.userID || null,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Merge a saved account block back into ~/.claude.json without clobbering other state. */
|
|
139
|
+
function patchClaudeJson(account) {
|
|
140
|
+
if (!account) return;
|
|
141
|
+
let data = readClaudeJson();
|
|
142
|
+
if (!data) {
|
|
143
|
+
// ~/.claude.json may legitimately not exist yet; create a minimal one.
|
|
144
|
+
data = {};
|
|
145
|
+
}
|
|
146
|
+
if (account.oauthAccount) data.oauthAccount = account.oauthAccount;
|
|
147
|
+
if (account.userID) data.userID = account.userID;
|
|
148
|
+
fs.mkdirSync(path.dirname(CLAUDE_JSON), { recursive: true });
|
|
149
|
+
fs.writeFileSync(CLAUDE_JSON, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
150
|
+
try { fs.chmodSync(CLAUDE_JSON, 0o600); } catch { /* best effort */ }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Credential parsing helpers
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
function parseOauth(rawCreds) {
|
|
158
|
+
try {
|
|
159
|
+
const j = JSON.parse(rawCreds);
|
|
160
|
+
return j.claudeAiOauth || j; // tolerate either shape
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function expiryInfo(oauth) {
|
|
167
|
+
const exp = oauth && oauth.expiresAt;
|
|
168
|
+
if (!exp) return { known: false };
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
return {
|
|
171
|
+
known: true,
|
|
172
|
+
expired: exp <= now,
|
|
173
|
+
at: new Date(exp),
|
|
174
|
+
msLeft: exp - now,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function humanDuration(ms) {
|
|
179
|
+
const abs = Math.abs(ms);
|
|
180
|
+
const d = Math.floor(abs / 86400000);
|
|
181
|
+
const h = Math.floor((abs % 86400000) / 3600000);
|
|
182
|
+
const m = Math.floor((abs % 3600000) / 60000);
|
|
183
|
+
if (d > 0) return `${d}d ${h}h`;
|
|
184
|
+
if (h > 0) return `${h}h ${m}m`;
|
|
185
|
+
return `${m}m`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Profile storage
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
function profilePath(name) { return path.join(PROFILES_DIR, name); }
|
|
193
|
+
|
|
194
|
+
function listProfiles() {
|
|
195
|
+
try {
|
|
196
|
+
return fs.readdirSync(PROFILES_DIR, { withFileTypes: true })
|
|
197
|
+
.filter((e) => e.isDirectory())
|
|
198
|
+
.map((e) => e.name)
|
|
199
|
+
.sort();
|
|
200
|
+
} catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function readProfile(name) {
|
|
206
|
+
const dir = profilePath(name);
|
|
207
|
+
const out = { name, dir };
|
|
208
|
+
try { out.credentials = fs.readFileSync(path.join(dir, 'credentials.json'), 'utf8'); } catch { out.credentials = null; }
|
|
209
|
+
try { out.account = JSON.parse(fs.readFileSync(path.join(dir, 'account.json'), 'utf8')); } catch { out.account = null; }
|
|
210
|
+
try { out.meta = JSON.parse(fs.readFileSync(path.join(dir, 'meta.json'), 'utf8')); } catch { out.meta = {}; }
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The account currently live on this machine (uuid + email), from ~/.claude.json. */
|
|
215
|
+
function liveAccountIdentity() {
|
|
216
|
+
const cj = readClaudeJson();
|
|
217
|
+
const oa = cj && cj.oauthAccount;
|
|
218
|
+
return {
|
|
219
|
+
uuid: oa && oa.accountUuid,
|
|
220
|
+
email: oa && oa.emailAddress,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Which saved profile (if any) matches the live account? Matched by stable accountUuid. */
|
|
225
|
+
function activeProfileName() {
|
|
226
|
+
const live = liveAccountIdentity();
|
|
227
|
+
if (!live.uuid) return null;
|
|
228
|
+
for (const name of listProfiles()) {
|
|
229
|
+
const p = readProfile(name);
|
|
230
|
+
const uuid = p.account && p.account.oauthAccount && p.account.oauthAccount.accountUuid;
|
|
231
|
+
if (uuid && uuid === live.uuid) return name;
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Live usage / rate-limit windows
|
|
238
|
+
//
|
|
239
|
+
// Usage left is NOT stored on disk — it's server-side, returned as
|
|
240
|
+
// `anthropic-ratelimit-unified-*` response headers. We make a single tiny
|
|
241
|
+
// request (Haiku, max_tokens:1 — negligible quota) and read those headers.
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
function fetchLiveUsage(accessToken) {
|
|
245
|
+
return new Promise((resolve) => {
|
|
246
|
+
if (!accessToken) { resolve({ ok: false, reason: 'nocreds' }); return; }
|
|
247
|
+
const body = JSON.stringify({
|
|
248
|
+
model: 'claude-haiku-4-5-20251001',
|
|
249
|
+
max_tokens: 1,
|
|
250
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
251
|
+
});
|
|
252
|
+
const req = https.request({
|
|
253
|
+
hostname: 'api.anthropic.com',
|
|
254
|
+
path: '/v1/messages',
|
|
255
|
+
method: 'POST',
|
|
256
|
+
timeout: 15000,
|
|
257
|
+
headers: {
|
|
258
|
+
'authorization': 'Bearer ' + accessToken,
|
|
259
|
+
'anthropic-version': '2023-06-01',
|
|
260
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
261
|
+
'content-type': 'application/json',
|
|
262
|
+
'content-length': Buffer.byteLength(body),
|
|
263
|
+
},
|
|
264
|
+
}, (res) => {
|
|
265
|
+
res.resume(); // drain body; we only want headers
|
|
266
|
+
if (res.statusCode === 401 || res.statusCode === 403) { resolve({ ok: false, reason: 'auth' }); return; }
|
|
267
|
+
const parsed = parseUsageHeaders(res.headers);
|
|
268
|
+
if (!parsed) { resolve({ ok: false, reason: 'noheaders', httpStatus: res.statusCode }); return; }
|
|
269
|
+
resolve({ ok: true, ...parsed });
|
|
270
|
+
});
|
|
271
|
+
req.on('timeout', () => { req.destroy(); resolve({ ok: false, reason: 'timeout' }); });
|
|
272
|
+
req.on('error', (e) => resolve({ ok: false, reason: 'network', error: e.message }));
|
|
273
|
+
req.write(body);
|
|
274
|
+
req.end();
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function parseUsageHeaders(h) {
|
|
279
|
+
const win = (prefix, label) => {
|
|
280
|
+
const u = h['anthropic-ratelimit-unified-' + prefix + '-utilization'];
|
|
281
|
+
if (u === undefined) return null;
|
|
282
|
+
const r = h['anthropic-ratelimit-unified-' + prefix + '-reset'];
|
|
283
|
+
const used = parseFloat(u);
|
|
284
|
+
return {
|
|
285
|
+
label,
|
|
286
|
+
usedPct: Math.round(used * 100),
|
|
287
|
+
leftPct: Math.max(0, Math.round((1 - used) * 100)),
|
|
288
|
+
resetAt: r ? new Date(parseInt(r, 10) * 1000) : null,
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
const windows = [win('5h', '5-hour'), win('7d', 'weekly')].filter(Boolean);
|
|
292
|
+
if (!windows.length) return null;
|
|
293
|
+
return { overall: h['anthropic-ratelimit-unified-status'] || null, windows };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function usageColor(leftPct) {
|
|
297
|
+
return leftPct <= 10 ? red : leftPct <= 30 ? yellow : green;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** One-line summary per window, e.g. "43% left (5-hour, resets in 2h 10m)". */
|
|
301
|
+
function renderUsageInline(u) {
|
|
302
|
+
if (!u.ok) {
|
|
303
|
+
if (u.reason === 'auth') return yellow('session invalid/expired — run ') + cyan('claude') + cyan(' → /login');
|
|
304
|
+
if (u.reason === 'timeout') return dim('usage check timed out');
|
|
305
|
+
if (u.reason === 'network') return dim('offline');
|
|
306
|
+
if (u.reason === 'nocreds') return dim('no token');
|
|
307
|
+
return dim('usage unavailable');
|
|
308
|
+
}
|
|
309
|
+
return u.windows.map((w) => {
|
|
310
|
+
const resets = w.resetAt ? `, resets in ${humanDuration(w.resetAt.getTime() - Date.now())}` : '';
|
|
311
|
+
return usageColor(w.leftPct)(`${w.leftPct}% left`) + dim(` (${w.label}${resets})`);
|
|
312
|
+
}).join(' · ');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// Commands
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
/** Write the live session (token + account identity) into profile <name>. */
|
|
320
|
+
function persistProfile(name) {
|
|
321
|
+
const raw = readLiveCredentials();
|
|
322
|
+
if (!raw) {
|
|
323
|
+
die(`No active Claude Code session found.\n Looked in ${IS_MAC ? 'Keychain "' + KEYCHAIN_SERVICE + '"' : CREDS_FILE}.\n Run ${cyan('claude') + ' → ' + cyan('/login')} first.`);
|
|
324
|
+
}
|
|
325
|
+
const oauth = parseOauth(raw);
|
|
326
|
+
const account = extractAccount(readClaudeJson());
|
|
327
|
+
const email = account && account.oauthAccount && account.oauthAccount.emailAddress;
|
|
328
|
+
|
|
329
|
+
const dir = profilePath(name);
|
|
330
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
331
|
+
fs.writeFileSync(path.join(dir, 'credentials.json'), raw, { mode: 0o600 });
|
|
332
|
+
fs.writeFileSync(path.join(dir, 'account.json'), JSON.stringify(account, null, 2), { mode: 0o600 });
|
|
333
|
+
|
|
334
|
+
const meta = {
|
|
335
|
+
name,
|
|
336
|
+
email: email || null,
|
|
337
|
+
subscriptionType: oauth && oauth.subscriptionType || null,
|
|
338
|
+
expiresAt: oauth && oauth.expiresAt || null,
|
|
339
|
+
savedAt: Date.now(),
|
|
340
|
+
platform: process.platform,
|
|
341
|
+
};
|
|
342
|
+
fs.writeFileSync(path.join(dir, 'meta.json'), JSON.stringify(meta, null, 2), { mode: 0o600 });
|
|
343
|
+
|
|
344
|
+
return { email, exp: expiryInfo(oauth) };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function cmdSave(name) {
|
|
348
|
+
if (!validName(name)) die(`Invalid profile name. Use letters, digits, and . _ @ -`);
|
|
349
|
+
if (fs.existsSync(profilePath(name))) warn(`Overwriting existing profile "${name}".`);
|
|
350
|
+
|
|
351
|
+
const { email, exp } = persistProfile(name);
|
|
352
|
+
ok(`Saved profile ${bold(name)}${email ? dim(' (' + email + ')') : ''}.`);
|
|
353
|
+
if (exp.known && exp.expired) {
|
|
354
|
+
warn(`Heads up: this token is already expired. Run ${cyan('claude') + ' → ' + cyan('/login')} and re-save.`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Re-sync a profile from the current live session — use after re-authenticating (`claude` → `/login`)
|
|
360
|
+
* refreshes the token. With no argument, updates whichever profile is active.
|
|
361
|
+
*/
|
|
362
|
+
function cmdUpdate(name) {
|
|
363
|
+
if (name) {
|
|
364
|
+
if (!validName(name)) die(`Invalid profile name.`);
|
|
365
|
+
if (!fs.existsSync(profilePath(name))) {
|
|
366
|
+
die(`Profile "${name}" not found. Create it with ${cyan('claude-switch save ' + name)}.`);
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
name = activeProfileName();
|
|
370
|
+
if (!name) {
|
|
371
|
+
const live = liveAccountIdentity();
|
|
372
|
+
die(`No saved profile matches the current account${live.email ? ' (' + live.email + ')' : ''}.\n Save it first with ${cyan('claude-switch save <name>')}.`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const { email, exp } = persistProfile(name);
|
|
377
|
+
ok(`Updated profile ${bold(name)}${email ? dim(' (' + email + ')') : ''} from the live session.`);
|
|
378
|
+
if (exp.known) {
|
|
379
|
+
if (exp.expired) warn(`The live token is expired. Run ${cyan('claude') + ' → ' + cyan('/login')} first, then update again.`);
|
|
380
|
+
else info(dim(` Token now valid for ~${humanDuration(exp.msLeft)}.`));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function cmdUse(name, opts) {
|
|
385
|
+
if (!validName(name)) die(`Invalid profile name.`);
|
|
386
|
+
const p = readProfile(name);
|
|
387
|
+
if (!p.credentials) {
|
|
388
|
+
const have = listProfiles();
|
|
389
|
+
die(`Profile "${name}" not found.` + (have.length ? `\n Available: ${have.join(', ')}` : `\n No profiles saved yet — use ${cyan('claude-switch save <name>')}.`));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Back up whatever is currently live, so a bad switch is recoverable.
|
|
393
|
+
const current = readLiveCredentials();
|
|
394
|
+
if (current) {
|
|
395
|
+
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
|
396
|
+
fs.writeFileSync(path.join(BACKUP_DIR, 'credentials.last.json'), current, { mode: 0o600 });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Swap in the token + the matching account identity.
|
|
400
|
+
writeLiveCredentials(p.credentials);
|
|
401
|
+
patchClaudeJson(p.account);
|
|
402
|
+
|
|
403
|
+
const email = p.meta && p.meta.email;
|
|
404
|
+
ok(`Switched to ${bold(name)}${email ? dim(' (' + email + ')') : ''}.`);
|
|
405
|
+
|
|
406
|
+
// --- Verify the session ---------------------------------------------------
|
|
407
|
+
const oauth = parseOauth(p.credentials);
|
|
408
|
+
const exp = expiryInfo(oauth);
|
|
409
|
+
if (exp.known) {
|
|
410
|
+
if (exp.expired) {
|
|
411
|
+
warn(`This session's token expired ${humanDuration(exp.msLeft)} ago (${exp.at.toLocaleString()}).`);
|
|
412
|
+
warn(`Run ${cyan('claude') + ' → ' + cyan('/login')} to refresh it, then ${cyan('claude-switch save ' + name)} to update this profile.`);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
info(dim(` Token valid for ~${humanDuration(exp.msLeft)} (until ${exp.at.toLocaleString()}).`));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (opts.noCheck) return;
|
|
419
|
+
liveCheck(name);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Quick live check: ask Claude Code to confirm the session works end-to-end. */
|
|
423
|
+
function liveCheck(name) {
|
|
424
|
+
const claude = findClaude();
|
|
425
|
+
if (!claude) {
|
|
426
|
+
info(dim(' Skipping live check: `claude` not found on PATH.'));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
process.stdout.write(dim(' Verifying session with a quick `claude` call... '));
|
|
430
|
+
const r = spawnSync(claude, ['-p', 'Reply with exactly: OK'], {
|
|
431
|
+
encoding: 'utf8',
|
|
432
|
+
timeout: 45000,
|
|
433
|
+
shell: IS_WIN, // Windows resolves claude.cmd via the shell
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const out = ((r.stdout || '') + (r.stderr || '')).toLowerCase();
|
|
437
|
+
const authProblem = /401|403|unauthor|expired|invalid.*token|authentication|please run .*login|not logged in/.test(out);
|
|
438
|
+
|
|
439
|
+
if (r.error && r.error.code === 'ETIMEDOUT') {
|
|
440
|
+
process.stdout.write('\n');
|
|
441
|
+
warn('Live check timed out (network slow?). Token looks valid locally; try `claude` manually.');
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (r.status === 0 && !authProblem) {
|
|
445
|
+
process.stdout.write(green('OK\n'));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
process.stdout.write(red('failed\n'));
|
|
449
|
+
if (authProblem) {
|
|
450
|
+
warn(`Session appears invalid/expired. Run ${cyan('claude') + ' → ' + cyan('/login')}, then ${cyan('claude-switch save ' + name)} to refresh this profile.`);
|
|
451
|
+
} else {
|
|
452
|
+
warn(`Could not confirm the session (exit ${r.status}). Run ${cyan('claude')} manually to check.`);
|
|
453
|
+
if (r.stderr) info(dim(' ' + r.stderr.trim().split('\n')[0]));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function findClaude() {
|
|
458
|
+
const probe = IS_WIN
|
|
459
|
+
? spawnSync('where', ['claude'], { encoding: 'utf8', shell: true })
|
|
460
|
+
: spawnSync('command', ['-v', 'claude'], { encoding: 'utf8', shell: '/bin/sh' });
|
|
461
|
+
if (probe.status === 0 && probe.stdout.trim()) {
|
|
462
|
+
return probe.stdout.trim().split(/\r?\n/)[0];
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function cmdList(opts) {
|
|
468
|
+
const profiles = listProfiles();
|
|
469
|
+
const active = activeProfileName();
|
|
470
|
+
const live = liveAccountIdentity();
|
|
471
|
+
|
|
472
|
+
if (!profiles.length) {
|
|
473
|
+
info(`No profiles saved yet.`);
|
|
474
|
+
if (live.email) info(dim(`Current live session: ${live.email} — save it with `) + cyan('claude-switch save <name>'));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Live usage applies to the active session only (its token is the current one).
|
|
479
|
+
let liveUsage = null;
|
|
480
|
+
if (opts && opts.usage && active) {
|
|
481
|
+
const done = spinner('Fetching live usage for the active session...');
|
|
482
|
+
liveUsage = await fetchLiveUsage((parseOauth(readLiveCredentials() || '{}') || {}).accessToken);
|
|
483
|
+
done();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
info(bold('Saved Claude Code profiles:\n'));
|
|
487
|
+
for (const name of profiles) {
|
|
488
|
+
const p = readProfile(name);
|
|
489
|
+
const isActive = name === active;
|
|
490
|
+
const marker = isActive ? green('● ') : ' ';
|
|
491
|
+
const oauth = parseOauth(p.credentials || '{}');
|
|
492
|
+
const exp = expiryInfo(oauth);
|
|
493
|
+
|
|
494
|
+
let status;
|
|
495
|
+
if (!exp.known) status = dim('expiry unknown');
|
|
496
|
+
else if (exp.expired) status = red(`expired ${humanDuration(exp.msLeft)} ago`);
|
|
497
|
+
else status = green(`valid ${humanDuration(exp.msLeft)} left`);
|
|
498
|
+
|
|
499
|
+
const email = (p.meta && p.meta.email) || (p.account && p.account.oauthAccount && p.account.oauthAccount.emailAddress) || dim('unknown account');
|
|
500
|
+
const sub = p.meta && p.meta.subscriptionType ? dim(` [${p.meta.subscriptionType}]`) : '';
|
|
501
|
+
const tag = isActive ? green(' (active)') : '';
|
|
502
|
+
|
|
503
|
+
info(`${marker}${bold(name)}${tag}`);
|
|
504
|
+
info(` ${email}${sub} ${dim('token:')} ${status}`);
|
|
505
|
+
if (isActive && liveUsage) {
|
|
506
|
+
info(` ${dim('usage:')} ${renderUsageInline(liveUsage)}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (!active && live.email) {
|
|
510
|
+
info('\n' + dim(`Live session (${live.email}) doesn't match any saved profile — save it with `) + cyan('claude-switch save <name>'));
|
|
511
|
+
}
|
|
512
|
+
if (!(opts && opts.usage)) {
|
|
513
|
+
info('\n' + dim('Tip: ') + cyan('claude-switch list --usage') + dim(' shows live quota (1 tiny API call).'));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function cmdUsage() {
|
|
518
|
+
const raw = readLiveCredentials();
|
|
519
|
+
if (!raw) die(`No active session. Run ${cyan('claude') + ' → ' + cyan('/login')}.`);
|
|
520
|
+
const oauth = parseOauth(raw) || {};
|
|
521
|
+
const live = liveAccountIdentity();
|
|
522
|
+
const exp = expiryInfo(oauth);
|
|
523
|
+
|
|
524
|
+
info(`${bold('Current session:')} ${live.email || dim('unknown')}`);
|
|
525
|
+
if (exp.known) {
|
|
526
|
+
info(`${bold('Token:')} ${exp.expired ? red('expired') : green('valid, ' + humanDuration(exp.msLeft) + ' left')}`);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const done = spinner('Fetching live usage...');
|
|
530
|
+
const u = await fetchLiveUsage(oauth.accessToken);
|
|
531
|
+
done();
|
|
532
|
+
|
|
533
|
+
if (!u.ok) {
|
|
534
|
+
if (u.reason === 'auth') { warn(`Session invalid/expired. Run ${cyan('claude') + ' → ' + cyan('/login')} and re-save.`); return; }
|
|
535
|
+
warn(`Couldn't fetch usage (${u.reason}${u.error ? ': ' + u.error : ''}).`);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
info(bold('Usage limits:'));
|
|
539
|
+
for (const w of u.windows) {
|
|
540
|
+
const col = usageColor(w.leftPct);
|
|
541
|
+
const resets = w.resetAt
|
|
542
|
+
? dim(`resets in ${humanDuration(w.resetAt.getTime() - Date.now())} (${w.resetAt.toLocaleString()})`)
|
|
543
|
+
: '';
|
|
544
|
+
info(` ${w.label.padEnd(7)} ${col((w.leftPct + '% left').padEnd(9))} ${dim('(' + w.usedPct + '% used)')} ${resets}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function cmdDelete(name) {
|
|
549
|
+
if (!validName(name)) die(`Invalid profile name.`);
|
|
550
|
+
const dir = profilePath(name);
|
|
551
|
+
if (!fs.existsSync(dir)) die(`Profile "${name}" not found.`);
|
|
552
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
553
|
+
ok(`Deleted profile ${bold(name)}.`);
|
|
554
|
+
info(dim(' (Your live session is unchanged.)'));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function cmdWhoami() {
|
|
558
|
+
const live = liveAccountIdentity();
|
|
559
|
+
const raw = readLiveCredentials();
|
|
560
|
+
const exp = expiryInfo(parseOauth(raw || '{}'));
|
|
561
|
+
if (!raw) { info('No active Claude Code session.'); return; }
|
|
562
|
+
const active = activeProfileName();
|
|
563
|
+
info(`${bold('Account:')} ${live.email || dim('unknown')}`);
|
|
564
|
+
if (active) info(`${bold('Profile:')} ${green(active)}`);
|
|
565
|
+
else info(`${bold('Profile:')} ${dim('not saved')}`);
|
|
566
|
+
if (exp.known) info(`${bold('Token: ')} ${exp.expired ? red('expired') : green('valid, ' + humanDuration(exp.msLeft) + ' left')}`);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ---------------------------------------------------------------------------
|
|
570
|
+
// CLI
|
|
571
|
+
// ---------------------------------------------------------------------------
|
|
572
|
+
|
|
573
|
+
function usage() {
|
|
574
|
+
console.log(`${bold('claude-switch')} — switch between saved Claude Code accounts
|
|
575
|
+
|
|
576
|
+
${bold('Usage:')}
|
|
577
|
+
claude-switch save <profile> Save the current session as <profile>
|
|
578
|
+
claude-switch update [profile] Re-sync a profile from the live session ${dim('(default: active profile)')}
|
|
579
|
+
claude-switch use <profile> Switch to a saved profile ${dim('(--no-check to skip live verify)')}
|
|
580
|
+
claude-switch list List profiles and show which is active ${dim('(--usage for live quota)')}
|
|
581
|
+
claude-switch usage Show live usage limits for the current session
|
|
582
|
+
claude-switch delete <profile> Delete a saved profile
|
|
583
|
+
claude-switch whoami Show the current account/profile
|
|
584
|
+
claude-switch help Show this help
|
|
585
|
+
|
|
586
|
+
${dim('Profiles are stored in')} ${STORE_DIR}
|
|
587
|
+
`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function main() {
|
|
591
|
+
const argv = process.argv.slice(2);
|
|
592
|
+
const cmd = argv[0];
|
|
593
|
+
const rest = argv.slice(1);
|
|
594
|
+
const flags = new Set(rest.filter((a) => a.startsWith('--')));
|
|
595
|
+
const args = rest.filter((a) => !a.startsWith('--'));
|
|
596
|
+
const opts = { noCheck: flags.has('--no-check'), usage: flags.has('--usage') };
|
|
597
|
+
|
|
598
|
+
switch (cmd) {
|
|
599
|
+
case 'save': return cmdSave(args[0]);
|
|
600
|
+
case 'update':
|
|
601
|
+
case 'refresh': return cmdUpdate(args[0]);
|
|
602
|
+
case 'use': return cmdUse(args[0], opts);
|
|
603
|
+
case 'list':
|
|
604
|
+
case 'ls': return cmdList(opts);
|
|
605
|
+
case 'usage': return cmdUsage();
|
|
606
|
+
case 'delete':
|
|
607
|
+
case 'rm': return cmdDelete(args[0]);
|
|
608
|
+
case 'whoami': return cmdWhoami();
|
|
609
|
+
case 'help':
|
|
610
|
+
case '--help':
|
|
611
|
+
case '-h':
|
|
612
|
+
case undefined: return usage();
|
|
613
|
+
default:
|
|
614
|
+
die(`Unknown command "${cmd}". Run ${cyan('claude-switch help')}.`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
main().catch((e) => die(e && e.message ? e.message : String(e)));
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-profile-switch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Switch between multiple saved Claude Code accounts/sessions, with live usage limits. Installs the `claude-switch` command (macOS, Linux, Windows).",
|
|
5
|
+
"bin": {
|
|
6
|
+
"claude-switch": "claude-switch.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"claude-switch.js",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=16"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"claude",
|
|
18
|
+
"claude-code",
|
|
19
|
+
"account",
|
|
20
|
+
"profile",
|
|
21
|
+
"switcher",
|
|
22
|
+
"session",
|
|
23
|
+
"cli",
|
|
24
|
+
"anthropic"
|
|
25
|
+
],
|
|
26
|
+
"author": "Ashutosh Adhao <adhaoashutosh@gmail.com>",
|
|
27
|
+
"license": "MIT"
|
|
28
|
+
}
|