npm-cli-gh-issue-preparator 1.0.4 → 1.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.
@@ -0,0 +1,279 @@
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 CredentialInfo = {
14
+ name: string;
15
+ priority: number;
16
+ filePath: string;
17
+ };
18
+
19
+ type UsageWindow = {
20
+ utilization?: number;
21
+ resets_at?: string;
22
+ };
23
+
24
+ type UsageResponse = {
25
+ five_hour?: UsageWindow;
26
+ seven_day?: UsageWindow;
27
+ seven_day_opus?: UsageWindow;
28
+ seven_day_sonnet?: UsageWindow;
29
+ error?: string;
30
+ };
31
+
32
+ const isCredentialsFile = (value: unknown): value is CredentialsFile => {
33
+ if (typeof value !== 'object' || value === null) return false;
34
+ return true;
35
+ };
36
+
37
+ const isUsageResponse = (value: unknown): value is UsageResponse => {
38
+ if (typeof value !== 'object' || value === null) return false;
39
+ return true;
40
+ };
41
+
42
+ const findCredentials = (filePathList: string[]): CredentialInfo[] => {
43
+ const credentials: CredentialInfo[] = [];
44
+ const baseFileName = '.credentials.json';
45
+
46
+ for (const filePath of filePathList) {
47
+ const fileName = path.basename(filePath);
48
+
49
+ if (fileName === baseFileName) {
50
+ continue;
51
+ }
52
+
53
+ const suffix = fileName.slice(baseFileName.length + 1);
54
+ const parts = suffix.split('.');
55
+ if (parts.length !== 2) {
56
+ continue;
57
+ }
58
+
59
+ const name = parts[0];
60
+ const priorityStr = parts[1];
61
+ const priority = parseInt(priorityStr, 10);
62
+
63
+ if (isNaN(priority)) {
64
+ continue;
65
+ }
66
+
67
+ credentials.push({
68
+ name,
69
+ priority,
70
+ filePath,
71
+ });
72
+ }
73
+
74
+ return credentials.sort((a, b) => a.priority - b.priority);
75
+ };
76
+
77
+ export class OauthAPIClaudeRepository implements ClaudeRepository {
78
+ private readonly credentialsPath: string;
79
+ private readonly claudeDir: string;
80
+
81
+ constructor() {
82
+ this.claudeDir = path.join(os.homedir(), '.claude');
83
+ this.credentialsPath = path.join(this.claudeDir, '.credentials.json');
84
+ }
85
+
86
+ private getAccessToken(): string {
87
+ if (!fs.existsSync(this.credentialsPath)) {
88
+ throw new Error(
89
+ `Claude credentials file not found at ${this.credentialsPath}. Please login to Claude Code first using: claude login`,
90
+ );
91
+ }
92
+
93
+ const fileContent = fs.readFileSync(this.credentialsPath, 'utf-8');
94
+ const credentials: unknown = JSON.parse(fileContent);
95
+
96
+ if (!isCredentialsFile(credentials)) {
97
+ throw new Error('Invalid credentials file format');
98
+ }
99
+
100
+ const accessToken = credentials.claudeAiOauth?.accessToken;
101
+
102
+ if (!accessToken) {
103
+ throw new Error('No access token found in credentials file');
104
+ }
105
+
106
+ return accessToken;
107
+ }
108
+
109
+ async getUsage(): Promise<ClaudeWindowUsage[]> {
110
+ const accessToken = this.getAccessToken();
111
+
112
+ const response = await fetch('https://api.anthropic.com/api/oauth/usage', {
113
+ method: 'GET',
114
+ headers: {
115
+ Accept: 'application/json, text/plain, */*',
116
+ 'Content-Type': 'application/json',
117
+ 'User-Agent': 'claude-code/2.0.32',
118
+ Authorization: `Bearer ${accessToken}`,
119
+ 'anthropic-beta': 'oauth-2025-04-20',
120
+ },
121
+ });
122
+
123
+ if (!response.ok) {
124
+ const errorText = await response.text();
125
+ throw new Error(`Claude API error: ${errorText}`);
126
+ }
127
+
128
+ const responseData: unknown = await response.json();
129
+
130
+ if (!isUsageResponse(responseData)) {
131
+ throw new Error('Invalid API response format');
132
+ }
133
+
134
+ if (responseData.error) {
135
+ throw new Error(`API error: ${responseData.error}`);
136
+ }
137
+
138
+ const usages: ClaudeWindowUsage[] = [];
139
+
140
+ if (responseData.five_hour?.utilization !== undefined) {
141
+ usages.push({
142
+ hour: 5,
143
+ utilizationPercentage: responseData.five_hour.utilization,
144
+ resetsAt: responseData.five_hour.resets_at
145
+ ? new Date(responseData.five_hour.resets_at)
146
+ : new Date(),
147
+ });
148
+ }
149
+
150
+ if (responseData.seven_day?.utilization !== undefined) {
151
+ usages.push({
152
+ hour: 168,
153
+ utilizationPercentage: responseData.seven_day.utilization,
154
+ resetsAt: responseData.seven_day.resets_at
155
+ ? new Date(responseData.seven_day.resets_at)
156
+ : new Date(),
157
+ });
158
+ }
159
+
160
+ if (responseData.seven_day_opus?.utilization !== undefined) {
161
+ usages.push({
162
+ hour: 168,
163
+ utilizationPercentage: responseData.seven_day_opus.utilization,
164
+ resetsAt: responseData.seven_day_opus.resets_at
165
+ ? new Date(responseData.seven_day_opus.resets_at)
166
+ : new Date(),
167
+ });
168
+ }
169
+
170
+ if (responseData.seven_day_sonnet?.utilization !== undefined) {
171
+ usages.push({
172
+ hour: 168,
173
+ utilizationPercentage: responseData.seven_day_sonnet.utilization,
174
+ resetsAt: responseData.seven_day_sonnet.resets_at
175
+ ? new Date(responseData.seven_day_sonnet.resets_at)
176
+ : new Date(),
177
+ });
178
+ }
179
+
180
+ return usages;
181
+ }
182
+
183
+ private async getUsageWithToken(accessToken: string): Promise<UsageResponse> {
184
+ const response = await fetch('https://api.anthropic.com/api/oauth/usage', {
185
+ method: 'GET',
186
+ headers: {
187
+ Accept: 'application/json, text/plain, */*',
188
+ 'Content-Type': 'application/json',
189
+ 'User-Agent': 'claude-code/2.0.32',
190
+ Authorization: `Bearer ${accessToken}`,
191
+ 'anthropic-beta': 'oauth-2025-04-20',
192
+ },
193
+ });
194
+
195
+ if (!response.ok) {
196
+ const errorText = await response.text();
197
+ throw new Error(`Claude API error: ${errorText}`);
198
+ }
199
+
200
+ const responseData: unknown = await response.json();
201
+
202
+ if (!isUsageResponse(responseData)) {
203
+ throw new Error('Invalid API response format');
204
+ }
205
+
206
+ if (responseData.error) {
207
+ throw new Error(`API error: ${responseData.error}`);
208
+ }
209
+
210
+ return responseData;
211
+ }
212
+
213
+ private isUsageUnderThreshold(
214
+ usageResponse: UsageResponse,
215
+ threshold: number,
216
+ ): boolean {
217
+ const windows = [
218
+ usageResponse.five_hour,
219
+ usageResponse.seven_day,
220
+ usageResponse.seven_day_opus,
221
+ usageResponse.seven_day_sonnet,
222
+ ];
223
+
224
+ for (const window of windows) {
225
+ if (
226
+ window?.utilization !== undefined &&
227
+ window.utilization >= threshold
228
+ ) {
229
+ return false;
230
+ }
231
+ }
232
+
233
+ return true;
234
+ }
235
+
236
+ async isClaudeAvailable(threshold: number): Promise<boolean> {
237
+ if (!fs.existsSync(this.claudeDir)) {
238
+ return false;
239
+ }
240
+
241
+ const files = fs.readdirSync(this.claudeDir);
242
+ const filePathList = files
243
+ .filter((file) => file.startsWith('.credentials.json'))
244
+ .map((file) => path.join(this.claudeDir, file));
245
+
246
+ const credentials = findCredentials(filePathList);
247
+
248
+ if (credentials.length === 0) {
249
+ return false;
250
+ }
251
+
252
+ for (const credential of credentials) {
253
+ const fileContent = fs.readFileSync(credential.filePath, 'utf-8');
254
+ const credentialData: unknown = JSON.parse(fileContent);
255
+
256
+ if (!isCredentialsFile(credentialData)) {
257
+ continue;
258
+ }
259
+
260
+ const accessToken = credentialData.claudeAiOauth?.accessToken;
261
+ if (!accessToken) {
262
+ continue;
263
+ }
264
+
265
+ try {
266
+ const usageResponse = await this.getUsageWithToken(accessToken);
267
+
268
+ if (this.isUsageUnderThreshold(usageResponse, threshold)) {
269
+ fs.copyFileSync(credential.filePath, this.credentialsPath);
270
+ return true;
271
+ }
272
+ } catch {
273
+ continue;
274
+ }
275
+ }
276
+
277
+ return false;
278
+ }
279
+ }
@@ -0,0 +1,5 @@
1
+ export type ClaudeWindowUsage = {
2
+ hour: number;
3
+ utilizationPercentage: number;
4
+ resetsAt: Date;
5
+ };
@@ -0,0 +1,6 @@
1
+ import { ClaudeWindowUsage } from '../../entities/ClaudeWindowUsage';
2
+
3
+ export interface ClaudeRepository {
4
+ getUsage(): Promise<ClaudeWindowUsage[]>;
5
+ isClaudeAvailable(threshold: number): Promise<boolean>;
6
+ }
@@ -0,0 +1,13 @@
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
+ private readonly claudeDir;
6
+ constructor();
7
+ private getAccessToken;
8
+ getUsage(): Promise<ClaudeWindowUsage[]>;
9
+ private getUsageWithToken;
10
+ private isUsageUnderThreshold;
11
+ isClaudeAvailable(threshold: number): Promise<boolean>;
12
+ }
13
+ //# 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;AA2E5E,qBAAa,wBAAyB,YAAW,gBAAgB;IAC/D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;;IAOnC,OAAO,CAAC,cAAc;IAuBhB,QAAQ,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;YA0EhC,iBAAiB;IA8B/B,OAAO,CAAC,qBAAqB;IAuBvB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CA2C7D"}
@@ -0,0 +1,6 @@
1
+ export type ClaudeWindowUsage = {
2
+ hour: number;
3
+ utilizationPercentage: number;
4
+ resetsAt: Date;
5
+ };
6
+ //# sourceMappingURL=ClaudeWindowUsage.d.ts.map
@@ -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,6 @@
1
+ import { ClaudeWindowUsage } from '../../entities/ClaudeWindowUsage';
2
+ export interface ClaudeRepository {
3
+ getUsage(): Promise<ClaudeWindowUsage[]>;
4
+ isClaudeAvailable(threshold: number): Promise<boolean>;
5
+ }
6
+ //# sourceMappingURL=ClaudeRepository.d.ts.map
@@ -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;IACzC,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACxD"}