sidekick-shared 0.13.7 → 0.14.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/README.md +13 -1
- package/dist/accounts.d.ts +35 -0
- package/dist/accounts.js +325 -0
- package/dist/credentialIO.d.ts +28 -0
- package/dist/credentialIO.js +109 -0
- package/dist/credentials.d.ts +4 -3
- package/dist/credentials.js +11 -49
- package/dist/index.d.ts +5 -0
- package/dist/index.js +19 -1
- package/dist/quota.d.ts +6 -0
- package/dist/quota.js +38 -5
- package/dist/quotaPresentation.d.ts +10 -0
- package/dist/quotaPresentation.js +88 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,10 +77,22 @@ import { fetchQuota, readClaudeMaxCredentials } from 'sidekick-shared';
|
|
|
77
77
|
const creds = readClaudeMaxCredentials();
|
|
78
78
|
if (creds) {
|
|
79
79
|
const quota = await fetchQuota(creds.accessToken);
|
|
80
|
-
|
|
80
|
+
if (quota.available) {
|
|
81
|
+
console.log(`5-hour utilization: ${quota.fiveHour.utilization}%`);
|
|
82
|
+
} else {
|
|
83
|
+
console.log(quota.failureKind, quota.httpStatus, quota.retryAfterMs);
|
|
84
|
+
}
|
|
81
85
|
}
|
|
82
86
|
```
|
|
83
87
|
|
|
88
|
+
Unavailable quota responses remain non-throwing and may include:
|
|
89
|
+
|
|
90
|
+
- `failureKind`: `auth | network | rate_limit | server | unknown`
|
|
91
|
+
- `httpStatus`: HTTP response status when available
|
|
92
|
+
- `retryAfterMs`: retry delay in milliseconds for `429` responses when the API provides `Retry-After`
|
|
93
|
+
|
|
94
|
+
For first-party style messaging, `describeQuotaFailure()` maps unavailable quota states to stable alert keys plus display-ready severity/title/message/detail fields for CLI and VS Code consumers.
|
|
95
|
+
|
|
84
96
|
## Building
|
|
85
97
|
|
|
86
98
|
```bash
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-account management for Claude Max subscriptions.
|
|
3
|
+
*
|
|
4
|
+
* Handles saving, listing, switching, and removing Claude accounts.
|
|
5
|
+
* Both the VS Code extension and CLI consume this module.
|
|
6
|
+
*/
|
|
7
|
+
export interface AccountEntry {
|
|
8
|
+
uuid: string;
|
|
9
|
+
email: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
addedAt: string;
|
|
12
|
+
}
|
|
13
|
+
export interface AccountRegistry {
|
|
14
|
+
version: 1;
|
|
15
|
+
activeAccountUuid: string | null;
|
|
16
|
+
accounts: AccountEntry[];
|
|
17
|
+
}
|
|
18
|
+
export interface ActiveAccountInfo {
|
|
19
|
+
email: string;
|
|
20
|
+
uuid: string;
|
|
21
|
+
}
|
|
22
|
+
export interface AccountManagerResult {
|
|
23
|
+
success: boolean;
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function getAccountsDir(): string;
|
|
27
|
+
export declare function readAccountRegistry(): AccountRegistry | null;
|
|
28
|
+
export declare function writeAccountRegistry(registry: AccountRegistry): void;
|
|
29
|
+
export declare function readActiveClaudeAccount(): ActiveAccountInfo | null;
|
|
30
|
+
export declare function addCurrentAccount(label?: string): AccountManagerResult;
|
|
31
|
+
export declare function switchToAccount(uuid: string): AccountManagerResult;
|
|
32
|
+
export declare function removeAccount(uuid: string): AccountManagerResult;
|
|
33
|
+
export declare function listAccounts(): AccountEntry[];
|
|
34
|
+
export declare function getActiveAccount(): AccountEntry | null;
|
|
35
|
+
export declare function isMultiAccountEnabled(): boolean;
|
package/dist/accounts.js
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Multi-account management for Claude Max subscriptions.
|
|
4
|
+
*
|
|
5
|
+
* Handles saving, listing, switching, and removing Claude accounts.
|
|
6
|
+
* Both the VS Code extension and CLI consume this module.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.getAccountsDir = getAccountsDir;
|
|
43
|
+
exports.readAccountRegistry = readAccountRegistry;
|
|
44
|
+
exports.writeAccountRegistry = writeAccountRegistry;
|
|
45
|
+
exports.readActiveClaudeAccount = readActiveClaudeAccount;
|
|
46
|
+
exports.addCurrentAccount = addCurrentAccount;
|
|
47
|
+
exports.switchToAccount = switchToAccount;
|
|
48
|
+
exports.removeAccount = removeAccount;
|
|
49
|
+
exports.listAccounts = listAccounts;
|
|
50
|
+
exports.getActiveAccount = getActiveAccount;
|
|
51
|
+
exports.isMultiAccountEnabled = isMultiAccountEnabled;
|
|
52
|
+
const fs = __importStar(require("fs"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
const os = __importStar(require("os"));
|
|
55
|
+
const paths_1 = require("./paths");
|
|
56
|
+
const credentialIO_1 = require("./credentialIO");
|
|
57
|
+
// ── Paths ────────────────────────────────────────────────────────────────
|
|
58
|
+
function getClaudeDir() {
|
|
59
|
+
return path.join(os.homedir(), '.claude');
|
|
60
|
+
}
|
|
61
|
+
function getClaudeConfigPath() {
|
|
62
|
+
// Primary: ~/.claude/.claude.json
|
|
63
|
+
const primary = path.join(getClaudeDir(), '.claude.json');
|
|
64
|
+
try {
|
|
65
|
+
const data = JSON.parse(fs.readFileSync(primary, 'utf8'));
|
|
66
|
+
if (data?.oauthAccount)
|
|
67
|
+
return primary;
|
|
68
|
+
}
|
|
69
|
+
catch { /* fall through */ }
|
|
70
|
+
// Fallback: ~/.claude.json (some Claude Code versions store config here)
|
|
71
|
+
return path.join(os.homedir(), '.claude.json');
|
|
72
|
+
}
|
|
73
|
+
function getAccountsDir() {
|
|
74
|
+
return path.join((0, paths_1.getConfigDir)(), 'accounts');
|
|
75
|
+
}
|
|
76
|
+
function getCredentialsDir() {
|
|
77
|
+
return path.join(getAccountsDir(), 'credentials');
|
|
78
|
+
}
|
|
79
|
+
function getConfigsDir() {
|
|
80
|
+
return path.join(getAccountsDir(), 'configs');
|
|
81
|
+
}
|
|
82
|
+
function getRegistryPath() {
|
|
83
|
+
return path.join(getAccountsDir(), 'accounts.json');
|
|
84
|
+
}
|
|
85
|
+
// ── Directory bootstrap ──────────────────────────────────────────────────
|
|
86
|
+
function ensureDirs() {
|
|
87
|
+
for (const dir of [getAccountsDir(), getCredentialsDir(), getConfigsDir()]) {
|
|
88
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// ── Atomic file write ────────────────────────────────────────────────────
|
|
92
|
+
function atomicWriteJson(filePath, data, mode = 0o600) {
|
|
93
|
+
const tmp = filePath + '.tmp';
|
|
94
|
+
const json = JSON.stringify(data, null, 2);
|
|
95
|
+
// Validate we can re-parse before writing
|
|
96
|
+
JSON.parse(json);
|
|
97
|
+
fs.writeFileSync(tmp, json, { encoding: 'utf8', mode });
|
|
98
|
+
fs.renameSync(tmp, filePath);
|
|
99
|
+
}
|
|
100
|
+
// ── Registry operations ──────────────────────────────────────────────────
|
|
101
|
+
function readAccountRegistry() {
|
|
102
|
+
try {
|
|
103
|
+
const content = fs.readFileSync(getRegistryPath(), 'utf8');
|
|
104
|
+
const parsed = JSON.parse(content);
|
|
105
|
+
if (parsed?.version !== 1 || !Array.isArray(parsed.accounts))
|
|
106
|
+
return null;
|
|
107
|
+
return parsed;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function writeAccountRegistry(registry) {
|
|
114
|
+
ensureDirs();
|
|
115
|
+
atomicWriteJson(getRegistryPath(), registry);
|
|
116
|
+
}
|
|
117
|
+
// ── Read active Claude account ───────────────────────────────────────────
|
|
118
|
+
function readActiveClaudeAccount() {
|
|
119
|
+
try {
|
|
120
|
+
const content = fs.readFileSync(getClaudeConfigPath(), 'utf8');
|
|
121
|
+
const parsed = JSON.parse(content);
|
|
122
|
+
const email = parsed?.oauthAccount?.emailAddress;
|
|
123
|
+
const uuid = parsed?.oauthAccount?.accountUuid;
|
|
124
|
+
if (!email || !uuid)
|
|
125
|
+
return null;
|
|
126
|
+
return { email, uuid };
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// ── Add current account ──────────────────────────────────────────────────
|
|
133
|
+
function addCurrentAccount(label) {
|
|
134
|
+
// 1. Read current Claude identity
|
|
135
|
+
const active = readActiveClaudeAccount();
|
|
136
|
+
if (!active) {
|
|
137
|
+
return { success: false, error: 'No active Claude account found. Sign in with `claude` first.' };
|
|
138
|
+
}
|
|
139
|
+
// 2. Read current credentials (platform-aware: Keychain on macOS, file on Linux)
|
|
140
|
+
const credBlob = (0, credentialIO_1.readActiveCredentials)();
|
|
141
|
+
if (!credBlob) {
|
|
142
|
+
return { success: false, error: 'Could not read Claude credentials.' };
|
|
143
|
+
}
|
|
144
|
+
// 3. Read oauthAccount section from .claude.json
|
|
145
|
+
let configBlob;
|
|
146
|
+
try {
|
|
147
|
+
const raw = JSON.parse(fs.readFileSync(getClaudeConfigPath(), 'utf8'));
|
|
148
|
+
configBlob = raw.oauthAccount;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return { success: false, error: 'Could not read Claude config file.' };
|
|
152
|
+
}
|
|
153
|
+
ensureDirs();
|
|
154
|
+
// 4. Load or create registry
|
|
155
|
+
const registry = readAccountRegistry() ?? {
|
|
156
|
+
version: 1,
|
|
157
|
+
activeAccountUuid: null,
|
|
158
|
+
accounts: [],
|
|
159
|
+
};
|
|
160
|
+
// 5. Check if account already exists — update credentials + label
|
|
161
|
+
const existing = registry.accounts.find(a => a.uuid === active.uuid);
|
|
162
|
+
if (existing) {
|
|
163
|
+
if (label !== undefined)
|
|
164
|
+
existing.label = label;
|
|
165
|
+
atomicWriteJson(path.join(getCredentialsDir(), `${active.uuid}.credentials.json`), credBlob);
|
|
166
|
+
atomicWriteJson(path.join(getConfigsDir(), `${active.uuid}.config.json`), configBlob);
|
|
167
|
+
registry.activeAccountUuid = active.uuid;
|
|
168
|
+
writeAccountRegistry(registry);
|
|
169
|
+
return { success: true };
|
|
170
|
+
}
|
|
171
|
+
// 6. New account — back up credentials
|
|
172
|
+
atomicWriteJson(path.join(getCredentialsDir(), `${active.uuid}.credentials.json`), credBlob);
|
|
173
|
+
atomicWriteJson(path.join(getConfigsDir(), `${active.uuid}.config.json`), configBlob);
|
|
174
|
+
// 7. Add entry
|
|
175
|
+
registry.accounts.push({
|
|
176
|
+
uuid: active.uuid,
|
|
177
|
+
email: active.email,
|
|
178
|
+
label,
|
|
179
|
+
addedAt: new Date().toISOString(),
|
|
180
|
+
});
|
|
181
|
+
registry.activeAccountUuid = active.uuid;
|
|
182
|
+
writeAccountRegistry(registry);
|
|
183
|
+
return { success: true };
|
|
184
|
+
}
|
|
185
|
+
// ── Switch to account ────────────────────────────────────────────────────
|
|
186
|
+
function switchToAccount(uuid) {
|
|
187
|
+
const registry = readAccountRegistry();
|
|
188
|
+
if (!registry) {
|
|
189
|
+
return { success: false, error: 'No account registry found. Save an account first.' };
|
|
190
|
+
}
|
|
191
|
+
const target = registry.accounts.find(a => a.uuid === uuid);
|
|
192
|
+
if (!target) {
|
|
193
|
+
return { success: false, error: `Account ${uuid} not found in registry.` };
|
|
194
|
+
}
|
|
195
|
+
if (registry.activeAccountUuid === uuid) {
|
|
196
|
+
return { success: true }; // Already active
|
|
197
|
+
}
|
|
198
|
+
// 1. Back up current credentials + config (for the currently-active account)
|
|
199
|
+
let originalCreds = null;
|
|
200
|
+
let originalConfig = null;
|
|
201
|
+
originalCreds = (0, credentialIO_1.readActiveCredentials)();
|
|
202
|
+
try {
|
|
203
|
+
originalConfig = fs.readFileSync(getClaudeConfigPath(), 'utf8');
|
|
204
|
+
}
|
|
205
|
+
catch { /* no existing config to back up */ }
|
|
206
|
+
// Save current account's backup if we know who's active
|
|
207
|
+
const currentActive = readActiveClaudeAccount();
|
|
208
|
+
if (currentActive) {
|
|
209
|
+
if (originalCreds) {
|
|
210
|
+
try {
|
|
211
|
+
atomicWriteJson(path.join(getCredentialsDir(), `${currentActive.uuid}.credentials.json`), originalCreds);
|
|
212
|
+
}
|
|
213
|
+
catch { /* best effort */ }
|
|
214
|
+
}
|
|
215
|
+
if (originalConfig) {
|
|
216
|
+
try {
|
|
217
|
+
const parsed = JSON.parse(originalConfig);
|
|
218
|
+
if (parsed.oauthAccount) {
|
|
219
|
+
atomicWriteJson(path.join(getConfigsDir(), `${currentActive.uuid}.config.json`), parsed.oauthAccount);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch { /* best effort */ }
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// 2. Read target account's backed-up credentials + config
|
|
226
|
+
let targetCreds;
|
|
227
|
+
let targetOauthAccount;
|
|
228
|
+
try {
|
|
229
|
+
targetCreds = JSON.parse(fs.readFileSync(path.join(getCredentialsDir(), `${uuid}.credentials.json`), 'utf8'));
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return { success: false, error: `Backed-up credentials for ${target.email} not found.` };
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
targetOauthAccount = JSON.parse(fs.readFileSync(path.join(getConfigsDir(), `${uuid}.config.json`), 'utf8'));
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return { success: false, error: `Backed-up config for ${target.email} not found.` };
|
|
239
|
+
}
|
|
240
|
+
// 3. Write target credentials (platform-aware: Keychain on macOS, file on Linux)
|
|
241
|
+
try {
|
|
242
|
+
(0, credentialIO_1.writeActiveCredentials)(targetCreds);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
// Rollback: restore original credentials
|
|
246
|
+
if (originalCreds) {
|
|
247
|
+
try {
|
|
248
|
+
(0, credentialIO_1.writeActiveCredentials)(originalCreds);
|
|
249
|
+
}
|
|
250
|
+
catch { /* rollback failed */ }
|
|
251
|
+
}
|
|
252
|
+
return { success: false, error: `Failed to write credentials: ${err}` };
|
|
253
|
+
}
|
|
254
|
+
// 4. Merge target oauthAccount into current .claude.json
|
|
255
|
+
try {
|
|
256
|
+
let configObj = {};
|
|
257
|
+
try {
|
|
258
|
+
configObj = JSON.parse(fs.readFileSync(getClaudeConfigPath(), 'utf8'));
|
|
259
|
+
}
|
|
260
|
+
catch { /* file may not exist yet */ }
|
|
261
|
+
configObj.oauthAccount = targetOauthAccount;
|
|
262
|
+
atomicWriteJson(getClaudeConfigPath(), configObj);
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
// Rollback: restore original credentials and config
|
|
266
|
+
if (originalCreds) {
|
|
267
|
+
try {
|
|
268
|
+
(0, credentialIO_1.writeActiveCredentials)(originalCreds);
|
|
269
|
+
}
|
|
270
|
+
catch { /* rollback failed */ }
|
|
271
|
+
}
|
|
272
|
+
if (originalConfig) {
|
|
273
|
+
try {
|
|
274
|
+
fs.writeFileSync(getClaudeConfigPath(), originalConfig);
|
|
275
|
+
}
|
|
276
|
+
catch { /* rollback failed */ }
|
|
277
|
+
}
|
|
278
|
+
return { success: false, error: `Failed to write config: ${err}` };
|
|
279
|
+
}
|
|
280
|
+
// 5. Update registry
|
|
281
|
+
registry.activeAccountUuid = uuid;
|
|
282
|
+
writeAccountRegistry(registry);
|
|
283
|
+
return { success: true };
|
|
284
|
+
}
|
|
285
|
+
// ── Remove account ───────────────────────────────────────────────────────
|
|
286
|
+
function removeAccount(uuid) {
|
|
287
|
+
const registry = readAccountRegistry();
|
|
288
|
+
if (!registry) {
|
|
289
|
+
return { success: false, error: 'No account registry found.' };
|
|
290
|
+
}
|
|
291
|
+
const idx = registry.accounts.findIndex(a => a.uuid === uuid);
|
|
292
|
+
if (idx === -1) {
|
|
293
|
+
return { success: false, error: `Account ${uuid} not found.` };
|
|
294
|
+
}
|
|
295
|
+
// Remove backed-up files
|
|
296
|
+
try {
|
|
297
|
+
fs.unlinkSync(path.join(getCredentialsDir(), `${uuid}.credentials.json`));
|
|
298
|
+
}
|
|
299
|
+
catch { /* ok */ }
|
|
300
|
+
try {
|
|
301
|
+
fs.unlinkSync(path.join(getConfigsDir(), `${uuid}.config.json`));
|
|
302
|
+
}
|
|
303
|
+
catch { /* ok */ }
|
|
304
|
+
// Remove from registry
|
|
305
|
+
registry.accounts.splice(idx, 1);
|
|
306
|
+
if (registry.activeAccountUuid === uuid) {
|
|
307
|
+
registry.activeAccountUuid = registry.accounts[0]?.uuid ?? null;
|
|
308
|
+
}
|
|
309
|
+
writeAccountRegistry(registry);
|
|
310
|
+
return { success: true };
|
|
311
|
+
}
|
|
312
|
+
// ── Query helpers ────────────────────────────────────────────────────────
|
|
313
|
+
function listAccounts() {
|
|
314
|
+
return readAccountRegistry()?.accounts ?? [];
|
|
315
|
+
}
|
|
316
|
+
function getActiveAccount() {
|
|
317
|
+
const registry = readAccountRegistry();
|
|
318
|
+
if (!registry?.activeAccountUuid)
|
|
319
|
+
return null;
|
|
320
|
+
return registry.accounts.find(a => a.uuid === registry.activeAccountUuid) ?? null;
|
|
321
|
+
}
|
|
322
|
+
function isMultiAccountEnabled() {
|
|
323
|
+
const registry = readAccountRegistry();
|
|
324
|
+
return (registry?.accounts.length ?? 0) >= 1;
|
|
325
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-aware credential I/O for Claude Code.
|
|
3
|
+
*
|
|
4
|
+
* On macOS, Claude Code stores OAuth credentials in the system Keychain
|
|
5
|
+
* (service "Claude Code-credentials"). On Linux/WSL/Windows, credentials
|
|
6
|
+
* live in ~/.claude/.credentials.json.
|
|
7
|
+
*
|
|
8
|
+
* Both accounts.ts and credentials.ts consume this module so that all
|
|
9
|
+
* credential access is platform-correct in one place.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Read the active Claude Code credentials as a parsed object.
|
|
13
|
+
*
|
|
14
|
+
* - macOS: reads from system Keychain via the `security` CLI
|
|
15
|
+
* - Linux / WSL / Windows: reads `~/.claude/.credentials.json`
|
|
16
|
+
*
|
|
17
|
+
* Returns `null` when credentials are absent or unreadable.
|
|
18
|
+
*/
|
|
19
|
+
export declare function readActiveCredentials(): unknown;
|
|
20
|
+
/**
|
|
21
|
+
* Write credentials to the active Claude Code credential store.
|
|
22
|
+
*
|
|
23
|
+
* - macOS: writes to system Keychain via the `security` CLI
|
|
24
|
+
* - Linux / WSL / Windows: atomic-writes `~/.claude/.credentials.json`
|
|
25
|
+
*
|
|
26
|
+
* Throws on failure.
|
|
27
|
+
*/
|
|
28
|
+
export declare function writeActiveCredentials(credentials: unknown): void;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Platform-aware credential I/O for Claude Code.
|
|
4
|
+
*
|
|
5
|
+
* On macOS, Claude Code stores OAuth credentials in the system Keychain
|
|
6
|
+
* (service "Claude Code-credentials"). On Linux/WSL/Windows, credentials
|
|
7
|
+
* live in ~/.claude/.credentials.json.
|
|
8
|
+
*
|
|
9
|
+
* Both accounts.ts and credentials.ts consume this module so that all
|
|
10
|
+
* credential access is platform-correct in one place.
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.readActiveCredentials = readActiveCredentials;
|
|
47
|
+
exports.writeActiveCredentials = writeActiveCredentials;
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const os = __importStar(require("os"));
|
|
51
|
+
const child_process_1 = require("child_process");
|
|
52
|
+
const KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
53
|
+
function getCredentialsFilePath() {
|
|
54
|
+
return path.join(os.homedir(), '.claude', '.credentials.json');
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Read the active Claude Code credentials as a parsed object.
|
|
58
|
+
*
|
|
59
|
+
* - macOS: reads from system Keychain via the `security` CLI
|
|
60
|
+
* - Linux / WSL / Windows: reads `~/.claude/.credentials.json`
|
|
61
|
+
*
|
|
62
|
+
* Returns `null` when credentials are absent or unreadable.
|
|
63
|
+
*/
|
|
64
|
+
function readActiveCredentials() {
|
|
65
|
+
if (process.platform === 'darwin') {
|
|
66
|
+
try {
|
|
67
|
+
const raw = (0, child_process_1.execFileSync)('security', [
|
|
68
|
+
'find-generic-password', '-s', KEYCHAIN_SERVICE, '-w',
|
|
69
|
+
], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
70
|
+
return JSON.parse(raw.trim());
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Linux / WSL / Windows — file-based
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(fs.readFileSync(getCredentialsFilePath(), 'utf8'));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Write credentials to the active Claude Code credential store.
|
|
86
|
+
*
|
|
87
|
+
* - macOS: writes to system Keychain via the `security` CLI
|
|
88
|
+
* - Linux / WSL / Windows: atomic-writes `~/.claude/.credentials.json`
|
|
89
|
+
*
|
|
90
|
+
* Throws on failure.
|
|
91
|
+
*/
|
|
92
|
+
function writeActiveCredentials(credentials) {
|
|
93
|
+
const json = JSON.stringify(credentials);
|
|
94
|
+
JSON.parse(json); // validate round-trip
|
|
95
|
+
if (process.platform === 'darwin') {
|
|
96
|
+
(0, child_process_1.execFileSync)('security', [
|
|
97
|
+
'add-generic-password', '-U',
|
|
98
|
+
'-s', KEYCHAIN_SERVICE,
|
|
99
|
+
'-a', process.env.USER || 'user',
|
|
100
|
+
'-w', json,
|
|
101
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Linux / WSL / Windows — file-based
|
|
105
|
+
const credPath = getCredentialsFilePath();
|
|
106
|
+
const tmp = credPath + '.tmp';
|
|
107
|
+
fs.writeFileSync(tmp, json, { encoding: 'utf8', mode: 0o600 });
|
|
108
|
+
fs.renameSync(tmp, credPath);
|
|
109
|
+
}
|
package/dist/credentials.d.ts
CHANGED
|
@@ -11,10 +11,11 @@ export interface ClaudeMaxCredentials {
|
|
|
11
11
|
subscriptionType?: string;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
* Reads Claude Max OAuth credentials
|
|
14
|
+
* Reads Claude Max OAuth credentials.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* On macOS reads from system Keychain; on Linux/Windows reads from
|
|
17
|
+
* `~/.claude/.credentials.json`. Returns `null` if credentials are
|
|
18
|
+
* absent, the token is missing, or the token is expired. Never throws.
|
|
18
19
|
*/
|
|
19
20
|
export declare function readClaudeMaxCredentials(): Promise<ClaudeMaxCredentials | null>;
|
|
20
21
|
/**
|
package/dist/credentials.js
CHANGED
|
@@ -4,59 +4,23 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Shared by sidekick-cli, sidekick-vscode, and any external consumer.
|
|
6
6
|
*/
|
|
7
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
-
if (k2 === undefined) k2 = k;
|
|
9
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
-
}
|
|
13
|
-
Object.defineProperty(o, k2, desc);
|
|
14
|
-
}) : (function(o, m, k, k2) {
|
|
15
|
-
if (k2 === undefined) k2 = k;
|
|
16
|
-
o[k2] = m[k];
|
|
17
|
-
}));
|
|
18
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
-
}) : function(o, v) {
|
|
21
|
-
o["default"] = v;
|
|
22
|
-
});
|
|
23
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
-
var ownKeys = function(o) {
|
|
25
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
-
var ar = [];
|
|
27
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
-
return ar;
|
|
29
|
-
};
|
|
30
|
-
return ownKeys(o);
|
|
31
|
-
};
|
|
32
|
-
return function (mod) {
|
|
33
|
-
if (mod && mod.__esModule) return mod;
|
|
34
|
-
var result = {};
|
|
35
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
-
__setModuleDefault(result, mod);
|
|
37
|
-
return result;
|
|
38
|
-
};
|
|
39
|
-
})();
|
|
40
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
8
|
exports.readClaudeMaxCredentials = readClaudeMaxCredentials;
|
|
42
9
|
exports.readClaudeMaxAccessTokenSync = readClaudeMaxAccessTokenSync;
|
|
43
|
-
const
|
|
44
|
-
const path = __importStar(require("path"));
|
|
45
|
-
const os = __importStar(require("os"));
|
|
10
|
+
const credentialIO_1 = require("./credentialIO");
|
|
46
11
|
/**
|
|
47
|
-
* Reads Claude Max OAuth credentials
|
|
12
|
+
* Reads Claude Max OAuth credentials.
|
|
48
13
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
14
|
+
* On macOS reads from system Keychain; on Linux/Windows reads from
|
|
15
|
+
* `~/.claude/.credentials.json`. Returns `null` if credentials are
|
|
16
|
+
* absent, the token is missing, or the token is expired. Never throws.
|
|
51
17
|
*/
|
|
52
18
|
async function readClaudeMaxCredentials() {
|
|
53
|
-
const credPath = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
54
19
|
try {
|
|
55
|
-
|
|
20
|
+
const raw = (0, credentialIO_1.readActiveCredentials)();
|
|
21
|
+
if (!raw)
|
|
56
22
|
return null;
|
|
57
|
-
const
|
|
58
|
-
const parsed = JSON.parse(content);
|
|
59
|
-
const oauth = parsed?.claudeAiOauth;
|
|
23
|
+
const oauth = raw.claudeAiOauth;
|
|
60
24
|
if (!oauth?.accessToken)
|
|
61
25
|
return null;
|
|
62
26
|
if (oauth.expiresAt && Date.now() > oauth.expiresAt)
|
|
@@ -77,13 +41,11 @@ async function readClaudeMaxCredentials() {
|
|
|
77
41
|
* Synchronous convenience — returns just the access token or `null`.
|
|
78
42
|
*/
|
|
79
43
|
function readClaudeMaxAccessTokenSync() {
|
|
80
|
-
const credPath = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
81
44
|
try {
|
|
82
|
-
|
|
45
|
+
const raw = (0, credentialIO_1.readActiveCredentials)();
|
|
46
|
+
if (!raw)
|
|
83
47
|
return null;
|
|
84
|
-
const
|
|
85
|
-
const parsed = JSON.parse(content);
|
|
86
|
-
const oauth = parsed?.claudeAiOauth;
|
|
48
|
+
const oauth = raw.claudeAiOauth;
|
|
87
49
|
if (!oauth?.accessToken)
|
|
88
50
|
return null;
|
|
89
51
|
if (oauth.expiresAt && Date.now() > oauth.expiresAt)
|
package/dist/index.d.ts
CHANGED
|
@@ -79,9 +79,14 @@ export { PatternExtractor } from './aggregation/PatternExtractor';
|
|
|
79
79
|
export type { PatternCluster, SerializedPatternState } from './aggregation/PatternExtractor';
|
|
80
80
|
export { generateHtmlReport, parseTranscript, openInBrowser } from './report';
|
|
81
81
|
export type { TranscriptContentBlock, TranscriptEntry, HtmlReportOptions } from './report';
|
|
82
|
+
export { readActiveCredentials, writeActiveCredentials } from './credentialIO';
|
|
82
83
|
export { readClaudeMaxCredentials, readClaudeMaxAccessTokenSync } from './credentials';
|
|
83
84
|
export type { ClaudeMaxCredentials } from './credentials';
|
|
85
|
+
export { getAccountsDir, readAccountRegistry, writeAccountRegistry, readActiveClaudeAccount, addCurrentAccount, switchToAccount, removeAccount, listAccounts, getActiveAccount, isMultiAccountEnabled, } from './accounts';
|
|
86
|
+
export type { AccountEntry, AccountRegistry, ActiveAccountInfo, AccountManagerResult } from './accounts';
|
|
84
87
|
export { fetchQuota } from './quota';
|
|
85
88
|
export type { QuotaWindow, QuotaState } from './quota';
|
|
89
|
+
export { describeQuotaFailure } from './quotaPresentation';
|
|
90
|
+
export type { QuotaFailureDescriptor } from './quotaPresentation';
|
|
86
91
|
export { fetchProviderStatus } from './providerStatus';
|
|
87
92
|
export type { ProviderStatusState } from './providerStatus';
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.findActiveClaudeSession = exports.discoverSessionDirectory = exports.getClaudeSessionDirectory = exports.encodeClaudeWorkspacePath = exports.detectSessionActivity = exports.extractTaskInfo = exports.scanSubagentDir = exports.normalizeCodexToolInput = exports.normalizeCodexToolName = exports.extractPatchFilePaths = exports.CodexRolloutParser = exports.parseDbPartData = exports.parseDbMessageData = exports.convertOpenCodeMessage = exports.detectPlanModeFromText = exports.normalizeToolInput = exports.normalizeToolName = exports.TRUNCATION_PATTERNS = exports.JsonlParser = exports.CodexProvider = exports.OpenCodeProvider = exports.ClaudeCodeProvider = exports.getAllDetectedProviders = exports.detectProvider = exports.readClaudeCodePlanFiles = exports.getPlanAnalytics = exports.writePlans = exports.getLatestPlan = exports.readPlans = exports.readLatestHandoff = exports.readHistory = exports.readNotes = exports.readDecisions = exports.readTasks = exports.getProjectSlugRaw = exports.getProjectSlug = exports.encodeWorkspacePath = exports.getGlobalDataPath = exports.getProjectDataPath = exports.getConfigDir = exports.MAX_PLANS_PER_PROJECT = exports.PLAN_SCHEMA_VERSION = exports.createEmptyTokenTotals = exports.HISTORICAL_DATA_SCHEMA_VERSION = exports.STALENESS_THRESHOLDS = exports.IMPORTANCE_DECAY_FACTORS = exports.KNOWLEDGE_NOTE_SCHEMA_VERSION = exports.DECISION_LOG_SCHEMA_VERSION = exports.normalizeTaskStatus = exports.TASK_PERSISTENCE_SCHEMA_VERSION = void 0;
|
|
7
7
|
exports.HeatmapTracker = exports.FrequencyTracker = exports.getSnapshotPath = exports.isSnapshotValid = exports.deleteSnapshot = exports.loadSnapshot = exports.saveSnapshot = exports.parseTodoDependencies = exports.EventAggregator = exports.getRandomPhrase = exports.ALL_PHRASES = exports.HIGHLIGHT_CSS = exports.clearHighlightCache = exports.highlightEvent = exports.formatSessionJson = exports.formatSessionMarkdown = exports.formatSessionText = exports.classifyNoise = exports.shouldMergeWithPrevious = exports.classifyFollowEvent = exports.classifyMessage = exports.getSoftNoiseReason = exports.isHardNoiseFollowEvent = exports.isHardNoise = exports.formatToolSummary = exports.toFollowEvents = exports.createWatcher = exports.parseChangelog = exports.extractProposedPlanShared = exports.parsePlanMarkdownShared = exports.PlanExtractor = exports.composeContext = exports.FilterEngine = exports.searchSessions = exports.CodexDatabase = exports.OpenCodeDatabase = exports.discoverDebugLogs = exports.collapseDuplicates = exports.filterByLevel = exports.parseDebugLog = exports.scanSubagentTraces = exports.findAllSessionsWithWorktrees = exports.discoverWorktreeSiblings = exports.resolveWorktreeMainRepo = exports.getAllClaudeProjectFolders = exports.decodeEncodedPath = exports.getMostRecentlyActiveSessionDir = exports.findSubdirectorySessionDirs = exports.findSessionsInDirectory = exports.findAllClaudeSessions = void 0;
|
|
8
|
-
exports.fetchProviderStatus = exports.fetchQuota = exports.readClaudeMaxAccessTokenSync = exports.readClaudeMaxCredentials = exports.openInBrowser = exports.parseTranscript = exports.generateHtmlReport = exports.PatternExtractor = void 0;
|
|
8
|
+
exports.fetchProviderStatus = exports.describeQuotaFailure = exports.fetchQuota = exports.isMultiAccountEnabled = exports.getActiveAccount = exports.listAccounts = exports.removeAccount = exports.switchToAccount = exports.addCurrentAccount = exports.readActiveClaudeAccount = exports.writeAccountRegistry = exports.readAccountRegistry = exports.getAccountsDir = exports.readClaudeMaxAccessTokenSync = exports.readClaudeMaxCredentials = exports.writeActiveCredentials = exports.readActiveCredentials = exports.openInBrowser = exports.parseTranscript = exports.generateHtmlReport = exports.PatternExtractor = void 0;
|
|
9
9
|
var taskPersistence_1 = require("./types/taskPersistence");
|
|
10
10
|
Object.defineProperty(exports, "TASK_PERSISTENCE_SCHEMA_VERSION", { enumerable: true, get: function () { return taskPersistence_1.TASK_PERSISTENCE_SCHEMA_VERSION; } });
|
|
11
11
|
Object.defineProperty(exports, "normalizeTaskStatus", { enumerable: true, get: function () { return taskPersistence_1.normalizeTaskStatus; } });
|
|
@@ -174,13 +174,31 @@ var report_1 = require("./report");
|
|
|
174
174
|
Object.defineProperty(exports, "generateHtmlReport", { enumerable: true, get: function () { return report_1.generateHtmlReport; } });
|
|
175
175
|
Object.defineProperty(exports, "parseTranscript", { enumerable: true, get: function () { return report_1.parseTranscript; } });
|
|
176
176
|
Object.defineProperty(exports, "openInBrowser", { enumerable: true, get: function () { return report_1.openInBrowser; } });
|
|
177
|
+
// Credential I/O (platform-aware: Keychain on macOS, file on Linux/Windows)
|
|
178
|
+
var credentialIO_1 = require("./credentialIO");
|
|
179
|
+
Object.defineProperty(exports, "readActiveCredentials", { enumerable: true, get: function () { return credentialIO_1.readActiveCredentials; } });
|
|
180
|
+
Object.defineProperty(exports, "writeActiveCredentials", { enumerable: true, get: function () { return credentialIO_1.writeActiveCredentials; } });
|
|
177
181
|
// Credentials
|
|
178
182
|
var credentials_1 = require("./credentials");
|
|
179
183
|
Object.defineProperty(exports, "readClaudeMaxCredentials", { enumerable: true, get: function () { return credentials_1.readClaudeMaxCredentials; } });
|
|
180
184
|
Object.defineProperty(exports, "readClaudeMaxAccessTokenSync", { enumerable: true, get: function () { return credentials_1.readClaudeMaxAccessTokenSync; } });
|
|
185
|
+
// Accounts
|
|
186
|
+
var accounts_1 = require("./accounts");
|
|
187
|
+
Object.defineProperty(exports, "getAccountsDir", { enumerable: true, get: function () { return accounts_1.getAccountsDir; } });
|
|
188
|
+
Object.defineProperty(exports, "readAccountRegistry", { enumerable: true, get: function () { return accounts_1.readAccountRegistry; } });
|
|
189
|
+
Object.defineProperty(exports, "writeAccountRegistry", { enumerable: true, get: function () { return accounts_1.writeAccountRegistry; } });
|
|
190
|
+
Object.defineProperty(exports, "readActiveClaudeAccount", { enumerable: true, get: function () { return accounts_1.readActiveClaudeAccount; } });
|
|
191
|
+
Object.defineProperty(exports, "addCurrentAccount", { enumerable: true, get: function () { return accounts_1.addCurrentAccount; } });
|
|
192
|
+
Object.defineProperty(exports, "switchToAccount", { enumerable: true, get: function () { return accounts_1.switchToAccount; } });
|
|
193
|
+
Object.defineProperty(exports, "removeAccount", { enumerable: true, get: function () { return accounts_1.removeAccount; } });
|
|
194
|
+
Object.defineProperty(exports, "listAccounts", { enumerable: true, get: function () { return accounts_1.listAccounts; } });
|
|
195
|
+
Object.defineProperty(exports, "getActiveAccount", { enumerable: true, get: function () { return accounts_1.getActiveAccount; } });
|
|
196
|
+
Object.defineProperty(exports, "isMultiAccountEnabled", { enumerable: true, get: function () { return accounts_1.isMultiAccountEnabled; } });
|
|
181
197
|
// Quota
|
|
182
198
|
var quota_1 = require("./quota");
|
|
183
199
|
Object.defineProperty(exports, "fetchQuota", { enumerable: true, get: function () { return quota_1.fetchQuota; } });
|
|
200
|
+
var quotaPresentation_1 = require("./quotaPresentation");
|
|
201
|
+
Object.defineProperty(exports, "describeQuotaFailure", { enumerable: true, get: function () { return quotaPresentation_1.describeQuotaFailure; } });
|
|
184
202
|
// Provider Status
|
|
185
203
|
var providerStatus_1 = require("./providerStatus");
|
|
186
204
|
Object.defineProperty(exports, "fetchProviderStatus", { enumerable: true, get: function () { return providerStatus_1.fetchProviderStatus; } });
|
package/dist/quota.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export interface QuotaState {
|
|
|
17
17
|
available: boolean;
|
|
18
18
|
/** Human-readable error if not available */
|
|
19
19
|
error?: string;
|
|
20
|
+
/** Machine-readable failure classification for unavailable states */
|
|
21
|
+
failureKind?: 'auth' | 'network' | 'rate_limit' | 'server' | 'unknown';
|
|
22
|
+
/** HTTP status code for unavailable API responses */
|
|
23
|
+
httpStatus?: number;
|
|
24
|
+
/** Retry delay in milliseconds when rate-limited */
|
|
25
|
+
retryAfterMs?: number;
|
|
20
26
|
/** Projected 5-hour utilization at reset (percentage, capped at 200) */
|
|
21
27
|
projectedFiveHour?: number;
|
|
22
28
|
/** Projected 7-day utilization at reset (percentage, capped at 200) */
|
package/dist/quota.js
CHANGED
|
@@ -29,14 +29,27 @@ function projectFromElapsed(utilization, resetsAt, windowMs) {
|
|
|
29
29
|
/**
|
|
30
30
|
* Helper to build an unavailable QuotaState.
|
|
31
31
|
*/
|
|
32
|
-
function unavailableState(error) {
|
|
32
|
+
function unavailableState(error, meta = {}) {
|
|
33
33
|
return {
|
|
34
34
|
fiveHour: { utilization: 0, resetsAt: '' },
|
|
35
35
|
sevenDay: { utilization: 0, resetsAt: '' },
|
|
36
36
|
available: false,
|
|
37
37
|
error,
|
|
38
|
+
...meta,
|
|
38
39
|
};
|
|
39
40
|
}
|
|
41
|
+
function parseRetryAfterMs(retryAfter) {
|
|
42
|
+
if (!retryAfter)
|
|
43
|
+
return undefined;
|
|
44
|
+
const seconds = Number(retryAfter);
|
|
45
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
46
|
+
return Math.round(seconds * 1000);
|
|
47
|
+
}
|
|
48
|
+
const retryAt = Date.parse(retryAfter);
|
|
49
|
+
if (Number.isNaN(retryAt))
|
|
50
|
+
return undefined;
|
|
51
|
+
return Math.max(retryAt - Date.now(), 0);
|
|
52
|
+
}
|
|
40
53
|
/**
|
|
41
54
|
* Fetch current quota utilization from the Anthropic OAuth usage API.
|
|
42
55
|
*
|
|
@@ -57,9 +70,29 @@ async function fetchQuota(accessToken) {
|
|
|
57
70
|
},
|
|
58
71
|
});
|
|
59
72
|
if (!res.ok) {
|
|
60
|
-
if (res.status === 401)
|
|
61
|
-
return unavailableState('Sign in to Claude Code to view quota'
|
|
62
|
-
|
|
73
|
+
if (res.status === 401) {
|
|
74
|
+
return unavailableState('Sign in to Claude Code to view quota', {
|
|
75
|
+
failureKind: 'auth',
|
|
76
|
+
httpStatus: res.status,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (res.status === 429) {
|
|
80
|
+
return unavailableState(`API error: ${res.status}`, {
|
|
81
|
+
failureKind: 'rate_limit',
|
|
82
|
+
httpStatus: res.status,
|
|
83
|
+
retryAfterMs: parseRetryAfterMs(res.headers.get('retry-after')),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (res.status >= 500 && res.status <= 599) {
|
|
87
|
+
return unavailableState(`API error: ${res.status}`, {
|
|
88
|
+
failureKind: 'server',
|
|
89
|
+
httpStatus: res.status,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return unavailableState(`API error: ${res.status}`, {
|
|
93
|
+
failureKind: 'unknown',
|
|
94
|
+
httpStatus: res.status,
|
|
95
|
+
});
|
|
63
96
|
}
|
|
64
97
|
const data = await res.json();
|
|
65
98
|
const fiveUtil = data.five_hour?.utilization ?? 0;
|
|
@@ -75,6 +108,6 @@ async function fetchQuota(accessToken) {
|
|
|
75
108
|
};
|
|
76
109
|
}
|
|
77
110
|
catch {
|
|
78
|
-
return unavailableState('Network error');
|
|
111
|
+
return unavailableState('Network error', { failureKind: 'network' });
|
|
79
112
|
}
|
|
80
113
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { QuotaState } from './quota';
|
|
2
|
+
export interface QuotaFailureDescriptor {
|
|
3
|
+
severity: 'info' | 'warning' | 'error';
|
|
4
|
+
title: string;
|
|
5
|
+
message: string;
|
|
6
|
+
detail?: string;
|
|
7
|
+
alertKey: string;
|
|
8
|
+
isRetryable: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function describeQuotaFailure(quota: QuotaState | null | undefined): QuotaFailureDescriptor | null;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.describeQuotaFailure = describeQuotaFailure;
|
|
4
|
+
function formatRetryAfter(ms) {
|
|
5
|
+
if (ms <= 0)
|
|
6
|
+
return 'now';
|
|
7
|
+
const totalSeconds = Math.ceil(ms / 1000);
|
|
8
|
+
const days = Math.floor(totalSeconds / 86_400);
|
|
9
|
+
const hours = Math.floor((totalSeconds % 86_400) / 3_600);
|
|
10
|
+
const minutes = Math.floor((totalSeconds % 3_600) / 60);
|
|
11
|
+
const seconds = totalSeconds % 60;
|
|
12
|
+
const parts = [];
|
|
13
|
+
if (days > 0)
|
|
14
|
+
parts.push(`${days}d`);
|
|
15
|
+
if (hours > 0)
|
|
16
|
+
parts.push(`${hours}h`);
|
|
17
|
+
if (minutes > 0 && days === 0)
|
|
18
|
+
parts.push(`${minutes}m`);
|
|
19
|
+
if (parts.length === 0)
|
|
20
|
+
parts.push(`${seconds}s`);
|
|
21
|
+
return parts.join(' ');
|
|
22
|
+
}
|
|
23
|
+
function describeQuotaFailure(quota) {
|
|
24
|
+
if (!quota || quota.available || !quota.failureKind)
|
|
25
|
+
return null;
|
|
26
|
+
switch (quota.failureKind) {
|
|
27
|
+
case 'auth':
|
|
28
|
+
if (quota.error === 'No OAuth token available') {
|
|
29
|
+
return {
|
|
30
|
+
severity: 'error',
|
|
31
|
+
title: 'Sign in required',
|
|
32
|
+
message: 'No Claude Code credentials are available in this environment.',
|
|
33
|
+
detail: 'Run `claude` to sign in, then retry quota refresh.',
|
|
34
|
+
alertKey: 'auth:no-credentials',
|
|
35
|
+
isRetryable: false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
severity: 'error',
|
|
40
|
+
title: 'Claude Code sign-in expired',
|
|
41
|
+
message: 'The current Claude Code OAuth session was rejected.',
|
|
42
|
+
detail: 'Sign in again to refresh subscription quota.',
|
|
43
|
+
alertKey: `auth:${quota.httpStatus ?? 'unknown'}`,
|
|
44
|
+
isRetryable: false,
|
|
45
|
+
};
|
|
46
|
+
case 'network':
|
|
47
|
+
return {
|
|
48
|
+
severity: 'warning',
|
|
49
|
+
title: 'Quota API unreachable',
|
|
50
|
+
message: 'Could not reach Anthropic from the current environment.',
|
|
51
|
+
detail: 'Check connectivity, proxy, or firewall settings, then retry.',
|
|
52
|
+
alertKey: 'network',
|
|
53
|
+
isRetryable: true,
|
|
54
|
+
};
|
|
55
|
+
case 'rate_limit':
|
|
56
|
+
return {
|
|
57
|
+
severity: 'warning',
|
|
58
|
+
title: 'Quota API rate limited',
|
|
59
|
+
message: quota.retryAfterMs != null
|
|
60
|
+
? `Retry in ${formatRetryAfter(quota.retryAfterMs)}.`
|
|
61
|
+
: 'Retry shortly.',
|
|
62
|
+
detail: quota.httpStatus != null ? `Anthropic returned HTTP ${quota.httpStatus}.` : undefined,
|
|
63
|
+
alertKey: `rate_limit:${quota.httpStatus ?? 429}`,
|
|
64
|
+
isRetryable: true,
|
|
65
|
+
};
|
|
66
|
+
case 'server':
|
|
67
|
+
return {
|
|
68
|
+
severity: 'warning',
|
|
69
|
+
title: 'Quota API unavailable',
|
|
70
|
+
message: quota.httpStatus != null
|
|
71
|
+
? `Anthropic returned HTTP ${quota.httpStatus}. Try again shortly.`
|
|
72
|
+
: 'Anthropic quota data is temporarily unavailable.',
|
|
73
|
+
alertKey: `server:${quota.httpStatus ?? 'unknown'}`,
|
|
74
|
+
isRetryable: true,
|
|
75
|
+
};
|
|
76
|
+
case 'unknown':
|
|
77
|
+
return {
|
|
78
|
+
severity: 'error',
|
|
79
|
+
title: 'Unexpected quota response',
|
|
80
|
+
message: quota.httpStatus != null
|
|
81
|
+
? `Anthropic returned HTTP ${quota.httpStatus}.`
|
|
82
|
+
: (quota.error ?? 'Quota data could not be retrieved.'),
|
|
83
|
+
detail: 'This failure is not classified as retryable.',
|
|
84
|
+
alertKey: `unknown:${quota.httpStatus ?? 'none'}`,
|
|
85
|
+
isRetryable: false,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
package/package.json
CHANGED