claude-nonstop 0.3.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/.env.example +33 -0
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/assets/icon.jpeg +0 -0
- package/assets/screenshot.png +0 -0
- package/bin/claude-nonstop.js +1679 -0
- package/lib/config.js +163 -0
- package/lib/keychain.js +397 -0
- package/lib/platform.js +9 -0
- package/lib/reauth.js +147 -0
- package/lib/runner.js +566 -0
- package/lib/scorer.js +100 -0
- package/lib/service.js +196 -0
- package/lib/session.js +294 -0
- package/lib/tmux.js +95 -0
- package/lib/usage.js +146 -0
- package/package.json +56 -0
- package/remote/channel-manager.cjs +548 -0
- package/remote/hook-notify.cjs +504 -0
- package/remote/load-env.cjs +32 -0
- package/remote/paths.cjs +17 -0
- package/remote/start-webhook.cjs +97 -0
- package/remote/webhook.cjs +228 -0
- package/scripts/postinstall.js +40 -0
- package/slack-manifest.yaml +32 -0
package/lib/usage.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query the Anthropic OAuth usage API.
|
|
3
|
+
*
|
|
4
|
+
* Endpoint: GET https://api.anthropic.com/api/oauth/usage
|
|
5
|
+
* Returns five_hour and seven_day utilization percentages (0-100).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalize a utilization value to a 0-100 percentage.
|
|
12
|
+
* Handles both 0.0-1.0 (fraction) and 0-100 (percentage) formats.
|
|
13
|
+
*/
|
|
14
|
+
export function normalizePercent(value) {
|
|
15
|
+
if (typeof value !== 'number' || isNaN(value)) return 0;
|
|
16
|
+
if (value >= 0 && value <= 1.0) {
|
|
17
|
+
return Math.round(value * 100);
|
|
18
|
+
}
|
|
19
|
+
return Math.round(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check usage for a single account token.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} token - OAuth access token
|
|
26
|
+
* @returns {Promise<{sessionPercent: number, weeklyPercent: number, sessionResetsAt: string|null, weeklyResetsAt: string|null, error: string|null}>}
|
|
27
|
+
*/
|
|
28
|
+
export async function checkUsage(token) {
|
|
29
|
+
try {
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
32
|
+
|
|
33
|
+
const res = await fetch('https://api.anthropic.com/api/oauth/usage', {
|
|
34
|
+
method: 'GET',
|
|
35
|
+
headers: {
|
|
36
|
+
'Authorization': `Bearer ${token}`,
|
|
37
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
38
|
+
'anthropic-version': '2023-06-01',
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
},
|
|
41
|
+
signal: controller.signal,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
clearTimeout(timeoutId);
|
|
45
|
+
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
return {
|
|
48
|
+
sessionPercent: 0,
|
|
49
|
+
weeklyPercent: 0,
|
|
50
|
+
sessionResetsAt: null,
|
|
51
|
+
weeklyResetsAt: null,
|
|
52
|
+
error: `HTTP ${res.status}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const data = await res.json();
|
|
57
|
+
|
|
58
|
+
// New nested format: { five_hour: { utilization: N, resets_at: "..." }, seven_day: { ... } }
|
|
59
|
+
if (data.five_hour !== undefined || data.seven_day !== undefined) {
|
|
60
|
+
return {
|
|
61
|
+
sessionPercent: normalizePercent(data.five_hour?.utilization ?? 0),
|
|
62
|
+
weeklyPercent: normalizePercent(data.seven_day?.utilization ?? 0),
|
|
63
|
+
sessionResetsAt: data.five_hour?.resets_at ?? null,
|
|
64
|
+
weeklyResetsAt: data.seven_day?.resets_at ?? null,
|
|
65
|
+
error: null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Legacy flat format: { five_hour_utilization: 0.72, ... }
|
|
70
|
+
return {
|
|
71
|
+
sessionPercent: normalizePercent(data.five_hour_utilization ?? 0),
|
|
72
|
+
weeklyPercent: normalizePercent(data.seven_day_utilization ?? 0),
|
|
73
|
+
sessionResetsAt: data.five_hour_reset_at ?? null,
|
|
74
|
+
weeklyResetsAt: data.seven_day_reset_at ?? null,
|
|
75
|
+
error: null,
|
|
76
|
+
};
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return {
|
|
79
|
+
sessionPercent: 0,
|
|
80
|
+
weeklyPercent: 0,
|
|
81
|
+
sessionResetsAt: null,
|
|
82
|
+
weeklyResetsAt: null,
|
|
83
|
+
error: error.name === 'AbortError' ? 'timeout' : error.message,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Fetch the account profile (name, email) from the OAuth profile API.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} token - OAuth access token
|
|
92
|
+
* @returns {Promise<{name: string|null, email: string|null}>}
|
|
93
|
+
*/
|
|
94
|
+
export async function fetchProfile(token) {
|
|
95
|
+
try {
|
|
96
|
+
const controller = new AbortController();
|
|
97
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
98
|
+
|
|
99
|
+
const res = await fetch('https://api.anthropic.com/api/oauth/profile', {
|
|
100
|
+
method: 'GET',
|
|
101
|
+
headers: {
|
|
102
|
+
'Authorization': `Bearer ${token}`,
|
|
103
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
104
|
+
'anthropic-version': '2023-06-01',
|
|
105
|
+
},
|
|
106
|
+
signal: controller.signal,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
clearTimeout(timeoutId);
|
|
110
|
+
|
|
111
|
+
if (!res.ok) return { name: null, email: null, orgId: null, orgName: null, orgType: null, accountUuid: null };
|
|
112
|
+
|
|
113
|
+
const data = await res.json();
|
|
114
|
+
return {
|
|
115
|
+
name: data.account?.full_name || data.account?.display_name || null,
|
|
116
|
+
email: data.account?.email || null,
|
|
117
|
+
// Organization-level identity. One email can belong to multiple orgs —
|
|
118
|
+
// e.g. an enterprise email that, per company policy, has both an
|
|
119
|
+
// enterprise org and a personal Max plan org — each with its own quota
|
|
120
|
+
// and OAuth token. organization.uuid is globally unique, so it — not
|
|
121
|
+
// email — is the correct identity key for account switching.
|
|
122
|
+
orgId: data.organization?.uuid || null,
|
|
123
|
+
orgName: data.organization?.name || null,
|
|
124
|
+
orgType: data.organization?.organization_type || null,
|
|
125
|
+
accountUuid: data.account?.uuid || null,
|
|
126
|
+
};
|
|
127
|
+
} catch {
|
|
128
|
+
return { name: null, email: null, orgId: null, orgName: null, orgType: null, accountUuid: null };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check usage for all accounts in parallel.
|
|
134
|
+
*
|
|
135
|
+
* @param {Array<{name: string, configDir: string, token: string}>} accounts
|
|
136
|
+
* @returns {Promise<Array<{name: string, configDir: string, token: string, usage: object}>>}
|
|
137
|
+
*/
|
|
138
|
+
export async function checkAllUsage(accounts) {
|
|
139
|
+
const results = await Promise.all(
|
|
140
|
+
accounts.map(async (account) => {
|
|
141
|
+
const usage = await checkUsage(account.token);
|
|
142
|
+
return { ...account, usage };
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
return results;
|
|
146
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-nonstop",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Automatic Claude Code account switching on rate limits + Slack remote access",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-nonstop": "bin/claude-nonstop.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"check": "node --check lib/*.js && node --check remote/*.cjs && node --check bin/claude-nonstop.js && node --check scripts/postinstall.js",
|
|
11
|
+
"test": "node --test 'test/unit/**/*.test.js' 'test/unit/**/*.test.cjs'",
|
|
12
|
+
"test:integration": "node --test 'test/integration/**/*.test.js'",
|
|
13
|
+
"test:security": "node --test 'test/security/**/*.test.js'",
|
|
14
|
+
"test:all": "node --test 'test/**/*.test.js' 'test/**/*.test.cjs'",
|
|
15
|
+
"webhook": "node remote/start-webhook.cjs",
|
|
16
|
+
"postinstall": "node scripts/postinstall.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"assets/",
|
|
20
|
+
"bin/",
|
|
21
|
+
"lib/",
|
|
22
|
+
"remote/",
|
|
23
|
+
"scripts/",
|
|
24
|
+
"slack-manifest.yaml",
|
|
25
|
+
".env.example"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22.0.0"
|
|
29
|
+
},
|
|
30
|
+
"author": "Rahul Chandrasekaran",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/rchaz/claude-nonstop.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/rchaz/claude-nonstop/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/rchaz/claude-nonstop#readme",
|
|
40
|
+
"keywords": [
|
|
41
|
+
"claude",
|
|
42
|
+
"claude-code",
|
|
43
|
+
"rate-limit",
|
|
44
|
+
"account-switching",
|
|
45
|
+
"multi-account",
|
|
46
|
+
"slack",
|
|
47
|
+
"remote-access",
|
|
48
|
+
"tmux",
|
|
49
|
+
"anthropic"
|
|
50
|
+
],
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@slack/bolt": "^4.7.3",
|
|
53
|
+
"@slack/web-api": "^7.16.0",
|
|
54
|
+
"node-pty": "^1.2.0-beta.13"
|
|
55
|
+
}
|
|
56
|
+
}
|