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/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
+ }