deepdebug-local-agent 0.3.1 → 0.3.2

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.
@@ -1,4 +1,5 @@
1
1
  import { GitHubProvider } from './github-provider.js';
2
+ import { BitbucketProvider } from './bitbucket-provider.js';
2
3
 
3
4
  /**
4
5
  * GitProviderRegistry
@@ -15,10 +16,10 @@ export class GitProviderRegistry {
15
16
  registerDefaults() {
16
17
  // Registrar providers disponíveis
17
18
  this.register('github', GitHubProvider);
19
+ this.register('bitbucket', BitbucketProvider);
18
20
 
19
- // GitLab e Bitbucket serão implementados depois
21
+ // GitLab será implementado depois
20
22
  // this.register('gitlab', GitLabProvider);
21
- // this.register('bitbucket', BitbucketProvider);
22
23
  }
23
24
 
24
25
  /**
@@ -28,13 +29,13 @@ export class GitProviderRegistry {
28
29
  */
29
30
  register(id, ProviderClass) {
30
31
  this.providers.set(id, ProviderClass);
31
- console.log(`📦 Registered git provider: ${id}`);
32
+ console.log(`📦 Registered git provider: ${id}`);
32
33
  }
33
34
 
34
35
  /**
35
- * Cria instância de um provider
36
+ * Cria instância de um provider
36
37
  * @param {string} id - ID do provider
37
- * @param {object} config - Configuração do provider
38
+ * @param {object} config - Configuração do provider
38
39
  * @returns {BaseGitProvider}
39
40
  */
40
41
  create(id, config = {}) {
@@ -48,7 +49,7 @@ export class GitProviderRegistry {
48
49
  }
49
50
 
50
51
  /**
51
- * Lista IDs dos providers disponíveis
52
+ * Lista IDs dos providers disponíveis
52
53
  * @returns {string[]}
53
54
  */
54
55
  listProviders() {
@@ -57,7 +58,7 @@ export class GitProviderRegistry {
57
58
 
58
59
  /**
59
60
  * Detecta provider a partir da URL
60
- * @param {string} url - URL do repositório
61
+ * @param {string} url - URL do repositório
61
62
  * @returns {string|null} - ID do provider
62
63
  */
63
64
  detectProviderFromUrl(url) {
@@ -74,9 +75,9 @@ export class GitProviderRegistry {
74
75
  }
75
76
 
76
77
  /**
77
- * Cria provider a partir da URL do repositório
78
- * @param {string} url - URL do repositório
79
- * @param {object} config - Configuração
78
+ * Cria provider a partir da URL do repositório
79
+ * @param {string} url - URL do repositório
80
+ * @param {object} config - Configuração
80
81
  * @returns {BaseGitProvider}
81
82
  */
82
83
  createFromUrl(url, config = {}) {
@@ -97,7 +98,7 @@ export class GitProviderRegistry {
97
98
  let instance = null;
98
99
 
99
100
  /**
100
- * Obtém instância singleton do GitProviderRegistry
101
+ * Obtém instância singleton do GitProviderRegistry
101
102
  * @returns {GitProviderRegistry}
102
103
  */
103
104
  export function getGitProviderRegistry() {
@@ -4,7 +4,7 @@ import { run } from '../utils/exec-utils.js';
4
4
  /**
5
5
  * GitHubProvider
6
6
  *
7
- * Implementação do Git provider para GitHub.
7
+ * Implementação do Git provider para GitHub.
8
8
  */
9
9
  export class GitHubProvider extends BaseGitProvider {
10
10
 
@@ -170,7 +170,7 @@ export class GitHubProvider extends BaseGitProvider {
170
170
 
171
171
  const cloneUrl = this.getCloneUrl(owner, repo, true);
172
172
 
173
- // Inserir token na URL para autenticação
173
+ // Inserir token na URL para autenticação
174
174
  const authedUrl = cloneUrl.replace(
175
175
  'https://',
176
176
  `https://${this.accessToken}@`
@@ -216,12 +216,12 @@ export class GitHubProvider extends BaseGitProvider {
216
216
  let sha = fromRef;
217
217
 
218
218
  if (!fromRef.match(/^[a-f0-9]{40}$/)) {
219
- // É um nome de branch, precisamos do SHA
219
+ // É um nome de branch, precisamos do SHA
220
220
  const branch = await this.getBranch(owner, repo, fromRef);
221
221
  sha = branch.sha;
222
222
  }
223
223
 
224
- // Criar a referência
224
+ // Criar a referência
225
225
  const data = await this.apiRequest(`/repos/${owner}/${repo}/git/refs`, {
226
226
  method: 'POST',
227
227
  body: JSON.stringify({
@@ -268,7 +268,7 @@ export class GitHubProvider extends BaseGitProvider {
268
268
  }
269
269
 
270
270
  async createCommit(owner, repo, branch, message, changes) {
271
- // 1. Obter referência atual
271
+ // 1. Obter referência atual
272
272
  const branchData = await this.getBranch(owner, repo, branch);
273
273
  const baseSha = branchData.sha;
274
274
 
@@ -327,7 +327,7 @@ export class GitHubProvider extends BaseGitProvider {
327
327
  })
328
328
  });
329
329
 
330
- // 6. Atualizar referência da branch
330
+ // 6. Atualizar referência da branch
331
331
  await this.apiRequest(`/repos/${owner}/${repo}/git/refs/heads/${branch}`, {
332
332
  method: 'PATCH',
333
333
  body: JSON.stringify({
@@ -442,6 +442,299 @@ export class GitHubProvider extends BaseGitProvider {
442
442
  };
443
443
  }
444
444
 
445
+ // ==========================================
446
+ // PULL REQUEST COMMENTS / REVIEWS
447
+ // ==========================================
448
+
449
+ /**
450
+ * List general (issue-level) comments on a PR.
451
+ * GitHub treats PRs as issues, so general comments use the issues API.
452
+ */
453
+ async listPRComments(owner, repo, prNumber, options = {}) {
454
+ const { page = 1, perPage = 100 } = options;
455
+
456
+ const comments = await this.apiRequest(
457
+ `/repos/${owner}/${repo}/issues/${prNumber}/comments?page=${page}&per_page=${perPage}`
458
+ );
459
+
460
+ return comments.map(c => ({
461
+ id: c.id,
462
+ body: c.body,
463
+ authorUsername: c.user?.login,
464
+ authorName: c.user?.login,
465
+ createdAt: c.created_at,
466
+ updatedAt: c.updated_at,
467
+ htmlUrl: c.html_url,
468
+ resolved: undefined, // General comments don't have resolved state in GitHub
469
+ type: 'general'
470
+ }));
471
+ }
472
+
473
+ /**
474
+ * List review comments (inline code comments) on a PR.
475
+ * These are comments attached to specific lines in the diff.
476
+ */
477
+ async listPRReviewComments(owner, repo, prNumber, options = {}) {
478
+ const { page = 1, perPage = 100 } = options;
479
+
480
+ const comments = await this.apiRequest(
481
+ `/repos/${owner}/${repo}/pulls/${prNumber}/comments?page=${page}&per_page=${perPage}`
482
+ );
483
+
484
+ return comments.map(c => ({
485
+ id: c.id,
486
+ body: c.body,
487
+ path: c.path,
488
+ line: c.line || c.original_line,
489
+ side: c.side,
490
+ commitId: c.commit_id,
491
+ authorUsername: c.user?.login,
492
+ authorName: c.user?.login,
493
+ createdAt: c.created_at,
494
+ updatedAt: c.updated_at,
495
+ htmlUrl: c.html_url,
496
+ resolved: c.resolved !== undefined ? c.resolved : undefined,
497
+ threadId: c.pull_request_review_id,
498
+ inReplyToId: c.in_reply_to_id || null,
499
+ type: 'review'
500
+ }));
501
+ }
502
+
503
+ /**
504
+ * Get a specific review comment by ID
505
+ */
506
+ async getPRComment(owner, repo, prNumber, commentId) {
507
+ // Try review comment first, then general
508
+ try {
509
+ const c = await this.apiRequest(`/repos/${owner}/${repo}/pulls/comments/${commentId}`);
510
+ return {
511
+ id: c.id,
512
+ body: c.body,
513
+ path: c.path,
514
+ line: c.line || c.original_line,
515
+ side: c.side,
516
+ commitId: c.commit_id,
517
+ authorUsername: c.user?.login,
518
+ authorName: c.user?.login,
519
+ createdAt: c.created_at,
520
+ updatedAt: c.updated_at,
521
+ htmlUrl: c.html_url,
522
+ resolved: c.resolved !== undefined ? c.resolved : undefined,
523
+ threadId: c.pull_request_review_id,
524
+ inReplyToId: c.in_reply_to_id || null,
525
+ type: 'review'
526
+ };
527
+ } catch {
528
+ // Fall back to general comment (issues API)
529
+ const c = await this.apiRequest(`/repos/${owner}/${repo}/issues/comments/${commentId}`);
530
+ return {
531
+ id: c.id,
532
+ body: c.body,
533
+ authorUsername: c.user?.login,
534
+ authorName: c.user?.login,
535
+ createdAt: c.created_at,
536
+ updatedAt: c.updated_at,
537
+ htmlUrl: c.html_url,
538
+ resolved: undefined,
539
+ type: 'general'
540
+ };
541
+ }
542
+ }
543
+
544
+ /**
545
+ * Add a general comment to a PR
546
+ */
547
+ async addPRComment(owner, repo, prNumber, body) {
548
+ const data = await this.apiRequest(`/repos/${owner}/${repo}/issues/${prNumber}/comments`, {
549
+ method: 'POST',
550
+ body: JSON.stringify({ body })
551
+ });
552
+
553
+ return {
554
+ id: data.id,
555
+ body: data.body,
556
+ authorUsername: data.user?.login,
557
+ authorName: data.user?.login,
558
+ createdAt: data.created_at,
559
+ updatedAt: data.updated_at,
560
+ htmlUrl: data.html_url,
561
+ type: 'general'
562
+ };
563
+ }
564
+
565
+ /**
566
+ * Add an inline review comment on a specific line of code.
567
+ *
568
+ * @param {object} comment - { body, path, line, side, commitId }
569
+ * - path: file path relative to repo root
570
+ * - line: line number in the diff
571
+ * - side: 'LEFT' (old) or 'RIGHT' (new) - default 'RIGHT'
572
+ * - commitId: the HEAD commit SHA of the PR
573
+ */
574
+ async addPRReviewComment(owner, repo, prNumber, comment) {
575
+ const requestBody = {
576
+ body: comment.body,
577
+ path: comment.path,
578
+ commit_id: comment.commitId,
579
+ side: comment.side || 'RIGHT'
580
+ };
581
+
582
+ // GitHub uses 'line' for single-line or 'start_line' + 'line' for multi-line
583
+ if (comment.line) {
584
+ requestBody.line = comment.line;
585
+ }
586
+
587
+ // If replying to an existing thread, use in_reply_to
588
+ if (comment.inReplyToId) {
589
+ requestBody.in_reply_to = comment.inReplyToId;
590
+ }
591
+
592
+ const data = await this.apiRequest(`/repos/${owner}/${repo}/pulls/${prNumber}/comments`, {
593
+ method: 'POST',
594
+ body: JSON.stringify(requestBody)
595
+ });
596
+
597
+ return {
598
+ id: data.id,
599
+ body: data.body,
600
+ path: data.path,
601
+ line: data.line || data.original_line,
602
+ side: data.side,
603
+ commitId: data.commit_id,
604
+ authorUsername: data.user?.login,
605
+ authorName: data.user?.login,
606
+ createdAt: data.created_at,
607
+ updatedAt: data.updated_at,
608
+ htmlUrl: data.html_url,
609
+ threadId: data.pull_request_review_id,
610
+ inReplyToId: data.in_reply_to_id || null,
611
+ type: 'review'
612
+ };
613
+ }
614
+
615
+ /**
616
+ * Reply to an existing review comment thread.
617
+ * GitHub uses the pulls/comments endpoint with in_reply_to.
618
+ */
619
+ async replyToPRComment(owner, repo, prNumber, commentId, body) {
620
+ // For review comments, use the reply endpoint
621
+ const data = await this.apiRequest(
622
+ `/repos/${owner}/${repo}/pulls/${prNumber}/comments/${commentId}/replies`, {
623
+ method: 'POST',
624
+ body: JSON.stringify({ body })
625
+ }
626
+ );
627
+
628
+ return {
629
+ id: data.id,
630
+ body: data.body,
631
+ path: data.path,
632
+ line: data.line || data.original_line,
633
+ authorUsername: data.user?.login,
634
+ authorName: data.user?.login,
635
+ createdAt: data.created_at,
636
+ updatedAt: data.updated_at,
637
+ htmlUrl: data.html_url,
638
+ inReplyToId: commentId,
639
+ type: 'review'
640
+ };
641
+ }
642
+
643
+ /**
644
+ * Resolve a review comment thread in GitHub.
645
+ *
646
+ * GitHub doesn't have a direct "resolve comment" API.
647
+ * Resolution is done by submitting a review that resolves the thread.
648
+ * We use the GraphQL API for thread resolution.
649
+ *
650
+ * Fallback: If GraphQL is not available, we reply with a "Resolved" message.
651
+ */
652
+ async resolvePRComment(owner, repo, prNumber, commentId) {
653
+ try {
654
+ // Try GraphQL approach (thread resolution)
655
+ const query = `
656
+ mutation($threadId: ID!) {
657
+ resolveReviewThread(input: { threadId: $threadId }) {
658
+ thread { isResolved }
659
+ }
660
+ }
661
+ `;
662
+
663
+ await fetch('https://api.github.com/graphql', {
664
+ method: 'POST',
665
+ headers: {
666
+ 'Authorization': `Bearer ${this.accessToken}`,
667
+ 'Content-Type': 'application/json'
668
+ },
669
+ body: JSON.stringify({
670
+ query,
671
+ variables: { threadId: String(commentId) }
672
+ })
673
+ });
674
+
675
+ return { resolved: true };
676
+ } catch {
677
+ // Fallback: add a reply indicating resolution
678
+ await this.replyToPRComment(owner, repo, prNumber, commentId,
679
+ '✅ Resolved by InspTech AI — fix has been applied.');
680
+ return { resolved: true };
681
+ }
682
+ }
683
+
684
+ /**
685
+ * Unresolve a review comment thread in GitHub via GraphQL.
686
+ */
687
+ async unresolvePRComment(owner, repo, prNumber, commentId) {
688
+ try {
689
+ const query = `
690
+ mutation($threadId: ID!) {
691
+ unresolveReviewThread(input: { threadId: $threadId }) {
692
+ thread { isResolved }
693
+ }
694
+ }
695
+ `;
696
+
697
+ await fetch('https://api.github.com/graphql', {
698
+ method: 'POST',
699
+ headers: {
700
+ 'Authorization': `Bearer ${this.accessToken}`,
701
+ 'Content-Type': 'application/json'
702
+ },
703
+ body: JSON.stringify({
704
+ query,
705
+ variables: { threadId: String(commentId) }
706
+ })
707
+ });
708
+
709
+ return { resolved: false };
710
+ } catch {
711
+ return { resolved: false };
712
+ }
713
+ }
714
+
715
+ /**
716
+ * List only unresolved review comments.
717
+ * Uses the reviews API to find pending threads.
718
+ */
719
+ async listUnresolvedPRComments(owner, repo, prNumber) {
720
+ const reviewComments = await this.listPRReviewComments(owner, repo, prNumber);
721
+
722
+ // Group by thread (in_reply_to chain) and filter unresolved
723
+ const threads = new Map();
724
+ for (const c of reviewComments) {
725
+ const threadKey = c.inReplyToId || c.id;
726
+ if (!threads.has(threadKey)) {
727
+ threads.set(threadKey, []);
728
+ }
729
+ threads.get(threadKey).push(c);
730
+ }
731
+
732
+ // Return root comments of unresolved threads
733
+ return reviewComments.filter(c =>
734
+ !c.inReplyToId && (c.resolved === false || c.resolved === undefined)
735
+ );
736
+ }
737
+
445
738
  // ==========================================
446
739
  // WEBHOOKS
447
740
  // ==========================================