claude-org 0.0.0-stage → 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — 2026-10-03
4
+
5
+ - Add `ccorg` as a short command alias for `claude-org` (same binary)
6
+ - Docs: clarify Setup (org picker, token is printed and saved manually, repeat once per org); example labels are `work` / `personal` and explicitly arbitrary
7
+
8
+ ## 0.1.0 — 2026-10-03
9
+
10
+ Initial public release.
11
+
12
+ - `claude-org` CLI: `proxy` / `auto` / `<name>` / `probe` / `watch` / `status` / `list`
13
+ - Dependency-free Node.js 18+ implementation (fetch / child_process); works on Windows, macOS, Linux
14
+ - `proxy`: session-scoped localhost proxy that rewrites `Authorization` per request and fails over to the next org on HTTP 429 or `5h-status=rejected`; SSE passthrough
15
+ - `proxy`: launches `claude` with `CLAUDE_CODE_PROMPT_CACHE_TTL=1h` and `ENABLE_TOOL_SEARCH=true` (user-set values win) to restore subscription-equivalent behavior under custom `ANTHROPIC_BASE_URL`; forwards `anthropic-beta` unchanged
16
+ - `watch`: recovery polling with externalized notification via `CLAUDE_ORG_NOTIFY_CMD`
17
+ - Preferred-org resolution: `CLAUDE_ORG_PREFER` -> `CLAUDE_ORG_ORDER` head -> first token file alphabetically
18
+ - Compatibility exception vs the original bash version: `help`/`--help`/`-h` exit 0 (was 2)
package/LICENSE ADDED
@@ -0,0 +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.
package/README.md CHANGED
@@ -1,3 +1,135 @@
1
- # Temporary Holding Version
1
+ # claude-org
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+
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
+
7
+ Requires Node.js 18+. Zero runtime dependencies. Works on Windows, macOS, and Linux.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install -g claude-org
13
+ ```
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
+
17
+ ## Setup
18
+
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):
24
+
25
+ ```sh
26
+ claude setup-token
27
+ ```
28
+
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.
30
+
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.
32
+
33
+ ```sh
34
+ # bash / macOS / Linux
35
+ mkdir -p ~/.claude/org-tokens
36
+ printf '%s' '<token>' > ~/.claude/org-tokens/work.token
37
+ ```
38
+
39
+ ```powershell
40
+ # PowerShell
41
+ New-Item -ItemType Directory -Force "$env:USERPROFILE\.claude\org-tokens" | Out-Null
42
+ Set-Content -NoNewline "$env:USERPROFILE\.claude\org-tokens\work.token" "<token>"
43
+ ```
44
+
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
+ ```
52
+
53
+ ## Commands
54
+
55
+ | Command | What it does |
56
+ |---|---|
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). |
58
+ | `claude-org auto [claude args...]` | Launch on the preferred org if usable, else on the default org. Decided once at launch. |
59
+ | `claude-org <name> [claude args...]` | Launch with that org's token, e.g. `claude-org work --resume`. |
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`. |
61
+ | `claude-org watch` | Poll the preferred org until its window recovers, then notify (terminal + optional `CLAUDE_ORG_NOTIFY_CMD`). |
62
+ | `claude-org status` | `claude auth status` for every saved token + the default login. |
63
+ | `claude-org list` | List saved org names. |
64
+
65
+ `auto` and `<name>` only read env at launch — they cannot switch a running session. Settings, plugins, and session history live under `~/.claude/` and are shared across orgs, so `claude-org auto --resume` continues a session started under the other org.
66
+
67
+ The **preferred org** (used by `auto`, `watch`, and as `proxy`'s first candidate) resolves in this order: `CLAUDE_ORG_PREFER` -> first entry of `CLAUDE_ORG_ORDER` -> first `.token` file alphabetically.
68
+
69
+ ## proxy mode
70
+
71
+ ```
72
+ claude-org proxy --resume
73
+ ```
74
+
75
+ 1. A session-scoped proxy starts on `127.0.0.1:<random port>` (one per session; no port collisions).
76
+ 2. `claude` launches with `ANTHROPIC_BASE_URL` pointing at the proxy and a **dummy** bearer token.
77
+ 3. The proxy replaces the `Authorization` header with a real org token and forwards to `api.anthropic.com`. On a 429 or `anthropic-ratelimit-unified-5h-status: rejected` it marks that org limited until `5h-reset` and **resends the same request** with the next org's token.
78
+ 4. Claude Code only ever sees the successful response — the org switched mid-session. SSE streaming is piped through untouched.
79
+
80
+ One log line per request (`via <org> -> <status>`) goes to a temp file printed at launch (`-> proxy 127.0.0.1:<port> (order: ..., log: ...)`).
81
+
82
+ **Limitations of proxy mode**
83
+
84
+ - claude.ai connector MCPs (Gmail / Drive / Calendar / Notion / Slack …) and Remote Control are **unavailable**: `ANTHROPIC_AUTH_TOKEN` makes Claude Code treat the session as external-token auth, so `mcp__claude_ai_*` tools do not appear. Local/plugin MCPs work. Open connectors in a plain `claude --resume` session — history is shared.
85
+ - Each proxy serves one session and dies when `claude` exits.
86
+
87
+ ### Prompt cache & tool search
88
+
89
+ A custom `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN` makes Claude Code treat the session as API-key auth, which changes two defaults:
90
+
91
+ - **Prompt cache TTL drops to 5 minutes** (subscription-direct sessions get 1 hour). The 1-hour TTL is requested via the `anthropic-beta` header, which the proxy forwards unchanged.
92
+ - **MCP tool search** (deferred tool-definition loading) is **disabled**.
93
+
94
+ `claude-org proxy` restores the subscription-equivalent behavior by launching `claude` with `CLAUDE_CODE_PROMPT_CACHE_TTL=1h` and `ENABLE_TOOL_SEARCH=true`. If you have set either variable yourself, your value wins.
95
+
96
+ | Entry point | Mid-session switch | Connector MCPs | Best for |
97
+ |---|---|---|---|
98
+ | `claude` | no (default org) | yes | connector work |
99
+ | `claude-org auto --resume` | at launch only | no on preferred org / yes on default | short tasks |
100
+ | `claude-org proxy --resume` | **automatic** | no | long-running work, agents |
101
+
102
+ ## Environment variables
103
+
104
+ | Variable | Default | Purpose |
105
+ |---|---|---|
106
+ | `CLAUDE_ORG_PREFER` | (unset) | Preferred org for `auto` / `watch` / `proxy` |
107
+ | `CLAUDE_ORG_ORDER` | (unset) | Comma-separated org priority for `proxy`; unlisted orgs follow alphabetically |
108
+ | `CLAUDE_ORG_TOKEN_DIR` | `~/.claude/org-tokens` | Where `<name>.token` files live |
109
+ | `CLAUDE_ORG_NOTIFY_CMD` | (unset) | `watch` notification command, run as `<cmd> "<title>" "<message>"` via the OS shell |
110
+ | `CLAUDE_ORG_WATCH_MAX_SLEEP` | `18000` | Max seconds between `watch` re-checks (values < 60 also lower the 60s minimum — for tests) |
111
+ | `CLAUDE_ORG_UPSTREAM` | `https://api.anthropic.com` | API base URL for `probe` and `proxy` (stub upstream in tests) |
112
+ | `CLAUDE_ORG_PROXY` | `1` when launched via `proxy` | Marker env passed to the claude process so user hooks can detect connector-less sessions |
113
+ | `CLAUDE_ORG_FORCE_PROBE` | (unset) | Test seam: `allowed` / `rejected[:<epoch>]` / `rejected_until:<epoch>` fakes `probe` results without API calls |
114
+ | `CLAUDE_ORG_REJECT_FILE` | (unset) | Test seam: orgs listed comma-separated in this file are treated as rate-limited by `proxy` |
115
+
116
+ ## Notes
117
+
118
+ - **Token safety**: tokens are read from files only and never printed to logs, errors, or test snapshots. The proxy binds to `127.0.0.1` only.
119
+ - **probe cost**: each `probe` sends one `max_tokens=1` request (~11 tokens billed to that org), once per `auto` launch.
120
+ - **Token expiry**: tokens last ~1 year. If `probe` returns `status=unknown` or `status` shows `(auth status failed)`, rerun `claude setup-token` and overwrite the file.
121
+
122
+ ## A note on terms
123
+
124
+ claude-org only switches between Anthropic organizations **you personally belong to**, using long-lived tokens you generated yourself via `claude setup-token`. All traffic stays on your machine (the proxy binds to 127.0.0.1). Per Anthropic's authentication docs, OAuth tokens are intended for ordinary use of Claude Code — do not use this tool to route other people's requests through your subscription credentials. Each org's usage is billed/limited to that org as usual. Check [Anthropic's Usage Policy](https://www.anthropic.com/legal/aup) and your plan's terms yourself before use. Commands that mention token env names may also be blocked by safety hooks in hardened Claude Code setups.
125
+
126
+ ## Development
127
+
128
+ ```sh
129
+ npm test # node:test suite, no real API calls
130
+ npm pack --dry-run # inspect the published file set
131
+ ```
132
+
133
+ ## License
134
+
135
+ MIT
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { printUsage } = require('../lib/usage');
5
+ const { listOrgs, resolvePrefer } = require('../lib/config');
6
+ const { probe } = require('../lib/probe');
7
+ const { pick } = require('../lib/pick');
8
+ const { launch } = require('../lib/launch');
9
+ const { statusAll } = require('../lib/status');
10
+ const { watch } = require('../lib/watch');
11
+ const { proxyLaunch } = require('../lib/proxy');
12
+
13
+ async function main() {
14
+ const [cmd, ...rest] = process.argv.slice(2);
15
+ switch (cmd) {
16
+ case undefined:
17
+ return printUsage(2);
18
+ case '-h':
19
+ case '--help':
20
+ case 'help':
21
+ return printUsage(0);
22
+ case 'list':
23
+ for (const o of listOrgs()) console.log(o);
24
+ return 0;
25
+ case 'status':
26
+ return statusAll();
27
+ case 'probe': {
28
+ const r = await probe(rest[0] || resolvePrefer());
29
+ if (r.line) console.log(r.line);
30
+ return r.allowed ? 0 : 1;
31
+ }
32
+ case 'pick':
33
+ console.log(await pick());
34
+ return 0;
35
+ case 'auto':
36
+ return launch(await pick(), rest);
37
+ case 'watch':
38
+ return watch();
39
+ case 'proxy':
40
+ return proxyLaunch(rest);
41
+ default:
42
+ // <name> launches claude with that org's token.
43
+ return launch(cmd, rest);
44
+ }
45
+ }
46
+
47
+ main()
48
+ .then(code => process.exit(code))
49
+ .catch(e => {
50
+ console.error(`claude-org: ${e.message}`);
51
+ process.exit(1);
52
+ });
package/lib/config.js ADDED
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ function tokenDir() {
7
+ return process.env.CLAUDE_ORG_TOKEN_DIR || path.join(os.homedir(), '.claude', 'org-tokens');
8
+ }
9
+
10
+ // Saved org names: *.token files, extension stripped, sorted.
11
+ function listOrgs(dir = tokenDir()) {
12
+ let files;
13
+ try {
14
+ files = fs.readdirSync(dir);
15
+ } catch {
16
+ return [];
17
+ }
18
+ return files.filter(f => f.endsWith('.token')).map(f => f.slice(0, -6)).sort();
19
+ }
20
+
21
+ // Read a token file, stripping CR/LF. Returns null (after printing guidance)
22
+ // when the file is missing or empty.
23
+ function readToken(name, dir = tokenDir()) {
24
+ const f = path.join(dir, `${name}.token`);
25
+ let raw = '';
26
+ try {
27
+ raw = fs.readFileSync(f, 'utf8');
28
+ } catch {
29
+ /* fall through to the error below */
30
+ }
31
+ if (!raw.trim()) {
32
+ console.error(`claude-org: token file not found: ${f}`);
33
+ console.error(` 取得: claude setup-token でブラウザから org '${name}' を選び、出力トークンを ${f} に保存(改行なし)`);
34
+ return null;
35
+ }
36
+ return raw.replace(/[\r\n]/g, '');
37
+ }
38
+
39
+ // Preferred org resolution order:
40
+ // 1. CLAUDE_ORG_PREFER
41
+ // 2. first entry of CLAUDE_ORG_ORDER
42
+ // 3. first .token file in the token dir (alphabetical, same rule as the proxy)
43
+ // 4. no tokens at all -> print setup guidance and exit 1
44
+ function resolvePrefer() {
45
+ if (process.env.CLAUDE_ORG_PREFER) return process.env.CLAUDE_ORG_PREFER;
46
+ const order = (process.env.CLAUDE_ORG_ORDER || '').split(',').map(s => s.trim()).filter(Boolean);
47
+ if (order.length) return order[0];
48
+ const orgs = listOrgs();
49
+ if (orgs.length) return orgs[0];
50
+ console.error(`claude-org: no org tokens found in ${tokenDir()}`);
51
+ console.error(` 取得: claude setup-token で org を選び、出力トークンを ${tokenDir()}/<name>.token に保存(改行なし)`);
52
+ process.exit(1);
53
+ }
54
+
55
+ module.exports = { tokenDir, listOrgs, readToken, resolvePrefer };
package/lib/launch.js ADDED
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+ const { spawn } = require('child_process');
3
+ const { readToken } = require('./config');
4
+
5
+ // Spawn claude with inherited stdio and extra env. Resolves to the child's
6
+ // exit code (128+n when killed by signal n, like a shell would report).
7
+ function runClaude(args, envExtra = {}) {
8
+ return new Promise(resolve => {
9
+ const child = spawn('claude', args, {
10
+ stdio: 'inherit',
11
+ shell: process.platform === 'win32', // resolves claude.cmd on Windows
12
+ env: { ...process.env, ...envExtra },
13
+ });
14
+ child.on('error', e => {
15
+ console.error(`claude-org: failed to launch claude: ${e.message}`);
16
+ resolve(1);
17
+ });
18
+ child.on('exit', (code, signal) => {
19
+ if (code !== null) return resolve(code);
20
+ resolve(signal ? 128 + ({ SIGINT: 2, SIGTERM: 15 }[signal] || 1) : 1);
21
+ });
22
+ });
23
+ }
24
+
25
+ // launch(name, args): 'default' launches a plain `claude`; a token org launches
26
+ // with CLAUDE_CODE_OAUTH_TOKEN set. Returns the process exit code.
27
+ async function launch(name, args) {
28
+ if (name === 'default') return runClaude(args);
29
+ const tok = readToken(name);
30
+ if (tok === null) return 1;
31
+ return runClaude(args, { CLAUDE_CODE_OAUTH_TOKEN: tok });
32
+ }
33
+
34
+ module.exports = { runClaude, launch };
package/lib/pick.js ADDED
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+ const { resolvePrefer } = require('./config');
3
+ const { probe } = require('./probe');
4
+
5
+ // pick(): which org `auto` should launch on. Returns 'default' when the
6
+ // preferred org is over its 5h limit (or its token is missing). The decision
7
+ // reason goes to stderr as a single `-> ...` line.
8
+ async function pick() {
9
+ const prefer = resolvePrefer();
10
+ const r = await probe(prefer);
11
+ if (r.allowed) {
12
+ let util = r.util;
13
+ if (util !== '?') util = `${Math.trunc(Number(util) * 100)}%`;
14
+ console.error(`-> ${prefer} (5h ${util} used)`);
15
+ return prefer;
16
+ }
17
+ console.error(`-> default (${prefer} is ${r.status}, resets ${r.resetLocal})`);
18
+ return 'default';
19
+ }
20
+
21
+ module.exports = { pick };
package/lib/probe.js ADDED
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+ const { readToken } = require('./config');
3
+ const { epochToHHMM } = require('./time');
4
+
5
+ // probe(name): send a 1-token inference request and read the unified 5h
6
+ // rate-limit headers. Returns { line, status, util, reset, resetLocal, allowed }.
7
+ // `line` is the canonical one-line output:
8
+ // status=<allowed|rejected|unknown> 5h_util=<0.xx|?> 5h_reset=<epoch|?> 5h_reset_local=<HH:MM|?>
9
+ //
10
+ // Test seam: CLAUDE_ORG_FORCE_PROBE = allowed | rejected[:<epoch>] |
11
+ // rejected_until:<epoch> (rejected_until flips to allowed once <epoch> passes).
12
+ // When forced, the token file is not required and no request is sent.
13
+ // CLAUDE_ORG_UPSTREAM overrides the API base URL (stub upstream in tests).
14
+ async function probe(name) {
15
+ const forced = process.env.CLAUDE_ORG_FORCE_PROBE;
16
+ let status, util, reset;
17
+ if (forced) {
18
+ const idx = forced.indexOf(':');
19
+ status = idx === -1 ? forced : forced.slice(0, idx);
20
+ reset = idx === -1 ? '?' : forced.slice(idx + 1);
21
+ if (status === 'rejected_until') {
22
+ status = Math.floor(Date.now() / 1000) >= Number(reset) ? 'allowed' : 'rejected';
23
+ }
24
+ util = '?';
25
+ } else {
26
+ const tok = readToken(name);
27
+ if (tok === null) return { line: null, status: 'unknown', util: '?', reset: '?', resetLocal: '?', allowed: false };
28
+ const base = (process.env.CLAUDE_ORG_UPSTREAM || 'https://api.anthropic.com').replace(/\/+$/, '');
29
+ let headers = null;
30
+ try {
31
+ const res = await fetch(`${base}/v1/messages`, {
32
+ method: 'POST',
33
+ headers: {
34
+ authorization: `Bearer ${tok}`,
35
+ 'anthropic-beta': 'oauth-2025-04-20',
36
+ 'anthropic-version': '2023-06-01',
37
+ 'content-type': 'application/json',
38
+ },
39
+ body: JSON.stringify({
40
+ model: 'claude-haiku-4-5-20251001',
41
+ max_tokens: 1,
42
+ messages: [{ role: 'user', content: 'hi' }],
43
+ }),
44
+ signal: AbortSignal.timeout(20000),
45
+ });
46
+ headers = res.headers;
47
+ res.body && res.body.cancel().catch(() => {});
48
+ } catch {
49
+ headers = null;
50
+ }
51
+ status = (headers && (headers.get('anthropic-ratelimit-unified-5h-status')
52
+ || headers.get('anthropic-ratelimit-unified-status'))) || 'unknown';
53
+ util = (headers && headers.get('anthropic-ratelimit-unified-5h-utilization')) || '?';
54
+ reset = (headers && headers.get('anthropic-ratelimit-unified-5h-reset')) || '?';
55
+ }
56
+ const resetLocal = reset !== '?' ? epochToHHMM(reset) : '?';
57
+ const line = `status=${status} 5h_util=${util} 5h_reset=${reset} 5h_reset_local=${resetLocal}`;
58
+ return { line, status, util, reset, resetLocal, allowed: status === 'allowed' };
59
+ }
60
+
61
+ module.exports = { probe };
package/lib/proxy.js ADDED
@@ -0,0 +1,199 @@
1
+ 'use strict';
2
+ // Local proxy between Claude Code and the Anthropic API. It rewrites the
3
+ // Authorization header to the token of "the org that is currently usable" and,
4
+ // when it sees the 5h limit (HTTP 429 or response header
5
+ // anthropic-ratelimit-unified-5h-status: rejected), resends the same request
6
+ // with the next org's token. A running Claude Code session never sees the 429,
7
+ // so the org switches without a restart.
8
+ //
9
+ // Why a proxy instead of apiKeyHelper: an OAuth long-lived token is accepted as
10
+ // `Authorization: Bearer` alone, but if `x-api-key` is also present it is checked
11
+ // first and returns 401. The proxy drops x-api-key and sends Bearer only.
12
+ //
13
+ // Env (standalone mode):
14
+ // PORT listen port (default 0 = OS picks; reported on the
15
+ // "listening on" stderr line)
16
+ // CLAUDE_ORG_TOKEN_DIR where <name>.token files live (~/.claude/org-tokens)
17
+ // CLAUDE_ORG_ORDER org priority, comma-separated (default: alphabetical)
18
+ // CLAUDE_ORG_UPSTREAM forward target (default https://api.anthropic.com)
19
+ // CLAUDE_ORG_REJECT_FILE test seam: orgs listed comma-separated in this file
20
+ // are treated as rate-limited
21
+ const http = require('http');
22
+ const https = require('https');
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const os = require('os');
26
+
27
+ function createProxy(opts = {}) {
28
+ const TOKEN_DIR = opts.tokenDir || process.env.CLAUDE_ORG_TOKEN_DIR
29
+ || path.join(os.homedir(), '.claude', 'org-tokens');
30
+ const ORDER = opts.order
31
+ || (process.env.CLAUDE_ORG_ORDER || '').split(',').map(s => s.trim()).filter(Boolean);
32
+ const UPSTREAM = new URL(opts.upstream || process.env.CLAUDE_ORG_UPSTREAM || 'https://api.anthropic.com');
33
+ const REJECT_FILE = 'rejectFile' in opts ? opts.rejectFile : (process.env.CLAUDE_ORG_REJECT_FILE || '');
34
+ const out = opts.log || process.stderr;
35
+ const transport = UPSTREAM.protocol === 'http:' ? http : https;
36
+
37
+ function log(...a) {
38
+ out.write(`[claude-org-proxy ${new Date().toISOString().slice(11, 19)}] ${a.join(' ')}\n`);
39
+ }
40
+
41
+ function listOrgs() {
42
+ const files = fs.readdirSync(TOKEN_DIR).filter(f => f.endsWith('.token')).map(f => f.slice(0, -6)).sort();
43
+ if (ORDER.length === 0) return files;
44
+ const known = new Set(files);
45
+ return ORDER.filter(o => known.has(o)).concat(files.filter(f => !ORDER.includes(f)));
46
+ }
47
+ function readToken(name) {
48
+ return fs.readFileSync(path.join(TOKEN_DIR, `${name}.token`), 'utf8').replace(/[\r\n]/g, '');
49
+ }
50
+
51
+ // Per-org "rate-limited" memory: name -> recovery epoch (ms). Expired entries
52
+ // automatically re-enter the candidate pool.
53
+ const rejectedUntil = new Map();
54
+ function fileRejected(name) {
55
+ if (!REJECT_FILE) return false;
56
+ try {
57
+ return fs.readFileSync(REJECT_FILE, 'utf8').split(',').map(s => s.trim()).includes(name);
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+ function isRejected(name) {
63
+ if (fileRejected(name)) return true;
64
+ const until = rejectedUntil.get(name);
65
+ if (until === undefined) return false;
66
+ if (Date.now() >= until) { rejectedUntil.delete(name); return false; }
67
+ return true;
68
+ }
69
+ // Usable orgs first, in priority order; rate-limited orgs are moved to the
70
+ // tail (kept as a last resort when everything is limited).
71
+ function pickOrder() {
72
+ const orgs = listOrgs();
73
+ return orgs.filter(o => !isRejected(o)).concat(orgs.filter(o => isRejected(o)));
74
+ }
75
+
76
+ function forward(org, method, urlPath, headers, body) {
77
+ return new Promise((resolve, reject) => {
78
+ const h = { ...headers };
79
+ for (const k of Object.keys(h)) {
80
+ const lk = k.toLowerCase();
81
+ if (lk === 'x-api-key' || lk === 'host' || lk === 'content-length' || lk === 'authorization') delete h[k];
82
+ }
83
+ h.authorization = `Bearer ${readToken(org)}`;
84
+ h.host = UPSTREAM.host;
85
+ if (body.length) h['content-length'] = String(body.length);
86
+ const req = transport.request(
87
+ { hostname: UPSTREAM.hostname, port: UPSTREAM.port || (transport === https ? 443 : 80), path: urlPath, method, headers: h },
88
+ res => resolve(res),
89
+ );
90
+ req.on('error', reject);
91
+ if (body.length) req.write(body);
92
+ req.end();
93
+ });
94
+ }
95
+
96
+ const server = http.createServer((clientReq, clientRes) => {
97
+ const chunks = [];
98
+ clientReq.on('data', c => chunks.push(c));
99
+ clientReq.on('end', async () => {
100
+ const body = Buffer.concat(chunks);
101
+ const isModelReq = clientReq.url.startsWith('/v1/messages');
102
+ const order = pickOrder();
103
+ let attempt = 0;
104
+ for (const org of order) {
105
+ attempt++;
106
+ let up;
107
+ try {
108
+ up = await forward(org, clientReq.method, clientReq.url, clientReq.headers, body);
109
+ } catch (e) {
110
+ log(`upstream error via ${org}: ${e.message}`);
111
+ continue;
112
+ }
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
120
+ continue;
121
+ }
122
+ 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 ?? '?'})`);
125
+ }
126
+ clientRes.writeHead(up.statusCode, { ...up.headers, 'x-claude-org': org });
127
+ up.pipe(clientRes); // SSE streaming is piped through untouched
128
+ return;
129
+ }
130
+ clientRes.writeHead(503, { 'content-type': 'application/json' });
131
+ clientRes.end(JSON.stringify({ type: 'error', error: { type: 'claude_org_proxy', message: 'all orgs rate-limited or unreachable' } }));
132
+ });
133
+ });
134
+
135
+ return { server, listOrgs, log, upstream: UPSTREAM };
136
+ }
137
+
138
+ // `claude-org proxy [claude args...]`: start an in-process proxy on a random
139
+ // port, point ANTHROPIC_BASE_URL at it, launch claude, and shut the proxy down
140
+ // when claude exits. One proxy per session, so parallel sessions never collide.
141
+ async function proxyLaunch(args) {
142
+ const { listOrgs, tokenDir, resolvePrefer } = require('./config');
143
+ const { runClaude } = require('./launch');
144
+
145
+ const orgs = listOrgs();
146
+ if (orgs.length < 2) {
147
+ console.error(`claude-org: proxy には org トークンが 2 つ以上必要です(${tokenDir()} に ${orgs.join(' ')})`);
148
+ console.error(` 取得: claude setup-token でもう片方の org を選び、${tokenDir()}/<name>.token に保存`);
149
+ return 1;
150
+ }
151
+
152
+ const order = process.env.CLAUDE_ORG_ORDER || resolvePrefer();
153
+ const logf = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'claude-org-proxy-')), 'proxy.log');
154
+ const logStream = fs.createWriteStream(logf);
155
+ const { server, log, upstream } = createProxy({ order: order.split(','), log: logStream });
156
+
157
+ try {
158
+ await new Promise((resolve, reject) => {
159
+ server.once('error', reject);
160
+ server.listen(0, '127.0.0.1', () => {
161
+ log(`listening on http://127.0.0.1:${server.address().port} orgs=${listOrgs().join(',')} upstream=${upstream.host}`);
162
+ resolve();
163
+ });
164
+ });
165
+ } catch (e) {
166
+ console.error(`claude-org: proxy が起動しませんでした: ${e.message}`);
167
+ return 1;
168
+ }
169
+ const port = server.address().port;
170
+ console.error(`-> proxy 127.0.0.1:${port} (order: ${order}, log: ${logf})`);
171
+
172
+ // A custom ANTHROPIC_BASE_URL + AUTH_TOKEN makes Claude Code treat the session
173
+ // as API-key auth: prompt cache TTL drops to 5min and MCP tool search is
174
+ // disabled. Restore subscription-equivalent defaults; user-set values win.
175
+ const envExtra = {
176
+ ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
177
+ ANTHROPIC_AUTH_TOKEN: 'claude-org-proxy', // dummy; the proxy replaces it
178
+ CLAUDE_ORG_PROXY: '1', // marker: "connector MCPs unavailable" session
179
+ };
180
+ if (!process.env.CLAUDE_CODE_PROMPT_CACHE_TTL) envExtra.CLAUDE_CODE_PROMPT_CACHE_TTL = '1h';
181
+ if (!process.env.ENABLE_TOOL_SEARCH) envExtra.ENABLE_TOOL_SEARCH = 'true';
182
+
183
+ const rc = await runClaude(args, envExtra);
184
+ server.close();
185
+ logStream.end();
186
+ return rc;
187
+ }
188
+
189
+ // Standalone mode: `node lib/proxy.js` (env-driven, same as the bash version's
190
+ // `node claude-org-proxy.js`).
191
+ if (require.main === module) {
192
+ const PORT = Number(process.env.PORT || 0);
193
+ const { server, listOrgs, log, upstream } = createProxy();
194
+ server.listen(PORT, '127.0.0.1', () => {
195
+ log(`listening on http://127.0.0.1:${server.address().port} orgs=${listOrgs().join(',')} upstream=${upstream.host}`);
196
+ });
197
+ }
198
+
199
+ module.exports = { createProxy, proxyLaunch };
package/lib/status.js ADDED
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+ const { spawn } = require('child_process');
3
+ const { listOrgs, readToken } = require('./config');
4
+ const { probe } = require('./probe');
5
+
6
+ const KEYS = '"(loggedIn|authMethod|orgName|subscriptionType|email)"';
7
+ const KEYS_RE = /"(loggedIn|authMethod|orgName|subscriptionType|email)"/;
8
+ const KEYS_RE_DEFAULT = /"(loggedIn|authMethod|orgName|subscriptionType)"/;
9
+
10
+ // Run `claude auth status` and return combined stdout+stderr (like 2>&1).
11
+ function authStatus(envExtra = {}) {
12
+ return new Promise(resolve => {
13
+ const child = spawn('claude', ['auth', 'status'], {
14
+ shell: process.platform === 'win32',
15
+ env: { ...process.env, ...envExtra },
16
+ });
17
+ const chunks = [];
18
+ child.stdout.on('data', c => chunks.push(c));
19
+ child.stderr.on('data', c => chunks.push(c));
20
+ child.on('error', () => resolve(''));
21
+ child.on('close', () => resolve(Buffer.concat(chunks).toString('utf8')));
22
+ });
23
+ }
24
+
25
+ async function statusAll() {
26
+ for (const name of listOrgs()) {
27
+ const tok = readToken(name);
28
+ if (tok === null) continue;
29
+ console.log(`== ${name} ==`);
30
+ const out = await authStatus({ CLAUDE_CODE_OAUTH_TOKEN: tok });
31
+ const lines = out.split('\n').filter(l => KEYS_RE.test(l));
32
+ console.log(lines.length ? lines.join('\n') : ' (auth status failed)');
33
+ const r = await probe(name);
34
+ if (r.line) console.log(` ${r.line}`);
35
+ }
36
+ console.log('== default (no env) ==');
37
+ const out = await authStatus();
38
+ console.log(out.split('\n').filter(l => KEYS_RE_DEFAULT.test(l)).join('\n'));
39
+ return 0;
40
+ }
41
+
42
+ module.exports = { statusAll, KEYS };
package/lib/time.js ADDED
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ // epoch seconds -> local HH:MM ('?' on garbage). Replaces the GNU/BSD `date`
4
+ // dependency of the bash version.
5
+ function epochToHHMM(epochSec) {
6
+ const d = new Date(Number(epochSec) * 1000);
7
+ if (Number.isNaN(d.getTime())) return '?';
8
+ const p = n => String(n).padStart(2, '0');
9
+ return `${p(d.getHours())}:${p(d.getMinutes())}`;
10
+ }
11
+
12
+ function hhmmss(d = new Date()) {
13
+ const p = n => String(n).padStart(2, '0');
14
+ return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
15
+ }
16
+
17
+ module.exports = { epochToHHMM, hhmmss };
package/lib/usage.js ADDED
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ // Usage text shown for `claude-org` with no args (exit 2) and for
4
+ // help/--help/-h (exit 0). Kept compatible with the original bash version.
5
+ const USAGE = `claude-org (alias: ccorg): launch Claude Code with a specific org's long-lived token
6
+ (switch between the "default org" login and "token orgs").
7
+
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
+ org with --resume. Claude Code has no org-switch command, but
11
+ CLAUDE_CODE_OAUTH_TOKEN (env) takes precedence over the saved OAuth login
12
+ (https://code.claude.com/docs/en/authentication "Authentication precedence").
13
+ Get a token per org with \`claude setup-token\` (valid 1 year) and store it as
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";
16
+ no token file is needed for it.
17
+
18
+ Usage (<prefer> resolves: CLAUDE_ORG_PREFER -> first of CLAUDE_ORG_ORDER -> first
19
+ token org alphabetically):
20
+ claude-org proxy [claude args...] launch via a local proxy; a running session
21
+ fails over to another org the moment the 5h
22
+ limit hits (needs 2+ org tokens; claude.ai
23
+ connector MCPs are unavailable)
24
+ claude-org auto [claude args...] launch on <prefer> if usable, else default
25
+ (decided once at launch)
26
+ claude-org <name> [claude args...] e.g. claude-org work --resume
27
+ claude-org probe [<name>] print the org's 5h window state on one line
28
+ (exit 0 when allowed)
29
+ claude-org watch wait until <prefer> recovers, then notify
30
+ (terminal + optional CLAUDE_ORG_NOTIFY_CMD)
31
+ claude-org status show \`claude auth status\` for each org token
32
+ claude-org list list saved org names
33
+
34
+ auto / <name> only read env at launch, so they cannot switch mid-session. To keep
35
+ switching while running, use proxy (Claude Code never sees the 429; the proxy
36
+ resends on another org).
37
+ `;
38
+
39
+ function printUsage(exitCode) {
40
+ process.stdout.write(USAGE);
41
+ return exitCode;
42
+ }
43
+
44
+ module.exports = { USAGE, printUsage };
package/lib/watch.js ADDED
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+ const { spawn } = require('child_process');
3
+ const { resolvePrefer } = require('./config');
4
+ const { probe } = require('./probe');
5
+ const { hhmmss } = require('./time');
6
+
7
+ const sleep = s => new Promise(r => setTimeout(r, s * 1000));
8
+
9
+ // Externalized notification: CLAUDE_ORG_NOTIFY_CMD is run as
10
+ // `<cmd> <title> <message>` via the OS shell. Terminal output always happens.
11
+ function notify(title, message) {
12
+ const cmd = process.env.CLAUDE_ORG_NOTIFY_CMD;
13
+ if (!cmd) {
14
+ console.log(' (CLAUDE_ORG_NOTIFY_CMD 未設定のため端末通知のみ)');
15
+ return Promise.resolve();
16
+ }
17
+ // Quote args ourselves: spawn(shell:true) does not quote on Windows and the
18
+ // title/message contain spaces.
19
+ const q = s => `"${String(s).replace(/(["\\$`])/g, '\\$1')}"`;
20
+ return new Promise(resolve => {
21
+ const child = spawn(`${cmd} ${q(title)} ${q(message)}`, { shell: true, stdio: 'ignore' });
22
+ child.on('error', () => {
23
+ console.log(' (通知コマンドの実行に失敗。端末通知のみ)');
24
+ resolve();
25
+ });
26
+ child.on('close', code => {
27
+ if (code) console.log(' (通知コマンドが失敗。端末通知のみ)');
28
+ resolve();
29
+ });
30
+ });
31
+ }
32
+
33
+ // watch: poll the preferred org until its 5h window recovers, notify once, exit.
34
+ // Re-check interval: (5h_reset - now + 30s), clamped to [min, max]. max is
35
+ // CLAUDE_ORG_WATCH_MAX_SLEEP (default 18000s); min is 60s unless max < 60,
36
+ // in which case min = max (so tests can shrink the loop).
37
+ async function watch() {
38
+ const prefer = resolvePrefer();
39
+ const maxSleep = Number(process.env.CLAUDE_ORG_WATCH_MAX_SLEEP || 18000);
40
+ let minSleep = 60;
41
+ if (maxSleep < minSleep) minSleep = maxSleep;
42
+
43
+ for (;;) {
44
+ const r = await probe(prefer);
45
+ if (r.allowed) {
46
+ console.log(`${hhmmss()} ${prefer} 復活: ${r.line}`);
47
+ console.log(' → claude-org auto --resume で戻れます');
48
+ await notify(`Claude ${prefer} 復活`, `claude-org auto --resume で ${prefer} に戻れます`);
49
+ return 0;
50
+ }
51
+ const nowSec = Math.floor(Date.now() / 1000);
52
+ let wait;
53
+ if (r.reset !== '?' && Number(r.reset) > nowSec) {
54
+ wait = Number(r.reset) - nowSec + 30;
55
+ } else {
56
+ wait = 600;
57
+ }
58
+ wait = Math.min(Math.max(wait, minSleep), maxSleep);
59
+ console.log(`${hhmmss()} ${prefer} 上限中 (${r.line}) → ${wait}s 後に再確認 (reset ${r.resetLocal})`);
60
+ await sleep(wait);
61
+ }
62
+ }
63
+
64
+ module.exports = { watch };
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "claude-org",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Switch Claude Code between multiple Anthropic organizations, with automatic failover on the 5-hour rate limit",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "bin": {
8
+ "claude-org": "bin/claude-org.js",
9
+ "ccorg": "bin/claude-org.js"
10
+ },
11
+ "engines": {
12
+ "node": ">=18"
13
+ },
14
+ "files": [
15
+ "bin",
16
+ "lib",
17
+ "README.md",
18
+ "LICENSE",
19
+ "CHANGELOG.md"
20
+ ],
21
+ "scripts": {
22
+ "test": "node --test"
23
+ },
24
+ "keywords": [
25
+ "claude-code",
26
+ "anthropic",
27
+ "cli",
28
+ "rate-limit",
29
+ "failover"
30
+ ],
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/miyashita337/claude-org.git"
34
+ },
35
+ "homepage": "https://github.com/miyashita337/claude-org",
36
+ "bugs": "https://github.com/miyashita337/claude-org/issues"
37
+ }