npm-cli-gh-issue-preparator 1.1.0 → 1.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.
Files changed (22) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/bin/adapter/repositories/OauthAPIClaudeRepository.js +103 -1
  3. package/bin/adapter/repositories/OauthAPIClaudeRepository.js.map +1 -1
  4. package/bin/adapter/repositories/Xfce4TerminalCopilotRepository.js +27 -0
  5. package/bin/adapter/repositories/Xfce4TerminalCopilotRepository.js.map +1 -0
  6. package/bin/domain/usecases/adapter-interfaces/CopilotRepository.js +3 -0
  7. package/bin/domain/usecases/adapter-interfaces/CopilotRepository.js.map +1 -0
  8. package/package.json +1 -1
  9. package/src/adapter/repositories/OauthAPIClaudeRepository.test.ts +495 -0
  10. package/src/adapter/repositories/OauthAPIClaudeRepository.ts +141 -5
  11. package/src/adapter/repositories/Xfce4TerminalCopilotRepository.test.ts +143 -0
  12. package/src/adapter/repositories/Xfce4TerminalCopilotRepository.ts +32 -0
  13. package/src/domain/usecases/adapter-interfaces/ClaudeRepository.ts +1 -0
  14. package/src/domain/usecases/adapter-interfaces/CopilotRepository.ts +3 -0
  15. package/types/adapter/repositories/OauthAPIClaudeRepository.d.ts +4 -0
  16. package/types/adapter/repositories/OauthAPIClaudeRepository.d.ts.map +1 -1
  17. package/types/adapter/repositories/Xfce4TerminalCopilotRepository.d.ts +6 -0
  18. package/types/adapter/repositories/Xfce4TerminalCopilotRepository.d.ts.map +1 -0
  19. package/types/domain/usecases/adapter-interfaces/ClaudeRepository.d.ts +1 -0
  20. package/types/domain/usecases/adapter-interfaces/ClaudeRepository.d.ts.map +1 -1
  21. package/types/domain/usecases/adapter-interfaces/CopilotRepository.d.ts +4 -0
  22. package/types/domain/usecases/adapter-interfaces/CopilotRepository.d.ts.map +1 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ # [1.3.0](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/compare/v1.2.0...v1.3.0) (2026-01-12)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * prevent command injection in xfce4-terminal spawn ([0a4e008](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/commit/0a4e008749b293cd1f1d311ad08adfc9f37033c3))
7
+
8
+
9
+ ### Features
10
+
11
+ * **src:** create CopilotRepository ([c79ed81](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/commit/c79ed8127342a5aa58fc2f83b10334bbb0fc6a0d)), closes [#38](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/issues/38)
12
+
13
+ # [1.2.0](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/compare/v1.1.0...v1.2.0) (2026-01-12)
14
+
15
+
16
+ ### Features
17
+
18
+ * **src:** add isClaudeAvailable method to rotate Claude credentials ([7b966bd](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/commit/7b966bd123eb9474fd9f675e7addb4b54b509abe))
19
+
1
20
  # [1.1.0](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/compare/v1.0.4...v1.1.0) (2026-01-12)
2
21
 
3
22
 
@@ -47,9 +47,37 @@ const isUsageResponse = (value) => {
47
47
  return false;
48
48
  return true;
49
49
  };
50
+ const findCredentials = (filePathList) => {
51
+ const credentials = [];
52
+ const baseFileName = '.credentials.json';
53
+ for (const filePath of filePathList) {
54
+ const fileName = path.basename(filePath);
55
+ if (fileName === baseFileName) {
56
+ continue;
57
+ }
58
+ const suffix = fileName.slice(baseFileName.length + 1);
59
+ const parts = suffix.split('.');
60
+ if (parts.length !== 2) {
61
+ continue;
62
+ }
63
+ const name = parts[0];
64
+ const priorityStr = parts[1];
65
+ const priority = parseInt(priorityStr, 10);
66
+ if (isNaN(priority)) {
67
+ continue;
68
+ }
69
+ credentials.push({
70
+ name,
71
+ priority,
72
+ filePath,
73
+ });
74
+ }
75
+ return credentials.sort((a, b) => a.priority - b.priority);
76
+ };
50
77
  class OauthAPIClaudeRepository {
51
78
  constructor() {
52
- this.credentialsPath = path.join(os.homedir(), '.claude', '.credentials.json');
79
+ this.claudeDir = path.join(os.homedir(), '.claude');
80
+ this.credentialsPath = path.join(this.claudeDir, '.credentials.json');
53
81
  }
54
82
  getAccessToken() {
55
83
  if (!fs.existsSync(this.credentialsPath)) {
@@ -128,6 +156,80 @@ class OauthAPIClaudeRepository {
128
156
  }
129
157
  return usages;
130
158
  }
159
+ async getUsageWithToken(accessToken) {
160
+ const response = await fetch('https://api.anthropic.com/api/oauth/usage', {
161
+ method: 'GET',
162
+ headers: {
163
+ Accept: 'application/json, text/plain, */*',
164
+ 'Content-Type': 'application/json',
165
+ 'User-Agent': 'claude-code/2.0.32',
166
+ Authorization: `Bearer ${accessToken}`,
167
+ 'anthropic-beta': 'oauth-2025-04-20',
168
+ },
169
+ });
170
+ if (!response.ok) {
171
+ const errorText = await response.text();
172
+ throw new Error(`Claude API error: ${errorText}`);
173
+ }
174
+ const responseData = await response.json();
175
+ if (!isUsageResponse(responseData)) {
176
+ throw new Error('Invalid API response format');
177
+ }
178
+ if (responseData.error) {
179
+ throw new Error(`API error: ${responseData.error}`);
180
+ }
181
+ return responseData;
182
+ }
183
+ isUsageUnderThreshold(usageResponse, threshold) {
184
+ const windows = [
185
+ usageResponse.five_hour,
186
+ usageResponse.seven_day,
187
+ usageResponse.seven_day_opus,
188
+ usageResponse.seven_day_sonnet,
189
+ ];
190
+ for (const window of windows) {
191
+ if (window?.utilization !== undefined &&
192
+ window.utilization >= threshold) {
193
+ return false;
194
+ }
195
+ }
196
+ return true;
197
+ }
198
+ async isClaudeAvailable(threshold) {
199
+ if (!fs.existsSync(this.claudeDir)) {
200
+ return false;
201
+ }
202
+ const files = fs.readdirSync(this.claudeDir);
203
+ const filePathList = files
204
+ .filter((file) => file.startsWith('.credentials.json'))
205
+ .map((file) => path.join(this.claudeDir, file));
206
+ const credentials = findCredentials(filePathList);
207
+ if (credentials.length === 0) {
208
+ return false;
209
+ }
210
+ for (const credential of credentials) {
211
+ const fileContent = fs.readFileSync(credential.filePath, 'utf-8');
212
+ const credentialData = JSON.parse(fileContent);
213
+ if (!isCredentialsFile(credentialData)) {
214
+ continue;
215
+ }
216
+ const accessToken = credentialData.claudeAiOauth?.accessToken;
217
+ if (!accessToken) {
218
+ continue;
219
+ }
220
+ try {
221
+ const usageResponse = await this.getUsageWithToken(accessToken);
222
+ if (this.isUsageUnderThreshold(usageResponse, threshold)) {
223
+ fs.copyFileSync(credential.filePath, this.credentialsPath);
224
+ return true;
225
+ }
226
+ }
227
+ catch {
228
+ continue;
229
+ }
230
+ }
231
+ return false;
232
+ }
131
233
  }
132
234
  exports.OauthAPIClaudeRepository = OauthAPIClaudeRepository;
133
235
  //# sourceMappingURL=OauthAPIClaudeRepository.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"OauthAPIClaudeRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/OauthAPIClaudeRepository.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"OauthAPIClaudeRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/OauthAPIClaudeRepository.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Xfce4TerminalCopilotRepository = void 0;
4
+ const child_process_1 = require("child_process");
5
+ class Xfce4TerminalCopilotRepository {
6
+ escapeForSingleQuotes(str) {
7
+ return str.replace(/'/g, "'\\''");
8
+ }
9
+ run(prompt, model, processTitle) {
10
+ const escapedPrompt = this.escapeForSingleQuotes(prompt);
11
+ const escapedTitle = this.escapeForSingleQuotes(processTitle);
12
+ const innerCommand = `copilot --model ${model} --allow-all-tools -p '${escapedPrompt}'`;
13
+ const escapedInnerCommand = this.escapeForSingleQuotes(innerCommand);
14
+ const title = `gh-issue-preparator: ${escapedTitle}`;
15
+ const child = (0, child_process_1.spawn)('xfce4-terminal', ['-T', title, '-e', `bash -c '${escapedInnerCommand}'`], {
16
+ detached: true,
17
+ stdio: 'ignore',
18
+ });
19
+ child.on('error', () => {
20
+ // Intentionally empty - fire-and-forget behavior
21
+ // Errors are silently ignored as this is a background terminal spawn
22
+ });
23
+ child.unref();
24
+ }
25
+ }
26
+ exports.Xfce4TerminalCopilotRepository = Xfce4TerminalCopilotRepository;
27
+ //# sourceMappingURL=Xfce4TerminalCopilotRepository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Xfce4TerminalCopilotRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/Xfce4TerminalCopilotRepository.ts"],"names":[],"mappings":";;;AACA,iDAAsC;AAEtC,MAAa,8BAA8B;IACjC,qBAAqB,CAAC,GAAW;QACvC,OAAO,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACpC,CAAC;IAED,GAAG,CAAC,MAAc,EAAE,KAAmB,EAAE,YAAoB;QAC3D,MAAM,aAAa,GAAG,IAAI,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC;QACzD,MAAM,YAAY,GAAG,IAAI,CAAC,qBAAqB,CAAC,YAAY,CAAC,CAAC;QAC9D,MAAM,YAAY,GAAG,mBAAmB,KAAK,0BAA0B,aAAa,GAAG,CAAC;QACxF,MAAM,mBAAmB,GAAG,IAAI,CAAC,qBAAqB,CAAC,YAAY,CAAC,CAAC;QACrE,MAAM,KAAK,GAAG,wBAAwB,YAAY,EAAE,CAAC;QAErD,MAAM,KAAK,GAAG,IAAA,qBAAK,EACjB,gBAAgB,EAChB,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,mBAAmB,GAAG,CAAC,EACvD;YACE,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,QAAQ;SAChB,CACF,CAAC;QAEF,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACrB,iDAAiD;YACjD,qEAAqE;QACvE,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC;CACF;AA5BD,wEA4BC"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=CopilotRepository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CopilotRepository.js","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/CopilotRepository.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npm-cli-gh-issue-preparator",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -1,10 +1,14 @@
1
1
  const mockExistsSync = jest.fn();
2
2
  const mockReadFileSync = jest.fn();
3
+ const mockReaddirSync = jest.fn();
4
+ const mockCopyFileSync = jest.fn();
3
5
  const mockHomedir = jest.fn();
4
6
 
5
7
  jest.mock('fs', () => ({
6
8
  existsSync: mockExistsSync,
7
9
  readFileSync: mockReadFileSync,
10
+ readdirSync: mockReaddirSync,
11
+ copyFileSync: mockCopyFileSync,
8
12
  }));
9
13
 
10
14
  jest.mock('os', () => ({
@@ -343,4 +347,495 @@ describe('OauthAPIClaudeRepository', () => {
343
347
  }
344
348
  });
345
349
  });
350
+
351
+ describe('isClaudeAvailable', () => {
352
+ const claudeDir = path.join('/home/testuser', '.claude');
353
+
354
+ it('should return false when claude directory does not exist', async () => {
355
+ mockExistsSync.mockReturnValue(false);
356
+
357
+ repository = new OauthAPIClaudeRepository();
358
+ const result = await repository.isClaudeAvailable(80);
359
+
360
+ expect(result).toBe(false);
361
+ });
362
+
363
+ it('should return false when no credential files exist', async () => {
364
+ mockExistsSync.mockReturnValue(true);
365
+ mockReaddirSync.mockReturnValue(['.credentials.json']);
366
+
367
+ repository = new OauthAPIClaudeRepository();
368
+ const result = await repository.isClaudeAvailable(80);
369
+
370
+ expect(result).toBe(false);
371
+ });
372
+
373
+ it('should return true and copy credential file when usage is under threshold', async () => {
374
+ mockExistsSync.mockReturnValue(true);
375
+ mockReaddirSync.mockReturnValue([
376
+ '.credentials.json',
377
+ '.credentials.json.dev1.1',
378
+ ]);
379
+ mockReadFileSync.mockReturnValue(
380
+ JSON.stringify({
381
+ claudeAiOauth: {
382
+ accessToken: 'test-token-dev1',
383
+ },
384
+ }),
385
+ );
386
+
387
+ mockFetch.mockResolvedValueOnce({
388
+ ok: true,
389
+ json: jest.fn().mockResolvedValue({
390
+ five_hour: {
391
+ utilization: 50.0,
392
+ resets_at: '2026-01-12T10:00:00Z',
393
+ },
394
+ }),
395
+ });
396
+
397
+ repository = new OauthAPIClaudeRepository();
398
+ const result = await repository.isClaudeAvailable(80);
399
+
400
+ expect(result).toBe(true);
401
+ expect(mockCopyFileSync).toHaveBeenCalledWith(
402
+ path.join(claudeDir, '.credentials.json.dev1.1'),
403
+ path.join(claudeDir, '.credentials.json'),
404
+ );
405
+ });
406
+
407
+ it('should return false when all credentials are over threshold', async () => {
408
+ mockExistsSync.mockReturnValue(true);
409
+ mockReaddirSync.mockReturnValue([
410
+ '.credentials.json',
411
+ '.credentials.json.dev1.1',
412
+ ]);
413
+ mockReadFileSync.mockReturnValue(
414
+ JSON.stringify({
415
+ claudeAiOauth: {
416
+ accessToken: 'test-token-dev1',
417
+ },
418
+ }),
419
+ );
420
+
421
+ mockFetch.mockResolvedValueOnce({
422
+ ok: true,
423
+ json: jest.fn().mockResolvedValue({
424
+ five_hour: {
425
+ utilization: 90.0,
426
+ resets_at: '2026-01-12T10:00:00Z',
427
+ },
428
+ }),
429
+ });
430
+
431
+ repository = new OauthAPIClaudeRepository();
432
+ const result = await repository.isClaudeAvailable(80);
433
+
434
+ expect(result).toBe(false);
435
+ expect(mockCopyFileSync).not.toHaveBeenCalled();
436
+ });
437
+
438
+ it('should sort credentials by priority and try lower priority first', async () => {
439
+ mockExistsSync.mockReturnValue(true);
440
+ mockReaddirSync.mockReturnValue([
441
+ '.credentials.json',
442
+ '.credentials.json.dev2.5',
443
+ '.credentials.json.dev1.1',
444
+ ]);
445
+
446
+ const credentialContents: Record<string, string> = {
447
+ [path.join(claudeDir, '.credentials.json.dev1.1')]: JSON.stringify({
448
+ claudeAiOauth: { accessToken: 'token-dev1' },
449
+ }),
450
+ [path.join(claudeDir, '.credentials.json.dev2.5')]: JSON.stringify({
451
+ claudeAiOauth: { accessToken: 'token-dev2' },
452
+ }),
453
+ };
454
+
455
+ mockReadFileSync.mockImplementation((filePath: string) => {
456
+ return credentialContents[filePath] || '';
457
+ });
458
+
459
+ mockFetch.mockResolvedValueOnce({
460
+ ok: true,
461
+ json: jest.fn().mockResolvedValue({
462
+ five_hour: { utilization: 30.0 },
463
+ }),
464
+ });
465
+
466
+ repository = new OauthAPIClaudeRepository();
467
+ const result = await repository.isClaudeAvailable(80);
468
+
469
+ expect(result).toBe(true);
470
+ expect(mockFetch).toHaveBeenCalledWith(
471
+ 'https://api.anthropic.com/api/oauth/usage',
472
+ expect.objectContaining({
473
+ method: 'GET',
474
+ headers: {
475
+ Accept: 'application/json, text/plain, */*',
476
+ 'Content-Type': 'application/json',
477
+ 'User-Agent': 'claude-code/2.0.32',
478
+ Authorization: 'Bearer token-dev1',
479
+ 'anthropic-beta': 'oauth-2025-04-20',
480
+ },
481
+ }),
482
+ );
483
+ expect(mockCopyFileSync).toHaveBeenCalledWith(
484
+ path.join(claudeDir, '.credentials.json.dev1.1'),
485
+ path.join(claudeDir, '.credentials.json'),
486
+ );
487
+ });
488
+
489
+ it('should skip to next credential when API call fails', async () => {
490
+ mockExistsSync.mockReturnValue(true);
491
+ mockReaddirSync.mockReturnValue([
492
+ '.credentials.json',
493
+ '.credentials.json.dev1.1',
494
+ '.credentials.json.dev2.2',
495
+ ]);
496
+
497
+ const credentialContents: Record<string, string> = {
498
+ [path.join(claudeDir, '.credentials.json.dev1.1')]: JSON.stringify({
499
+ claudeAiOauth: { accessToken: 'token-dev1' },
500
+ }),
501
+ [path.join(claudeDir, '.credentials.json.dev2.2')]: JSON.stringify({
502
+ claudeAiOauth: { accessToken: 'token-dev2' },
503
+ }),
504
+ };
505
+
506
+ mockReadFileSync.mockImplementation((filePath: string) => {
507
+ return credentialContents[filePath] || '';
508
+ });
509
+
510
+ mockFetch
511
+ .mockResolvedValueOnce({
512
+ ok: false,
513
+ text: jest.fn().mockResolvedValue('API Error'),
514
+ })
515
+ .mockResolvedValueOnce({
516
+ ok: true,
517
+ json: jest.fn().mockResolvedValue({
518
+ five_hour: { utilization: 30.0 },
519
+ }),
520
+ });
521
+
522
+ repository = new OauthAPIClaudeRepository();
523
+ const result = await repository.isClaudeAvailable(80);
524
+
525
+ expect(result).toBe(true);
526
+ expect(mockFetch).toHaveBeenCalledTimes(2);
527
+ expect(mockCopyFileSync).toHaveBeenCalledWith(
528
+ path.join(claudeDir, '.credentials.json.dev2.2'),
529
+ path.join(claudeDir, '.credentials.json'),
530
+ );
531
+ });
532
+
533
+ it('should skip credential files with invalid format', async () => {
534
+ mockExistsSync.mockReturnValue(true);
535
+ mockReaddirSync.mockReturnValue([
536
+ '.credentials.json',
537
+ '.credentials.json.invalid',
538
+ '.credentials.json.dev1.1',
539
+ ]);
540
+
541
+ mockReadFileSync.mockReturnValue(
542
+ JSON.stringify({
543
+ claudeAiOauth: { accessToken: 'token-dev1' },
544
+ }),
545
+ );
546
+
547
+ mockFetch.mockResolvedValueOnce({
548
+ ok: true,
549
+ json: jest.fn().mockResolvedValue({
550
+ five_hour: { utilization: 30.0 },
551
+ }),
552
+ });
553
+
554
+ repository = new OauthAPIClaudeRepository();
555
+ const result = await repository.isClaudeAvailable(80);
556
+
557
+ expect(result).toBe(true);
558
+ expect(mockCopyFileSync).toHaveBeenCalledWith(
559
+ path.join(claudeDir, '.credentials.json.dev1.1'),
560
+ path.join(claudeDir, '.credentials.json'),
561
+ );
562
+ });
563
+
564
+ it('should skip credential files without access token', async () => {
565
+ mockExistsSync.mockReturnValue(true);
566
+ mockReaddirSync.mockReturnValue([
567
+ '.credentials.json',
568
+ '.credentials.json.dev1.1',
569
+ '.credentials.json.dev2.2',
570
+ ]);
571
+
572
+ const credentialContents: Record<string, string> = {
573
+ [path.join(claudeDir, '.credentials.json.dev1.1')]: JSON.stringify({
574
+ claudeAiOauth: {},
575
+ }),
576
+ [path.join(claudeDir, '.credentials.json.dev2.2')]: JSON.stringify({
577
+ claudeAiOauth: { accessToken: 'token-dev2' },
578
+ }),
579
+ };
580
+
581
+ mockReadFileSync.mockImplementation((filePath: string) => {
582
+ return credentialContents[filePath] || '';
583
+ });
584
+
585
+ mockFetch.mockResolvedValueOnce({
586
+ ok: true,
587
+ json: jest.fn().mockResolvedValue({
588
+ five_hour: { utilization: 30.0 },
589
+ }),
590
+ });
591
+
592
+ repository = new OauthAPIClaudeRepository();
593
+ const result = await repository.isClaudeAvailable(80);
594
+
595
+ expect(result).toBe(true);
596
+ expect(mockFetch).toHaveBeenCalledTimes(1);
597
+ expect(mockCopyFileSync).toHaveBeenCalledWith(
598
+ path.join(claudeDir, '.credentials.json.dev2.2'),
599
+ path.join(claudeDir, '.credentials.json'),
600
+ );
601
+ });
602
+
603
+ it('should check all usage windows against threshold', async () => {
604
+ mockExistsSync.mockReturnValue(true);
605
+ mockReaddirSync.mockReturnValue([
606
+ '.credentials.json',
607
+ '.credentials.json.dev1.1',
608
+ ]);
609
+
610
+ mockReadFileSync.mockReturnValue(
611
+ JSON.stringify({
612
+ claudeAiOauth: { accessToken: 'token-dev1' },
613
+ }),
614
+ );
615
+
616
+ mockFetch.mockResolvedValueOnce({
617
+ ok: true,
618
+ json: jest.fn().mockResolvedValue({
619
+ five_hour: { utilization: 30.0 },
620
+ seven_day: { utilization: 90.0 },
621
+ }),
622
+ });
623
+
624
+ repository = new OauthAPIClaudeRepository();
625
+ const result = await repository.isClaudeAvailable(80);
626
+
627
+ expect(result).toBe(false);
628
+ expect(mockCopyFileSync).not.toHaveBeenCalled();
629
+ });
630
+
631
+ it('should return true when usage equals threshold minus 1', async () => {
632
+ mockExistsSync.mockReturnValue(true);
633
+ mockReaddirSync.mockReturnValue([
634
+ '.credentials.json',
635
+ '.credentials.json.dev1.1',
636
+ ]);
637
+
638
+ mockReadFileSync.mockReturnValue(
639
+ JSON.stringify({
640
+ claudeAiOauth: { accessToken: 'token-dev1' },
641
+ }),
642
+ );
643
+
644
+ mockFetch.mockResolvedValueOnce({
645
+ ok: true,
646
+ json: jest.fn().mockResolvedValue({
647
+ five_hour: { utilization: 79.0 },
648
+ }),
649
+ });
650
+
651
+ repository = new OauthAPIClaudeRepository();
652
+ const result = await repository.isClaudeAvailable(80);
653
+
654
+ expect(result).toBe(true);
655
+ });
656
+
657
+ it('should return false when usage equals threshold', async () => {
658
+ mockExistsSync.mockReturnValue(true);
659
+ mockReaddirSync.mockReturnValue([
660
+ '.credentials.json',
661
+ '.credentials.json.dev1.1',
662
+ ]);
663
+
664
+ mockReadFileSync.mockReturnValue(
665
+ JSON.stringify({
666
+ claudeAiOauth: { accessToken: 'token-dev1' },
667
+ }),
668
+ );
669
+
670
+ mockFetch.mockResolvedValueOnce({
671
+ ok: true,
672
+ json: jest.fn().mockResolvedValue({
673
+ five_hour: { utilization: 80.0 },
674
+ }),
675
+ });
676
+
677
+ repository = new OauthAPIClaudeRepository();
678
+ const result = await repository.isClaudeAvailable(80);
679
+
680
+ expect(result).toBe(false);
681
+ });
682
+
683
+ it('should skip credential files with non-numeric priority', async () => {
684
+ mockExistsSync.mockReturnValue(true);
685
+ mockReaddirSync.mockReturnValue([
686
+ '.credentials.json',
687
+ '.credentials.json.dev.abc',
688
+ '.credentials.json.dev1.1',
689
+ ]);
690
+
691
+ mockReadFileSync.mockReturnValue(
692
+ JSON.stringify({
693
+ claudeAiOauth: { accessToken: 'token-dev1' },
694
+ }),
695
+ );
696
+
697
+ mockFetch.mockResolvedValueOnce({
698
+ ok: true,
699
+ json: jest.fn().mockResolvedValue({
700
+ five_hour: { utilization: 30.0 },
701
+ }),
702
+ });
703
+
704
+ repository = new OauthAPIClaudeRepository();
705
+ const result = await repository.isClaudeAvailable(80);
706
+
707
+ expect(result).toBe(true);
708
+ expect(mockCopyFileSync).toHaveBeenCalledWith(
709
+ path.join(claudeDir, '.credentials.json.dev1.1'),
710
+ path.join(claudeDir, '.credentials.json'),
711
+ );
712
+ });
713
+
714
+ it('should skip to next credential when API returns invalid response format', async () => {
715
+ mockExistsSync.mockReturnValue(true);
716
+ mockReaddirSync.mockReturnValue([
717
+ '.credentials.json',
718
+ '.credentials.json.dev1.1',
719
+ '.credentials.json.dev2.2',
720
+ ]);
721
+
722
+ const credentialContents: Record<string, string> = {
723
+ [path.join(claudeDir, '.credentials.json.dev1.1')]: JSON.stringify({
724
+ claudeAiOauth: { accessToken: 'token-dev1' },
725
+ }),
726
+ [path.join(claudeDir, '.credentials.json.dev2.2')]: JSON.stringify({
727
+ claudeAiOauth: { accessToken: 'token-dev2' },
728
+ }),
729
+ };
730
+
731
+ mockReadFileSync.mockImplementation((filePath: string) => {
732
+ return credentialContents[filePath] || '';
733
+ });
734
+
735
+ mockFetch
736
+ .mockResolvedValueOnce({
737
+ ok: true,
738
+ json: jest.fn().mockResolvedValue(null),
739
+ })
740
+ .mockResolvedValueOnce({
741
+ ok: true,
742
+ json: jest.fn().mockResolvedValue({
743
+ five_hour: { utilization: 30.0 },
744
+ }),
745
+ });
746
+
747
+ repository = new OauthAPIClaudeRepository();
748
+ const result = await repository.isClaudeAvailable(80);
749
+
750
+ expect(result).toBe(true);
751
+ expect(mockFetch).toHaveBeenCalledTimes(2);
752
+ expect(mockCopyFileSync).toHaveBeenCalledWith(
753
+ path.join(claudeDir, '.credentials.json.dev2.2'),
754
+ path.join(claudeDir, '.credentials.json'),
755
+ );
756
+ });
757
+
758
+ it('should skip to next credential when API returns error in response', async () => {
759
+ mockExistsSync.mockReturnValue(true);
760
+ mockReaddirSync.mockReturnValue([
761
+ '.credentials.json',
762
+ '.credentials.json.dev1.1',
763
+ '.credentials.json.dev2.2',
764
+ ]);
765
+
766
+ const credentialContents: Record<string, string> = {
767
+ [path.join(claudeDir, '.credentials.json.dev1.1')]: JSON.stringify({
768
+ claudeAiOauth: { accessToken: 'token-dev1' },
769
+ }),
770
+ [path.join(claudeDir, '.credentials.json.dev2.2')]: JSON.stringify({
771
+ claudeAiOauth: { accessToken: 'token-dev2' },
772
+ }),
773
+ };
774
+
775
+ mockReadFileSync.mockImplementation((filePath: string) => {
776
+ return credentialContents[filePath] || '';
777
+ });
778
+
779
+ mockFetch
780
+ .mockResolvedValueOnce({
781
+ ok: true,
782
+ json: jest.fn().mockResolvedValue({
783
+ error: 'Token expired',
784
+ }),
785
+ })
786
+ .mockResolvedValueOnce({
787
+ ok: true,
788
+ json: jest.fn().mockResolvedValue({
789
+ five_hour: { utilization: 30.0 },
790
+ }),
791
+ });
792
+
793
+ repository = new OauthAPIClaudeRepository();
794
+ const result = await repository.isClaudeAvailable(80);
795
+
796
+ expect(result).toBe(true);
797
+ expect(mockFetch).toHaveBeenCalledTimes(2);
798
+ expect(mockCopyFileSync).toHaveBeenCalledWith(
799
+ path.join(claudeDir, '.credentials.json.dev2.2'),
800
+ path.join(claudeDir, '.credentials.json'),
801
+ );
802
+ });
803
+
804
+ it('should skip credential files with invalid JSON format', async () => {
805
+ mockExistsSync.mockReturnValue(true);
806
+ mockReaddirSync.mockReturnValue([
807
+ '.credentials.json',
808
+ '.credentials.json.dev1.1',
809
+ '.credentials.json.dev2.2',
810
+ ]);
811
+
812
+ const credentialContents: Record<string, string> = {
813
+ [path.join(claudeDir, '.credentials.json.dev1.1')]: 'null',
814
+ [path.join(claudeDir, '.credentials.json.dev2.2')]: JSON.stringify({
815
+ claudeAiOauth: { accessToken: 'token-dev2' },
816
+ }),
817
+ };
818
+
819
+ mockReadFileSync.mockImplementation((filePath: string) => {
820
+ return credentialContents[filePath] || '';
821
+ });
822
+
823
+ mockFetch.mockResolvedValueOnce({
824
+ ok: true,
825
+ json: jest.fn().mockResolvedValue({
826
+ five_hour: { utilization: 30.0 },
827
+ }),
828
+ });
829
+
830
+ repository = new OauthAPIClaudeRepository();
831
+ const result = await repository.isClaudeAvailable(80);
832
+
833
+ expect(result).toBe(true);
834
+ expect(mockFetch).toHaveBeenCalledTimes(1);
835
+ expect(mockCopyFileSync).toHaveBeenCalledWith(
836
+ path.join(claudeDir, '.credentials.json.dev2.2'),
837
+ path.join(claudeDir, '.credentials.json'),
838
+ );
839
+ });
840
+ });
346
841
  });
@@ -10,6 +10,12 @@ type CredentialsFile = {
10
10
  };
11
11
  };
12
12
 
13
+ type CredentialInfo = {
14
+ name: string;
15
+ priority: number;
16
+ filePath: string;
17
+ };
18
+
13
19
  type UsageWindow = {
14
20
  utilization?: number;
15
21
  resets_at?: string;
@@ -33,15 +39,48 @@ const isUsageResponse = (value: unknown): value is UsageResponse => {
33
39
  return true;
34
40
  };
35
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
+
36
77
  export class OauthAPIClaudeRepository implements ClaudeRepository {
37
78
  private readonly credentialsPath: string;
79
+ private readonly claudeDir: string;
38
80
 
39
81
  constructor() {
40
- this.credentialsPath = path.join(
41
- os.homedir(),
42
- '.claude',
43
- '.credentials.json',
44
- );
82
+ this.claudeDir = path.join(os.homedir(), '.claude');
83
+ this.credentialsPath = path.join(this.claudeDir, '.credentials.json');
45
84
  }
46
85
 
47
86
  private getAccessToken(): string {
@@ -140,4 +179,101 @@ export class OauthAPIClaudeRepository implements ClaudeRepository {
140
179
 
141
180
  return usages;
142
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
+ }
143
279
  }
@@ -0,0 +1,143 @@
1
+ const mockUnref = jest.fn();
2
+ const mockOn = jest.fn();
3
+ const mockSpawn = jest.fn(() => ({ on: mockOn, unref: mockUnref }));
4
+
5
+ jest.mock('child_process', () => ({
6
+ spawn: mockSpawn,
7
+ }));
8
+
9
+ import { Xfce4TerminalCopilotRepository } from './Xfce4TerminalCopilotRepository';
10
+
11
+ describe('Xfce4TerminalCopilotRepository', () => {
12
+ let repository: Xfce4TerminalCopilotRepository;
13
+
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ repository = new Xfce4TerminalCopilotRepository();
17
+ });
18
+
19
+ describe('run', () => {
20
+ it('should spawn xfce4-terminal with correct arguments', () => {
21
+ repository.run('test prompt', 'gpt-5-mini', 'test-process');
22
+
23
+ expect(mockSpawn).toHaveBeenCalledWith(
24
+ 'xfce4-terminal',
25
+ [
26
+ '-T',
27
+ 'gh-issue-preparator: test-process',
28
+ '-e',
29
+ "bash -c 'copilot --model gpt-5-mini --allow-all-tools -p '\\''test prompt'\\'''",
30
+ ],
31
+ {
32
+ detached: true,
33
+ stdio: 'ignore',
34
+ },
35
+ );
36
+ expect(mockOn).toHaveBeenCalledWith('error', expect.any(Function));
37
+ expect(mockUnref).toHaveBeenCalled();
38
+ });
39
+
40
+ it('should escape single quotes in prompt to prevent command injection', () => {
41
+ repository.run(
42
+ "prompt with 'single quotes'",
43
+ 'gpt-5-mini',
44
+ 'test-process',
45
+ );
46
+
47
+ expect(mockSpawn).toHaveBeenCalledWith(
48
+ 'xfce4-terminal',
49
+ [
50
+ '-T',
51
+ 'gh-issue-preparator: test-process',
52
+ '-e',
53
+ "bash -c 'copilot --model gpt-5-mini --allow-all-tools -p '\\''prompt with '\\''\\'\\'''\\''single quotes'\\''\\'\\'''\\'''\\'''",
54
+ ],
55
+ {
56
+ detached: true,
57
+ stdio: 'ignore',
58
+ },
59
+ );
60
+ });
61
+
62
+ it('should escape backticks in prompt to prevent command injection', () => {
63
+ repository.run('prompt with `backticks`', 'gpt-5-mini', 'test-process');
64
+
65
+ expect(mockSpawn).toHaveBeenCalledWith(
66
+ 'xfce4-terminal',
67
+ [
68
+ '-T',
69
+ 'gh-issue-preparator: test-process',
70
+ '-e',
71
+ "bash -c 'copilot --model gpt-5-mini --allow-all-tools -p '\\''prompt with `backticks`'\\'''",
72
+ ],
73
+ {
74
+ detached: true,
75
+ stdio: 'ignore',
76
+ },
77
+ );
78
+ });
79
+
80
+ it('should escape command substitution in prompt to prevent injection', () => {
81
+ repository.run('prompt with $(whoami)', 'gpt-5-mini', 'test-process');
82
+
83
+ expect(mockSpawn).toHaveBeenCalledWith(
84
+ 'xfce4-terminal',
85
+ [
86
+ '-T',
87
+ 'gh-issue-preparator: test-process',
88
+ '-e',
89
+ "bash -c 'copilot --model gpt-5-mini --allow-all-tools -p '\\''prompt with $(whoami)'\\'''",
90
+ ],
91
+ {
92
+ detached: true,
93
+ stdio: 'ignore',
94
+ },
95
+ );
96
+ });
97
+
98
+ it('should escape single quotes in processTitle', () => {
99
+ repository.run('prompt', 'gpt-5-mini', "title with 'quotes'");
100
+
101
+ expect(mockSpawn).toHaveBeenCalledWith(
102
+ 'xfce4-terminal',
103
+ [
104
+ '-T',
105
+ "gh-issue-preparator: title with '\\''quotes'\\''",
106
+ '-e',
107
+ "bash -c 'copilot --model gpt-5-mini --allow-all-tools -p '\\''prompt'\\'''",
108
+ ],
109
+ {
110
+ detached: true,
111
+ stdio: 'ignore',
112
+ },
113
+ );
114
+ });
115
+
116
+ it('should set correct window title', () => {
117
+ repository.run('prompt', 'gpt-5-mini', 'my-custom-title');
118
+
119
+ expect(mockSpawn).toHaveBeenCalledWith(
120
+ 'xfce4-terminal',
121
+ expect.arrayContaining(['-T', 'gh-issue-preparator: my-custom-title']),
122
+ expect.any(Object),
123
+ );
124
+ });
125
+
126
+ it('should register error handler and silently ignore errors', () => {
127
+ let capturedHandler: ((error: Error) => void) | undefined;
128
+ mockOn.mockImplementation(
129
+ (event: string, handler: (error: Error) => void) => {
130
+ if (event === 'error') {
131
+ capturedHandler = handler;
132
+ }
133
+ },
134
+ );
135
+
136
+ repository.run('test', 'gpt-5-mini', 'test');
137
+
138
+ expect(mockOn).toHaveBeenCalledWith('error', expect.any(Function));
139
+ expect(capturedHandler).toBeDefined();
140
+ expect(() => capturedHandler?.(new Error('spawn failed'))).not.toThrow();
141
+ });
142
+ });
143
+ });
@@ -0,0 +1,32 @@
1
+ import { CopilotRepository } from '../../domain/usecases/adapter-interfaces/CopilotRepository';
2
+ import { spawn } from 'child_process';
3
+
4
+ export class Xfce4TerminalCopilotRepository implements CopilotRepository {
5
+ private escapeForSingleQuotes(str: string): string {
6
+ return str.replace(/'/g, "'\\''");
7
+ }
8
+
9
+ run(prompt: string, model: 'gpt-5-mini', processTitle: string): void {
10
+ const escapedPrompt = this.escapeForSingleQuotes(prompt);
11
+ const escapedTitle = this.escapeForSingleQuotes(processTitle);
12
+ const innerCommand = `copilot --model ${model} --allow-all-tools -p '${escapedPrompt}'`;
13
+ const escapedInnerCommand = this.escapeForSingleQuotes(innerCommand);
14
+ const title = `gh-issue-preparator: ${escapedTitle}`;
15
+
16
+ const child = spawn(
17
+ 'xfce4-terminal',
18
+ ['-T', title, '-e', `bash -c '${escapedInnerCommand}'`],
19
+ {
20
+ detached: true,
21
+ stdio: 'ignore',
22
+ },
23
+ );
24
+
25
+ child.on('error', () => {
26
+ // Intentionally empty - fire-and-forget behavior
27
+ // Errors are silently ignored as this is a background terminal spawn
28
+ });
29
+
30
+ child.unref();
31
+ }
32
+ }
@@ -2,4 +2,5 @@ import { ClaudeWindowUsage } from '../../entities/ClaudeWindowUsage';
2
2
 
3
3
  export interface ClaudeRepository {
4
4
  getUsage(): Promise<ClaudeWindowUsage[]>;
5
+ isClaudeAvailable(threshold: number): Promise<boolean>;
5
6
  }
@@ -0,0 +1,3 @@
1
+ export interface CopilotRepository {
2
+ run(prompt: string, model: 'gpt-5-mini', processTitle: string): void;
3
+ }
@@ -2,8 +2,12 @@ import { ClaudeRepository } from '../../domain/usecases/adapter-interfaces/Claud
2
2
  import { ClaudeWindowUsage } from '../../domain/entities/ClaudeWindowUsage';
3
3
  export declare class OauthAPIClaudeRepository implements ClaudeRepository {
4
4
  private readonly credentialsPath;
5
+ private readonly claudeDir;
5
6
  constructor();
6
7
  private getAccessToken;
7
8
  getUsage(): Promise<ClaudeWindowUsage[]>;
9
+ private getUsageWithToken;
10
+ private isUsageUnderThreshold;
11
+ isClaudeAvailable(threshold: number): Promise<boolean>;
8
12
  }
9
13
  //# sourceMappingURL=OauthAPIClaudeRepository.d.ts.map
@@ -1 +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"}
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
+ import { CopilotRepository } from '../../domain/usecases/adapter-interfaces/CopilotRepository';
2
+ export declare class Xfce4TerminalCopilotRepository implements CopilotRepository {
3
+ private escapeForSingleQuotes;
4
+ run(prompt: string, model: 'gpt-5-mini', processTitle: string): void;
5
+ }
6
+ //# sourceMappingURL=Xfce4TerminalCopilotRepository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Xfce4TerminalCopilotRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/Xfce4TerminalCopilotRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAG/F,qBAAa,8BAA+B,YAAW,iBAAiB;IACtE,OAAO,CAAC,qBAAqB;IAI7B,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI;CAuBrE"}
@@ -1,5 +1,6 @@
1
1
  import { ClaudeWindowUsage } from '../../entities/ClaudeWindowUsage';
2
2
  export interface ClaudeRepository {
3
3
  getUsage(): Promise<ClaudeWindowUsage[]>;
4
+ isClaudeAvailable(threshold: number): Promise<boolean>;
4
5
  }
5
6
  //# sourceMappingURL=ClaudeRepository.d.ts.map
@@ -1 +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"}
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"}
@@ -0,0 +1,4 @@
1
+ export interface CopilotRepository {
2
+ run(prompt: string, model: 'gpt-5-mini', processTitle: string): void;
3
+ }
4
+ //# sourceMappingURL=CopilotRepository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CopilotRepository.d.ts","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/CopilotRepository.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CACtE"}