claude-org 0.1.0 → 0.2.2

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/CHANGELOG.md CHANGED
@@ -1,6 +1,23 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.0 (unreleased)
3
+ ## Unreleased
4
+
5
+ ## 0.2.2 — 2026-10-10
6
+
7
+ - Development: `npm run release -- <patch|minor|major|X.Y.Z>` runs the whole release (checks, tests, CHANGELOG, version, commit, tag, push, npm publish, registry check)
8
+
9
+ ## 0.2.1 — 2026-10-10
10
+
11
+ - `proxy`: a 429 is now classified before failing over (#8). Only a rejected 5h or 7d window takes the whole org out. Any other 429 (a used-up per-model allowance, a model that needs usage credits, a per-minute limit) takes the org out for that model only, for at most 10 minutes or the `retry-after` time, so other models keep using the org
12
+ - `proxy`: the log now shows the reason of each 429 (`error.type: message`, decoded from a brotli/gzip/deflate body) and the rate-limit claim, and says to switch the model when every org refused the same model
13
+ - Windows: launch `claude` without the Node DEP0190 deprecation warning (shell args passed as one quoted command string) (#10)
14
+
15
+ ## 0.2.0 — 2026-10-03
16
+
17
+ - Add `ccorg` as a short command alias for `claude-org` (same binary)
18
+ - Docs: clarify Setup (org picker, token is printed and saved manually, repeat once per org); example labels are `work` / `personal` and explicitly arbitrary
19
+
20
+ ## 0.1.0 — 2026-10-03
4
21
 
5
22
  Initial public release.
6
23
 
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 miyashita337
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.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 miyashita337
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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Launch [Claude Code](https://code.claude.com) under a specific Anthropic **organization** — and keep running when the 5-hour usage limit hits, by failing over to another org you belong to.
4
4
 
5
- When one account has multiple Claude organizations (e.g. a personal Pro/Max org and a company Team org), hitting the usage limit stalls your session. `claude-org` switches the credential `claude` starts with, and — in `proxy` mode — swaps it **mid-session** the moment the limit hits, so a long-running task never stops.
5
+ When one account has multiple Claude organizations (e.g. a personal org and your company's org, or several personal orgs), hitting the usage limit stalls your session. `claude-org` switches the credential `claude` starts with, and — in `proxy` mode — swaps it **mid-session** the moment the limit hits, so a long-running task never stops.
6
6
 
7
7
  Requires Node.js 18+. Zero runtime dependencies. Works on Windows, macOS, and Linux.
8
8
 
@@ -12,31 +12,43 @@ Requires Node.js 18+. Zero runtime dependencies. Works on Windows, macOS, and Li
12
12
  npm install -g claude-org
13
13
  ```
14
14
 
15
+ This installs two commands that behave identically: `claude-org` and the shorter alias `ccorg`. Examples below use `claude-org`; substitute `ccorg` anywhere.
16
+
15
17
  ## Setup
16
18
 
17
- 1. Generate a long-lived token per org (valid ~1 year):
19
+ Each org you want to switch between needs its own long-lived token, saved under a label you choose. The plain `claude` login (no token file) is called the **default org** and needs no setup.
20
+
21
+ Repeat steps 1–2 **once per org**:
22
+
23
+ 1. Generate a token for one org (valid ~1 year):
18
24
 
19
25
  ```sh
20
26
  claude setup-token
21
27
  ```
22
28
 
23
- In the browser, pick the org you want the token for, approve, and paste the auth code back into the terminal.
29
+ A browser opens. If your account belongs to several orgs, an **org picker** appears — choose the org this token is for. Approve, paste the auth code back into the terminal, and `claude setup-token` **prints the token to stdout**. It does not save it anywhere; you save it in the next step.
24
30
 
25
- 2. Save each token (no trailing newline) as `~/.claude/org-tokens/<name>.token` (`<name>` is your label, e.g. `team`):
31
+ 2. Save that token (no trailing newline) as `~/.claude/org-tokens/<name>.token`. `<name>` is just a label you pick — it does not need to match the org's plan or display name. `work` and `personal` are common; `client-a`, `personal2`, etc. are equally fine.
26
32
 
27
33
  ```sh
28
34
  # bash / macOS / Linux
29
35
  mkdir -p ~/.claude/org-tokens
30
- printf '%s' '<token>' > ~/.claude/org-tokens/team.token
36
+ printf '%s' '<token>' > ~/.claude/org-tokens/work.token
31
37
  ```
32
38
 
33
39
  ```powershell
34
40
  # PowerShell
35
41
  New-Item -ItemType Directory -Force "$env:USERPROFILE\.claude\org-tokens" | Out-Null
36
- Set-Content -NoNewline "$env:USERPROFILE\.claude\org-tokens\team.token" "<token>"
42
+ Set-Content -NoNewline "$env:USERPROFILE\.claude\org-tokens\work.token" "<token>"
37
43
  ```
38
44
 
39
- The plain `claude` login (no env) is called the **default org** and needs no token file. Note the token may visually wrap to two lines in the terminal — join it into one.
45
+ The token may visually wrap onto two lines in the terminal — join it into one.
46
+
47
+ 3. Check that each label points at the org you intended:
48
+
49
+ ```sh
50
+ claude-org status
51
+ ```
40
52
 
41
53
  ## Commands
42
54
 
@@ -44,7 +56,7 @@ npm install -g claude-org
44
56
  |---|---|
45
57
  | `claude-org proxy [claude args...]` | Launch via a local proxy: a **running** session fails over to another org the moment the 5h limit hits. Needs 2+ org tokens (incl. one for the default side). |
46
58
  | `claude-org auto [claude args...]` | Launch on the preferred org if usable, else on the default org. Decided once at launch. |
47
- | `claude-org <name> [claude args...]` | Launch with that org's token, e.g. `claude-org team --resume`. |
59
+ | `claude-org <name> [claude args...]` | Launch with that org's token, e.g. `claude-org work --resume`. |
48
60
  | `claude-org probe [<name>]` | Print the org's 5h window state on one line (`status= 5h_util= 5h_reset= 5h_reset_local=`). Exit 0 when `allowed`. |
49
61
  | `claude-org watch` | Poll the preferred org until its window recovers, then notify (terminal + optional `CLAUDE_ORG_NOTIFY_CMD`). |
50
62
  | `claude-org status` | `claude auth status` for every saved token + the default login. |
@@ -118,6 +130,20 @@ npm test # node:test suite, no real API calls
118
130
  npm pack --dry-run # inspect the published file set
119
131
  ```
120
132
 
133
+ ### Releasing
134
+
135
+ Write the changes under `## Unreleased` in `CHANGELOG.md`, then on an up-to-date `main`:
136
+
137
+ ```sh
138
+ npm login # once per machine
139
+ npm run release -- patch --dry-run # checks + tests only, changes nothing
140
+ npm run release -- patch # or minor / major / X.Y.Z
141
+ ```
142
+
143
+ The script refuses to run off `main`, with uncommitted tracked changes, out of sync with `origin/main`, with an existing tag or registry version, with files npm would publish that git does not track, or without npm login. It then runs the tests, renames `## Unreleased` to the new version heading, bumps `package.json`, commits `chore: release vX.Y.Z`, tags, pushes the commit and tag together (atomic), runs `npm publish` and waits until the registry serves the new version. `--no-publish` stops after the push.
144
+
145
+ Users update with `npm install -g claude-org@latest`.
146
+
121
147
  ## License
122
148
 
123
149
  MIT
package/bin/claude-org.js CHANGED
File without changes
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+ const { spawn } = require('child_process');
3
+
4
+ // DEP0190: passing an args array with shell:true is deprecated (args are
5
+ // concatenated unescaped). On Windows a shell is still needed to resolve
6
+ // claude.cmd, so build a single quoted command string instead.
7
+ const q = s => `"${String(s).replace(/(["\\$`])/g, '\\$1')}"`;
8
+
9
+ function spawnClaude(args, opts = {}) {
10
+ if (process.platform === 'win32') {
11
+ return spawn(['claude', ...args.map(q)].join(' '), { ...opts, shell: true });
12
+ }
13
+ return spawn('claude', args, opts);
14
+ }
15
+
16
+ module.exports = { spawnClaude, q };
package/lib/launch.js CHANGED
@@ -1,14 +1,13 @@
1
1
  'use strict';
2
- const { spawn } = require('child_process');
2
+ const { spawnClaude } = require('./claude-spawn');
3
3
  const { readToken } = require('./config');
4
4
 
5
5
  // Spawn claude with inherited stdio and extra env. Resolves to the child's
6
6
  // exit code (128+n when killed by signal n, like a shell would report).
7
7
  function runClaude(args, envExtra = {}) {
8
8
  return new Promise(resolve => {
9
- const child = spawn('claude', args, {
9
+ const child = spawnClaude(args, {
10
10
  stdio: 'inherit',
11
- shell: process.platform === 'win32', // resolves claude.cmd on Windows
12
11
  env: { ...process.env, ...envExtra },
13
12
  });
14
13
  child.on('error', e => {
package/lib/proxy.js CHANGED
@@ -6,6 +6,12 @@
6
6
  // with the next org's token. A running Claude Code session never sees the 429,
7
7
  // so the org switches without a restart.
8
8
  //
9
+ // Not every 429 means the org is used up. A model can have its own limit (for
10
+ // example a per-model weekly allowance, or usage credits that the org has not
11
+ // enabled) while the 5h and 7d windows are still "allowed" and other models
12
+ // work. Such a 429 only takes that org out for that model; the org keeps
13
+ // serving other models.
14
+ //
9
15
  // Why a proxy instead of apiKeyHelper: an OAuth long-lived token is accepted as
10
16
  // `Authorization: Bearer` alone, but if `x-api-key` is also present it is checked
11
17
  // first and returns 401. The proxy drops x-api-key and sends Bearer only.
@@ -23,6 +29,88 @@ const https = require('https');
23
29
  const fs = require('fs');
24
30
  const path = require('path');
25
31
  const os = require('os');
32
+ const zlib = require('zlib');
33
+
34
+ // How long an org stays out when it is org-wide limited but sent no reset time,
35
+ // and the longest a model-specific refusal keeps an (org, model) pair out.
36
+ const ORG_REJECT_FALLBACK_MS = 10 * 60 * 1000;
37
+ const MODEL_REJECT_MAX_MS = 10 * 60 * 1000;
38
+ // A real 429 body is well under 1 KiB. These caps keep a misbehaving upstream
39
+ // from exhausting memory: the raw body read, and the decompressed copy.
40
+ const ERROR_BODY_MAX_BYTES = 1024 * 1024;
41
+ const ERROR_DECODED_MAX_BYTES = 64 * 1024;
42
+
43
+ // The "model" field of a /v1/messages request body, or '?' when the body is
44
+ // not JSON (the proxy must still forward it). Used as is as the quarantine
45
+ // key, so two different model names never share a key.
46
+ function requestModel(body) {
47
+ try {
48
+ const m = JSON.parse(body.toString('utf8')).model;
49
+ return typeof m === 'string' && m ? m : '?';
50
+ } catch {
51
+ return '?';
52
+ }
53
+ }
54
+
55
+ // The model name for the log: client input, so limited to safe characters and
56
+ // cut short.
57
+ function logModel(model) {
58
+ return model.replace(/[^\w.:@/-]/g, '_').slice(0, 80);
59
+ }
60
+
61
+ // Milliseconds from a Retry-After header, which is either delay-seconds or an
62
+ // HTTP-date. NaN when absent or unparsable.
63
+ function retryAfterMs(value) {
64
+ if (value === undefined || value === '') return NaN;
65
+ const secs = Number(value);
66
+ if (Number.isFinite(secs)) return secs >= 0 ? secs * 1000 : NaN;
67
+ const at = Date.parse(value);
68
+ return Number.isFinite(at) ? Math.max(0, at - Date.now()) : NaN;
69
+ }
70
+
71
+ function readBody(res, maxBytes) {
72
+ return new Promise((resolve, reject) => {
73
+ const chunks = [];
74
+ let size = 0;
75
+ res.on('data', c => {
76
+ size += c.length;
77
+ if (size > maxBytes) {
78
+ res.destroy();
79
+ reject(new Error(`429 body larger than ${maxBytes} bytes`));
80
+ return;
81
+ }
82
+ chunks.push(c);
83
+ });
84
+ res.on('end', () => resolve(Buffer.concat(chunks)));
85
+ res.on('error', reject);
86
+ });
87
+ }
88
+
89
+ // The API compresses error bodies (brotli was observed on 2026-10-10) when the
90
+ // client sent accept-encoding. Decode a copy for the log; the client still
91
+ // gets the original bytes with the original content-encoding header.
92
+ // maxOutputLength makes a decompression bomb throw instead of filling memory.
93
+ function decodeBody(buf, encoding) {
94
+ const opts = { maxOutputLength: ERROR_DECODED_MAX_BYTES };
95
+ switch ((encoding || '').trim().toLowerCase()) {
96
+ case 'br': return zlib.brotliDecompressSync(buf, opts);
97
+ case 'gzip': case 'x-gzip': return zlib.gunzipSync(buf, opts);
98
+ case 'deflate': return zlib.inflateSync(buf, opts);
99
+ default: return buf;
100
+ }
101
+ }
102
+
103
+ // "error.type: error.message" from an API error body, for the log line. The
104
+ // message is cut short; the full body still reaches the client.
105
+ function describeError(buf, encoding) {
106
+ try {
107
+ const e = JSON.parse(decodeBody(buf, encoding).toString('utf8')).error || {};
108
+ const msg = String(e.message || '').replace(/\s+/g, ' ').slice(0, 160);
109
+ return msg ? `${e.type || 'unknown_error'}: ${msg}` : (e.type || 'unknown_error');
110
+ } catch {
111
+ return 'non-JSON 429 body';
112
+ }
113
+ }
26
114
 
27
115
  function createProxy(opts = {}) {
28
116
  const TOKEN_DIR = opts.tokenDir || process.env.CLAUDE_ORG_TOKEN_DIR
@@ -59,6 +147,15 @@ function createProxy(opts = {}) {
59
147
  return false;
60
148
  }
61
149
  }
150
+ // Per-(org, model) memory for model-specific refusals: "org|model" -> epoch (ms).
151
+ const modelRejectedUntil = new Map();
152
+ function isModelRejected(name, model) {
153
+ const key = `${name}|${model}`;
154
+ const until = modelRejectedUntil.get(key);
155
+ if (until === undefined) return false;
156
+ if (Date.now() >= until) { modelRejectedUntil.delete(key); return false; }
157
+ return true;
158
+ }
62
159
  function isRejected(name) {
63
160
  if (fileRejected(name)) return true;
64
161
  const until = rejectedUntil.get(name);
@@ -68,9 +165,10 @@ function createProxy(opts = {}) {
68
165
  }
69
166
  // Usable orgs first, in priority order; rate-limited orgs are moved to the
70
167
  // tail (kept as a last resort when everything is limited).
71
- function pickOrder() {
168
+ function pickOrder(model) {
72
169
  const orgs = listOrgs();
73
- return orgs.filter(o => !isRejected(o)).concat(orgs.filter(o => isRejected(o)));
170
+ const blocked = o => isRejected(o) || isModelRejected(o, model);
171
+ return orgs.filter(o => !blocked(o)).concat(orgs.filter(blocked));
74
172
  }
75
173
 
76
174
  function forward(org, method, urlPath, headers, body) {
@@ -99,8 +197,10 @@ function createProxy(opts = {}) {
99
197
  clientReq.on('end', async () => {
100
198
  const body = Buffer.concat(chunks);
101
199
  const isModelReq = clientReq.url.startsWith('/v1/messages');
102
- const order = pickOrder();
200
+ const model = isModelReq ? requestModel(body) : '';
201
+ const order = pickOrder(model);
103
202
  let attempt = 0;
203
+ let modelRefusals = 0;
104
204
  for (const org of order) {
105
205
  attempt++;
106
206
  let up;
@@ -110,21 +210,70 @@ function createProxy(opts = {}) {
110
210
  log(`upstream error via ${org}: ${e.message}`);
111
211
  continue;
112
212
  }
113
- const status5h = up.headers['anthropic-ratelimit-unified-5h-status'];
114
- const reset5h = Number(up.headers['anthropic-ratelimit-unified-5h-reset']);
115
- const limited = up.statusCode === 429 || status5h === 'rejected';
116
- if (limited && isModelReq && attempt < order.length) {
117
- rejectedUntil.set(org, Number.isFinite(reset5h) ? reset5h * 1000 : Date.now() + 10 * 60 * 1000);
118
- log(`${org} is rate-limited (HTTP ${up.statusCode}, 5h=${status5h ?? '?'}) -> retrying with next org`);
119
- up.resume(); // discard the response body and try the next org
213
+ const hdr = name => up.headers[`anthropic-ratelimit-unified-${name}`];
214
+ const status5h = hdr('5h-status');
215
+ const status7d = hdr('7d-status');
216
+ const claim = hdr('representative-claim');
217
+ // The 5h and 7d windows cover every model of the org, so only those make
218
+ // the org as a whole unusable. Other refusals are per model: a used-up
219
+ // per-model weekly allowance (claim=seven_day_overage_included) or a model
220
+ // that needs usage credits (429 with no window headers). The overall
221
+ // "unified-status" header is not used: it also reads "rejected" for those.
222
+ const orgWide = status5h === 'rejected' || status7d === 'rejected';
223
+ const limited = up.statusCode === 429 || orgWide;
224
+ const windows = `5h=${status5h ?? '?'}${claim ? `, claim=${claim}` : ''}`;
225
+ // A 429 body is a small JSON error; buffer it to log the reason and,
226
+ // if no org is left, to hand it to the client unchanged.
227
+ let errBody = null;
228
+ let reason = '';
229
+ if (isModelReq && up.statusCode === 429) {
230
+ try {
231
+ errBody = await readBody(up, ERROR_BODY_MAX_BYTES);
232
+ } catch (e) {
233
+ log(`upstream error via ${org} while reading 429 body: ${e.message}`);
234
+ continue;
235
+ }
236
+ reason = describeError(errBody, up.headers['content-encoding']);
237
+ }
238
+ const modelRefused = isModelReq && up.statusCode === 429 && !orgWide;
239
+ if (modelRefused) {
240
+ // retry-after is honoured when shorter (a transient per-minute limit
241
+ // must not keep the preferred org out for long); a long one (a weekly
242
+ // per-model allowance) is capped so the org is re-checked now and then.
243
+ const ra = retryAfterMs(up.headers['retry-after']);
244
+ const ms = Number.isFinite(ra) ? Math.min(ra, MODEL_REJECT_MAX_MS) : MODEL_REJECT_MAX_MS;
245
+ modelRejectedUntil.set(`${org}|${model}`, Date.now() + ms);
246
+ }
247
+ const isLast = attempt >= order.length;
248
+ if (limited && isModelReq && !isLast) {
249
+ if (orgWide) {
250
+ // When both windows are rejected the org is back only after the later reset.
251
+ const resets = [['5h', status5h], ['7d', status7d]]
252
+ .filter(([, s]) => s === 'rejected')
253
+ .map(([w]) => Number(hdr(`${w}-reset`)))
254
+ .filter(Number.isFinite);
255
+ rejectedUntil.set(org, resets.length ? Math.max(...resets) * 1000 : Date.now() + ORG_REJECT_FALLBACK_MS);
256
+ log(`${org} is rate-limited (HTTP ${up.statusCode}, ${windows}) -> retrying with next org${reason ? ` (${reason})` : ''}`);
257
+ } else {
258
+ modelRefusals++;
259
+ log(`${org} rejected model ${logModel(model)} (HTTP ${up.statusCode}, ${windows}, ${reason}) -> retrying with next org; ${org} stays in use for other models`);
260
+ }
261
+ if (!errBody) up.resume(); // discard the response body and try the next org
120
262
  continue;
121
263
  }
122
264
  if (isModelReq) {
123
- const util = up.headers['anthropic-ratelimit-unified-5h-utilization'];
124
- log(`${clientReq.method} ${clientReq.url} via ${org} -> ${up.statusCode} (5h ${status5h ?? '?'} ${util ?? '?'})`);
265
+ const util = hdr('5h-utilization');
266
+ log(`${clientReq.method} ${clientReq.url} via ${org} -> ${up.statusCode} (5h ${status5h ?? '?'} ${util ?? '?'})${reason ? ` ${reason}` : ''}`);
267
+ if (modelRefused && modelRefusals === attempt - 1) {
268
+ log(`all orgs rejected model ${logModel(model)} -> switch the model (e.g. /model) or wait for that model's limit to reset; the org tokens are not the cause`);
269
+ }
125
270
  }
126
271
  clientRes.writeHead(up.statusCode, { ...up.headers, 'x-claude-org': org });
127
- up.pipe(clientRes); // SSE streaming is piped through untouched
272
+ if (errBody) {
273
+ clientRes.end(errBody);
274
+ } else {
275
+ up.pipe(clientRes); // SSE streaming is piped through untouched
276
+ }
128
277
  return;
129
278
  }
130
279
  clientRes.writeHead(503, { 'content-type': 'application/json' });
package/lib/status.js CHANGED
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
- const { spawn } = require('child_process');
2
+ const { spawnClaude } = require('./claude-spawn');
3
3
  const { listOrgs, readToken } = require('./config');
4
4
  const { probe } = require('./probe');
5
5
 
@@ -10,8 +10,7 @@ const KEYS_RE_DEFAULT = /"(loggedIn|authMethod|orgName|subscriptionType)"/;
10
10
  // Run `claude auth status` and return combined stdout+stderr (like 2>&1).
11
11
  function authStatus(envExtra = {}) {
12
12
  return new Promise(resolve => {
13
- const child = spawn('claude', ['auth', 'status'], {
14
- shell: process.platform === 'win32',
13
+ const child = spawnClaude(['auth', 'status'], {
15
14
  env: { ...process.env, ...envExtra },
16
15
  });
17
16
  const chunks = [];
package/lib/usage.js CHANGED
@@ -2,16 +2,17 @@
2
2
 
3
3
  // Usage text shown for `claude-org` with no args (exit 2) and for
4
4
  // help/--help/-h (exit 0). Kept compatible with the original bash version.
5
- const USAGE = `claude-org: launch Claude Code with a specific org's long-lived token
5
+ const USAGE = `claude-org (alias: ccorg): launch Claude Code with a specific org's long-lived token
6
6
  (switch between the "default org" login and "token orgs").
7
7
 
8
- Background: when one email has multiple Claude orgs (e.g. a personal plan org and
9
- a company Team org) and you hit the usage limit, you want to continue on the other
8
+ Background: when one email has multiple Claude orgs (e.g. a personal org and
9
+ your company's org) and you hit the usage limit, you want to continue on the other
10
10
  org with --resume. Claude Code has no org-switch command, but
11
11
  CLAUDE_CODE_OAUTH_TOKEN (env) takes precedence over the saved OAuth login
12
12
  (https://code.claude.com/docs/en/authentication "Authentication precedence").
13
13
  Get a token per org with \`claude setup-token\` (valid 1 year) and store it as
14
- ~/.claude/org-tokens/<name>.token. The plain login (no env) org is called "default";
14
+ ~/.claude/org-tokens/<name>.token. <name> is any label you choose (e.g. work,
15
+ personal). The plain login (no env) org is called "default";
15
16
  no token file is needed for it.
16
17
 
17
18
  Usage (<prefer> resolves: CLAUDE_ORG_PREFER -> first of CLAUDE_ORG_ORDER -> first
@@ -22,7 +23,7 @@ token org alphabetically):
22
23
  connector MCPs are unavailable)
23
24
  claude-org auto [claude args...] launch on <prefer> if usable, else default
24
25
  (decided once at launch)
25
- claude-org <name> [claude args...] e.g. claude-org team --resume
26
+ claude-org <name> [claude args...] e.g. claude-org work --resume
26
27
  claude-org probe [<name>] print the org's 5h window state on one line
27
28
  (exit 0 when allowed)
28
29
  claude-org watch wait until <prefer> recovers, then notify
package/lib/watch.js CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
  const { spawn } = require('child_process');
3
+ const { q } = require('./claude-spawn');
3
4
  const { resolvePrefer } = require('./config');
4
5
  const { probe } = require('./probe');
5
6
  const { hhmmss } = require('./time');
@@ -16,7 +17,6 @@ function notify(title, message) {
16
17
  }
17
18
  // Quote args ourselves: spawn(shell:true) does not quote on Windows and the
18
19
  // title/message contain spaces.
19
- const q = s => `"${String(s).replace(/(["\\$`])/g, '\\$1')}"`;
20
20
  return new Promise(resolve => {
21
21
  const child = spawn(`${cmd} ${q(title)} ${q(message)}`, { shell: true, stdio: 'ignore' });
22
22
  child.on('error', () => {
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "claude-org",
3
- "version": "0.1.0",
3
+ "version": "0.2.2",
4
4
  "description": "Switch Claude Code between multiple Anthropic organizations, with automatic failover on the 5-hour rate limit",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
7
7
  "bin": {
8
- "claude-org": "bin/claude-org.js"
8
+ "claude-org": "bin/claude-org.js",
9
+ "ccorg": "bin/claude-org.js"
9
10
  },
10
11
  "engines": {
11
12
  "node": ">=18"
@@ -18,7 +19,8 @@
18
19
  "CHANGELOG.md"
19
20
  ],
20
21
  "scripts": {
21
- "test": "node --test"
22
+ "test": "node --test",
23
+ "release": "node scripts/release.js"
22
24
  },
23
25
  "keywords": [
24
26
  "claude-code",