npm-cli-gh-issue-preparator 1.0.4 → 1.1.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 +7 -0
- package/bin/adapter/repositories/OauthAPIClaudeRepository.js +133 -0
- package/bin/adapter/repositories/OauthAPIClaudeRepository.js.map +1 -0
- package/bin/domain/entities/ClaudeWindowUsage.js +3 -0
- package/bin/domain/entities/ClaudeWindowUsage.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/ClaudeRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/ClaudeRepository.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/repositories/OauthAPIClaudeRepository.test.ts +346 -0
- package/src/adapter/repositories/OauthAPIClaudeRepository.ts +143 -0
- package/src/domain/entities/ClaudeWindowUsage.ts +5 -0
- package/src/domain/usecases/adapter-interfaces/ClaudeRepository.ts +5 -0
- package/types/adapter/repositories/OauthAPIClaudeRepository.d.ts +9 -0
- package/types/adapter/repositories/OauthAPIClaudeRepository.d.ts.map +1 -0
- package/types/domain/entities/ClaudeWindowUsage.d.ts +6 -0
- package/types/domain/entities/ClaudeWindowUsage.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/ClaudeRepository.d.ts +5 -0
- package/types/domain/usecases/adapter-interfaces/ClaudeRepository.d.ts.map +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.1.0](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/compare/v1.0.4...v1.1.0) (2026-01-12)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **core:** add Claude usage repository for OAuth API integration ([57ab791](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/commit/57ab7914f9574a4e6746183457e643dac47f7261))
|
|
7
|
+
|
|
1
8
|
## [1.0.4](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/compare/v1.0.3...v1.0.4) (2026-01-10)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.OauthAPIClaudeRepository = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const isCredentialsFile = (value) => {
|
|
41
|
+
if (typeof value !== 'object' || value === null)
|
|
42
|
+
return false;
|
|
43
|
+
return true;
|
|
44
|
+
};
|
|
45
|
+
const isUsageResponse = (value) => {
|
|
46
|
+
if (typeof value !== 'object' || value === null)
|
|
47
|
+
return false;
|
|
48
|
+
return true;
|
|
49
|
+
};
|
|
50
|
+
class OauthAPIClaudeRepository {
|
|
51
|
+
constructor() {
|
|
52
|
+
this.credentialsPath = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
53
|
+
}
|
|
54
|
+
getAccessToken() {
|
|
55
|
+
if (!fs.existsSync(this.credentialsPath)) {
|
|
56
|
+
throw new Error(`Claude credentials file not found at ${this.credentialsPath}. Please login to Claude Code first using: claude login`);
|
|
57
|
+
}
|
|
58
|
+
const fileContent = fs.readFileSync(this.credentialsPath, 'utf-8');
|
|
59
|
+
const credentials = JSON.parse(fileContent);
|
|
60
|
+
if (!isCredentialsFile(credentials)) {
|
|
61
|
+
throw new Error('Invalid credentials file format');
|
|
62
|
+
}
|
|
63
|
+
const accessToken = credentials.claudeAiOauth?.accessToken;
|
|
64
|
+
if (!accessToken) {
|
|
65
|
+
throw new Error('No access token found in credentials file');
|
|
66
|
+
}
|
|
67
|
+
return accessToken;
|
|
68
|
+
}
|
|
69
|
+
async getUsage() {
|
|
70
|
+
const accessToken = this.getAccessToken();
|
|
71
|
+
const response = await fetch('https://api.anthropic.com/api/oauth/usage', {
|
|
72
|
+
method: 'GET',
|
|
73
|
+
headers: {
|
|
74
|
+
Accept: 'application/json, text/plain, */*',
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
'User-Agent': 'claude-code/2.0.32',
|
|
77
|
+
Authorization: `Bearer ${accessToken}`,
|
|
78
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const errorText = await response.text();
|
|
83
|
+
throw new Error(`Claude API error: ${errorText}`);
|
|
84
|
+
}
|
|
85
|
+
const responseData = await response.json();
|
|
86
|
+
if (!isUsageResponse(responseData)) {
|
|
87
|
+
throw new Error('Invalid API response format');
|
|
88
|
+
}
|
|
89
|
+
if (responseData.error) {
|
|
90
|
+
throw new Error(`API error: ${responseData.error}`);
|
|
91
|
+
}
|
|
92
|
+
const usages = [];
|
|
93
|
+
if (responseData.five_hour?.utilization !== undefined) {
|
|
94
|
+
usages.push({
|
|
95
|
+
hour: 5,
|
|
96
|
+
utilizationPercentage: responseData.five_hour.utilization,
|
|
97
|
+
resetsAt: responseData.five_hour.resets_at
|
|
98
|
+
? new Date(responseData.five_hour.resets_at)
|
|
99
|
+
: new Date(),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (responseData.seven_day?.utilization !== undefined) {
|
|
103
|
+
usages.push({
|
|
104
|
+
hour: 168,
|
|
105
|
+
utilizationPercentage: responseData.seven_day.utilization,
|
|
106
|
+
resetsAt: responseData.seven_day.resets_at
|
|
107
|
+
? new Date(responseData.seven_day.resets_at)
|
|
108
|
+
: new Date(),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (responseData.seven_day_opus?.utilization !== undefined) {
|
|
112
|
+
usages.push({
|
|
113
|
+
hour: 168,
|
|
114
|
+
utilizationPercentage: responseData.seven_day_opus.utilization,
|
|
115
|
+
resetsAt: responseData.seven_day_opus.resets_at
|
|
116
|
+
? new Date(responseData.seven_day_opus.resets_at)
|
|
117
|
+
: new Date(),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (responseData.seven_day_sonnet?.utilization !== undefined) {
|
|
121
|
+
usages.push({
|
|
122
|
+
hour: 168,
|
|
123
|
+
utilizationPercentage: responseData.seven_day_sonnet.utilization,
|
|
124
|
+
resetsAt: responseData.seven_day_sonnet.resets_at
|
|
125
|
+
? new Date(responseData.seven_day_sonnet.resets_at)
|
|
126
|
+
: new Date(),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return usages;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
exports.OauthAPIClaudeRepository = OauthAPIClaudeRepository;
|
|
133
|
+
//# sourceMappingURL=OauthAPIClaudeRepository.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OauthAPIClaudeRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/OauthAPIClaudeRepository.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ClaudeWindowUsage.js","sourceRoot":"","sources":["../../../src/domain/entities/ClaudeWindowUsage.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ClaudeRepository.js","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/ClaudeRepository.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
const mockExistsSync = jest.fn();
|
|
2
|
+
const mockReadFileSync = jest.fn();
|
|
3
|
+
const mockHomedir = jest.fn();
|
|
4
|
+
|
|
5
|
+
jest.mock('fs', () => ({
|
|
6
|
+
existsSync: mockExistsSync,
|
|
7
|
+
readFileSync: mockReadFileSync,
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
jest.mock('os', () => ({
|
|
11
|
+
homedir: mockHomedir,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
import { OauthAPIClaudeRepository } from './OauthAPIClaudeRepository';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
|
|
17
|
+
describe('OauthAPIClaudeRepository', () => {
|
|
18
|
+
let repository: OauthAPIClaudeRepository;
|
|
19
|
+
let mockFetch: jest.Mock;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
mockHomedir.mockReturnValue('/home/testuser');
|
|
23
|
+
mockFetch = jest.fn();
|
|
24
|
+
global.fetch = mockFetch;
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const credentialsPath = path.join(
|
|
29
|
+
'/home/testuser',
|
|
30
|
+
'.claude',
|
|
31
|
+
'.credentials.json',
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
describe('getUsage', () => {
|
|
35
|
+
it('should fetch usage data from Claude API', async () => {
|
|
36
|
+
mockExistsSync.mockReturnValue(true);
|
|
37
|
+
mockReadFileSync.mockReturnValue(
|
|
38
|
+
JSON.stringify({
|
|
39
|
+
claudeAiOauth: {
|
|
40
|
+
accessToken: 'test-access-token',
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
mockFetch.mockResolvedValueOnce({
|
|
46
|
+
ok: true,
|
|
47
|
+
json: jest.fn().mockResolvedValue({
|
|
48
|
+
five_hour: {
|
|
49
|
+
utilization: 25.5,
|
|
50
|
+
resets_at: '2026-01-12T10:00:00Z',
|
|
51
|
+
},
|
|
52
|
+
seven_day: {
|
|
53
|
+
utilization: 50.0,
|
|
54
|
+
resets_at: '2026-01-15T00:00:00Z',
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
repository = new OauthAPIClaudeRepository();
|
|
60
|
+
const usages = await repository.getUsage();
|
|
61
|
+
|
|
62
|
+
expect(usages).toHaveLength(2);
|
|
63
|
+
expect(usages[0]).toEqual({
|
|
64
|
+
hour: 5,
|
|
65
|
+
utilizationPercentage: 25.5,
|
|
66
|
+
resetsAt: new Date('2026-01-12T10:00:00Z'),
|
|
67
|
+
});
|
|
68
|
+
expect(usages[1]).toEqual({
|
|
69
|
+
hour: 168,
|
|
70
|
+
utilizationPercentage: 50.0,
|
|
71
|
+
resetsAt: new Date('2026-01-15T00:00:00Z'),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
75
|
+
'https://api.anthropic.com/api/oauth/usage',
|
|
76
|
+
expect.objectContaining({
|
|
77
|
+
method: 'GET',
|
|
78
|
+
headers: {
|
|
79
|
+
Accept: 'application/json, text/plain, */*',
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
'User-Agent': 'claude-code/2.0.32',
|
|
82
|
+
Authorization: 'Bearer test-access-token',
|
|
83
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should include opus and sonnet usage when available', async () => {
|
|
90
|
+
mockExistsSync.mockReturnValue(true);
|
|
91
|
+
mockReadFileSync.mockReturnValue(
|
|
92
|
+
JSON.stringify({
|
|
93
|
+
claudeAiOauth: {
|
|
94
|
+
accessToken: 'test-access-token',
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
mockFetch.mockResolvedValueOnce({
|
|
100
|
+
ok: true,
|
|
101
|
+
json: jest.fn().mockResolvedValue({
|
|
102
|
+
five_hour: {
|
|
103
|
+
utilization: 10.0,
|
|
104
|
+
resets_at: '2026-01-12T10:00:00Z',
|
|
105
|
+
},
|
|
106
|
+
seven_day: {
|
|
107
|
+
utilization: 20.0,
|
|
108
|
+
resets_at: '2026-01-15T00:00:00Z',
|
|
109
|
+
},
|
|
110
|
+
seven_day_opus: {
|
|
111
|
+
utilization: 30.0,
|
|
112
|
+
resets_at: '2026-01-16T00:00:00Z',
|
|
113
|
+
},
|
|
114
|
+
seven_day_sonnet: {
|
|
115
|
+
utilization: 40.0,
|
|
116
|
+
resets_at: '2026-01-17T00:00:00Z',
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
repository = new OauthAPIClaudeRepository();
|
|
122
|
+
const usages = await repository.getUsage();
|
|
123
|
+
|
|
124
|
+
expect(usages).toHaveLength(4);
|
|
125
|
+
expect(usages[2]).toEqual({
|
|
126
|
+
hour: 168,
|
|
127
|
+
utilizationPercentage: 30.0,
|
|
128
|
+
resetsAt: new Date('2026-01-16T00:00:00Z'),
|
|
129
|
+
});
|
|
130
|
+
expect(usages[3]).toEqual({
|
|
131
|
+
hour: 168,
|
|
132
|
+
utilizationPercentage: 40.0,
|
|
133
|
+
resetsAt: new Date('2026-01-17T00:00:00Z'),
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should throw error when credentials file not found', async () => {
|
|
138
|
+
mockExistsSync.mockReturnValue(false);
|
|
139
|
+
|
|
140
|
+
repository = new OauthAPIClaudeRepository();
|
|
141
|
+
|
|
142
|
+
await expect(repository.getUsage()).rejects.toThrow(
|
|
143
|
+
`Claude credentials file not found at ${credentialsPath}`,
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should throw error when access token is missing', async () => {
|
|
148
|
+
mockExistsSync.mockReturnValue(true);
|
|
149
|
+
mockReadFileSync.mockReturnValue(
|
|
150
|
+
JSON.stringify({
|
|
151
|
+
claudeAiOauth: {},
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
repository = new OauthAPIClaudeRepository();
|
|
156
|
+
|
|
157
|
+
await expect(repository.getUsage()).rejects.toThrow(
|
|
158
|
+
'No access token found in credentials file',
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should throw error when credentials file has invalid format', async () => {
|
|
163
|
+
mockExistsSync.mockReturnValue(true);
|
|
164
|
+
mockReadFileSync.mockReturnValue('null');
|
|
165
|
+
|
|
166
|
+
repository = new OauthAPIClaudeRepository();
|
|
167
|
+
|
|
168
|
+
await expect(repository.getUsage()).rejects.toThrow(
|
|
169
|
+
'Invalid credentials file format',
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should throw error when API response is not ok', async () => {
|
|
174
|
+
mockExistsSync.mockReturnValue(true);
|
|
175
|
+
mockReadFileSync.mockReturnValue(
|
|
176
|
+
JSON.stringify({
|
|
177
|
+
claudeAiOauth: {
|
|
178
|
+
accessToken: 'test-access-token',
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
mockFetch.mockResolvedValueOnce({
|
|
184
|
+
ok: false,
|
|
185
|
+
text: jest.fn().mockResolvedValue('API Error'),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
repository = new OauthAPIClaudeRepository();
|
|
189
|
+
|
|
190
|
+
await expect(repository.getUsage()).rejects.toThrow(
|
|
191
|
+
'Claude API error: API Error',
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should throw error when API returns error in response', async () => {
|
|
196
|
+
mockExistsSync.mockReturnValue(true);
|
|
197
|
+
mockReadFileSync.mockReturnValue(
|
|
198
|
+
JSON.stringify({
|
|
199
|
+
claudeAiOauth: {
|
|
200
|
+
accessToken: 'test-access-token',
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
mockFetch.mockResolvedValueOnce({
|
|
206
|
+
ok: true,
|
|
207
|
+
json: jest.fn().mockResolvedValue({
|
|
208
|
+
error: 'Invalid token',
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
repository = new OauthAPIClaudeRepository();
|
|
213
|
+
|
|
214
|
+
await expect(repository.getUsage()).rejects.toThrow(
|
|
215
|
+
'API error: Invalid token',
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should throw error when API response format is invalid', async () => {
|
|
220
|
+
mockExistsSync.mockReturnValue(true);
|
|
221
|
+
mockReadFileSync.mockReturnValue(
|
|
222
|
+
JSON.stringify({
|
|
223
|
+
claudeAiOauth: {
|
|
224
|
+
accessToken: 'test-access-token',
|
|
225
|
+
},
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
mockFetch.mockResolvedValueOnce({
|
|
230
|
+
ok: true,
|
|
231
|
+
json: jest.fn().mockResolvedValue(null),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
repository = new OauthAPIClaudeRepository();
|
|
235
|
+
|
|
236
|
+
await expect(repository.getUsage()).rejects.toThrow(
|
|
237
|
+
'Invalid API response format',
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should return empty array when no usage data available', async () => {
|
|
242
|
+
mockExistsSync.mockReturnValue(true);
|
|
243
|
+
mockReadFileSync.mockReturnValue(
|
|
244
|
+
JSON.stringify({
|
|
245
|
+
claudeAiOauth: {
|
|
246
|
+
accessToken: 'test-access-token',
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
mockFetch.mockResolvedValueOnce({
|
|
252
|
+
ok: true,
|
|
253
|
+
json: jest.fn().mockResolvedValue({}),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
repository = new OauthAPIClaudeRepository();
|
|
257
|
+
const usages = await repository.getUsage();
|
|
258
|
+
|
|
259
|
+
expect(usages).toHaveLength(0);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should handle missing resets_at by using current date', async () => {
|
|
263
|
+
mockExistsSync.mockReturnValue(true);
|
|
264
|
+
mockReadFileSync.mockReturnValue(
|
|
265
|
+
JSON.stringify({
|
|
266
|
+
claudeAiOauth: {
|
|
267
|
+
accessToken: 'test-access-token',
|
|
268
|
+
},
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const beforeTest = new Date();
|
|
273
|
+
|
|
274
|
+
mockFetch.mockResolvedValueOnce({
|
|
275
|
+
ok: true,
|
|
276
|
+
json: jest.fn().mockResolvedValue({
|
|
277
|
+
five_hour: {
|
|
278
|
+
utilization: 25.5,
|
|
279
|
+
},
|
|
280
|
+
}),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
repository = new OauthAPIClaudeRepository();
|
|
284
|
+
const usages = await repository.getUsage();
|
|
285
|
+
|
|
286
|
+
const afterTest = new Date();
|
|
287
|
+
|
|
288
|
+
expect(usages).toHaveLength(1);
|
|
289
|
+
expect(usages[0].hour).toBe(5);
|
|
290
|
+
expect(usages[0].utilizationPercentage).toBe(25.5);
|
|
291
|
+
expect(usages[0].resetsAt.getTime()).toBeGreaterThanOrEqual(
|
|
292
|
+
beforeTest.getTime(),
|
|
293
|
+
);
|
|
294
|
+
expect(usages[0].resetsAt.getTime()).toBeLessThanOrEqual(
|
|
295
|
+
afterTest.getTime(),
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should handle missing resets_at for all window types', async () => {
|
|
300
|
+
mockExistsSync.mockReturnValue(true);
|
|
301
|
+
mockReadFileSync.mockReturnValue(
|
|
302
|
+
JSON.stringify({
|
|
303
|
+
claudeAiOauth: {
|
|
304
|
+
accessToken: 'test-access-token',
|
|
305
|
+
},
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const beforeTest = new Date();
|
|
310
|
+
|
|
311
|
+
mockFetch.mockResolvedValueOnce({
|
|
312
|
+
ok: true,
|
|
313
|
+
json: jest.fn().mockResolvedValue({
|
|
314
|
+
five_hour: {
|
|
315
|
+
utilization: 10.0,
|
|
316
|
+
},
|
|
317
|
+
seven_day: {
|
|
318
|
+
utilization: 20.0,
|
|
319
|
+
},
|
|
320
|
+
seven_day_opus: {
|
|
321
|
+
utilization: 30.0,
|
|
322
|
+
},
|
|
323
|
+
seven_day_sonnet: {
|
|
324
|
+
utilization: 40.0,
|
|
325
|
+
},
|
|
326
|
+
}),
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
repository = new OauthAPIClaudeRepository();
|
|
330
|
+
const usages = await repository.getUsage();
|
|
331
|
+
|
|
332
|
+
const afterTest = new Date();
|
|
333
|
+
|
|
334
|
+
expect(usages).toHaveLength(4);
|
|
335
|
+
|
|
336
|
+
for (const usage of usages) {
|
|
337
|
+
expect(usage.resetsAt.getTime()).toBeGreaterThanOrEqual(
|
|
338
|
+
beforeTest.getTime(),
|
|
339
|
+
);
|
|
340
|
+
expect(usage.resetsAt.getTime()).toBeLessThanOrEqual(
|
|
341
|
+
afterTest.getTime(),
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { ClaudeRepository } from '../../domain/usecases/adapter-interfaces/ClaudeRepository';
|
|
2
|
+
import { ClaudeWindowUsage } from '../../domain/entities/ClaudeWindowUsage';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
|
|
7
|
+
type CredentialsFile = {
|
|
8
|
+
claudeAiOauth?: {
|
|
9
|
+
accessToken?: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type UsageWindow = {
|
|
14
|
+
utilization?: number;
|
|
15
|
+
resets_at?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type UsageResponse = {
|
|
19
|
+
five_hour?: UsageWindow;
|
|
20
|
+
seven_day?: UsageWindow;
|
|
21
|
+
seven_day_opus?: UsageWindow;
|
|
22
|
+
seven_day_sonnet?: UsageWindow;
|
|
23
|
+
error?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const isCredentialsFile = (value: unknown): value is CredentialsFile => {
|
|
27
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
28
|
+
return true;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const isUsageResponse = (value: unknown): value is UsageResponse => {
|
|
32
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export class OauthAPIClaudeRepository implements ClaudeRepository {
|
|
37
|
+
private readonly credentialsPath: string;
|
|
38
|
+
|
|
39
|
+
constructor() {
|
|
40
|
+
this.credentialsPath = path.join(
|
|
41
|
+
os.homedir(),
|
|
42
|
+
'.claude',
|
|
43
|
+
'.credentials.json',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private getAccessToken(): string {
|
|
48
|
+
if (!fs.existsSync(this.credentialsPath)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Claude credentials file not found at ${this.credentialsPath}. Please login to Claude Code first using: claude login`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fileContent = fs.readFileSync(this.credentialsPath, 'utf-8');
|
|
55
|
+
const credentials: unknown = JSON.parse(fileContent);
|
|
56
|
+
|
|
57
|
+
if (!isCredentialsFile(credentials)) {
|
|
58
|
+
throw new Error('Invalid credentials file format');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const accessToken = credentials.claudeAiOauth?.accessToken;
|
|
62
|
+
|
|
63
|
+
if (!accessToken) {
|
|
64
|
+
throw new Error('No access token found in credentials file');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return accessToken;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async getUsage(): Promise<ClaudeWindowUsage[]> {
|
|
71
|
+
const accessToken = this.getAccessToken();
|
|
72
|
+
|
|
73
|
+
const response = await fetch('https://api.anthropic.com/api/oauth/usage', {
|
|
74
|
+
method: 'GET',
|
|
75
|
+
headers: {
|
|
76
|
+
Accept: 'application/json, text/plain, */*',
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
'User-Agent': 'claude-code/2.0.32',
|
|
79
|
+
Authorization: `Bearer ${accessToken}`,
|
|
80
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
const errorText = await response.text();
|
|
86
|
+
throw new Error(`Claude API error: ${errorText}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const responseData: unknown = await response.json();
|
|
90
|
+
|
|
91
|
+
if (!isUsageResponse(responseData)) {
|
|
92
|
+
throw new Error('Invalid API response format');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (responseData.error) {
|
|
96
|
+
throw new Error(`API error: ${responseData.error}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const usages: ClaudeWindowUsage[] = [];
|
|
100
|
+
|
|
101
|
+
if (responseData.five_hour?.utilization !== undefined) {
|
|
102
|
+
usages.push({
|
|
103
|
+
hour: 5,
|
|
104
|
+
utilizationPercentage: responseData.five_hour.utilization,
|
|
105
|
+
resetsAt: responseData.five_hour.resets_at
|
|
106
|
+
? new Date(responseData.five_hour.resets_at)
|
|
107
|
+
: new Date(),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (responseData.seven_day?.utilization !== undefined) {
|
|
112
|
+
usages.push({
|
|
113
|
+
hour: 168,
|
|
114
|
+
utilizationPercentage: responseData.seven_day.utilization,
|
|
115
|
+
resetsAt: responseData.seven_day.resets_at
|
|
116
|
+
? new Date(responseData.seven_day.resets_at)
|
|
117
|
+
: new Date(),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (responseData.seven_day_opus?.utilization !== undefined) {
|
|
122
|
+
usages.push({
|
|
123
|
+
hour: 168,
|
|
124
|
+
utilizationPercentage: responseData.seven_day_opus.utilization,
|
|
125
|
+
resetsAt: responseData.seven_day_opus.resets_at
|
|
126
|
+
? new Date(responseData.seven_day_opus.resets_at)
|
|
127
|
+
: new Date(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (responseData.seven_day_sonnet?.utilization !== undefined) {
|
|
132
|
+
usages.push({
|
|
133
|
+
hour: 168,
|
|
134
|
+
utilizationPercentage: responseData.seven_day_sonnet.utilization,
|
|
135
|
+
resetsAt: responseData.seven_day_sonnet.resets_at
|
|
136
|
+
? new Date(responseData.seven_day_sonnet.resets_at)
|
|
137
|
+
: new Date(),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return usages;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ClaudeRepository } from '../../domain/usecases/adapter-interfaces/ClaudeRepository';
|
|
2
|
+
import { ClaudeWindowUsage } from '../../domain/entities/ClaudeWindowUsage';
|
|
3
|
+
export declare class OauthAPIClaudeRepository implements ClaudeRepository {
|
|
4
|
+
private readonly credentialsPath;
|
|
5
|
+
constructor();
|
|
6
|
+
private getAccessToken;
|
|
7
|
+
getUsage(): Promise<ClaudeWindowUsage[]>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=OauthAPIClaudeRepository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OauthAPIClaudeRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/OauthAPIClaudeRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,2DAA2D,CAAC;AAC7F,OAAO,EAAE,iBAAiB,EAAE,MAAM,yCAAyC,CAAC;AAkC5E,qBAAa,wBAAyB,YAAW,gBAAgB;IAC/D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;;IAUzC,OAAO,CAAC,cAAc;IAuBhB,QAAQ,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;CAyE/C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ClaudeWindowUsage.d.ts","sourceRoot":"","sources":["../../../src/domain/entities/ClaudeWindowUsage.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB,EAAE,MAAM,CAAC;IAC9B,QAAQ,EAAE,IAAI,CAAC;CAChB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ClaudeRepository.d.ts","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/ClaudeRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAErE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;CAC1C"}
|