codeant-cli 0.3.3 → 0.3.4

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 (74) hide show
  1. package/package.json +6 -9
  2. package/src/commands/getBaseUrl.js +21 -0
  3. package/src/commands/login.js +120 -0
  4. package/src/commands/logout.js +30 -0
  5. package/src/commands/review.js +294 -0
  6. package/src/commands/secrets.js +148 -0
  7. package/src/commands/setBaseUrl.js +37 -0
  8. package/src/components/ReviewUI.js +410 -0
  9. package/src/components/SecretsUI.js +366 -0
  10. package/src/components/Welcome.js +144 -0
  11. package/src/index.js +390 -0
  12. package/src/reviewHeadless.js +198 -0
  13. package/src/scm/azure/client.js +21 -0
  14. package/src/scm/azure/getCodeReview.js +20 -0
  15. package/src/scm/azure/getPullRequest.js +30 -0
  16. package/src/scm/azure/index.js +7 -0
  17. package/src/scm/azure/listCodeReviews.js +17 -0
  18. package/src/scm/azure/listPullRequestComments.js +41 -0
  19. package/src/scm/azure/listPullRequests.js +26 -0
  20. package/src/scm/azure/resolveConversation.js +22 -0
  21. package/src/scm/azure/searchComments.js +35 -0
  22. package/src/scm/bitbucket/client.js +18 -0
  23. package/src/scm/bitbucket/getCodeReview.js +3 -0
  24. package/src/scm/bitbucket/getPullRequest.js +26 -0
  25. package/src/scm/bitbucket/index.js +7 -0
  26. package/src/scm/bitbucket/listCodeReviews.js +17 -0
  27. package/src/scm/bitbucket/listPullRequestComments.js +33 -0
  28. package/src/scm/bitbucket/listPullRequests.js +33 -0
  29. package/src/scm/bitbucket/resolveConversation.js +21 -0
  30. package/src/scm/bitbucket/searchComments.js +29 -0
  31. package/src/scm/github/client.js +17 -0
  32. package/src/scm/github/getCodeReview.js +26 -0
  33. package/src/scm/github/getPullRequest.js +27 -0
  34. package/src/scm/github/index.js +7 -0
  35. package/src/scm/github/listCodeReviews.js +17 -0
  36. package/src/scm/github/listPullRequestComments.js +51 -0
  37. package/src/scm/github/listPullRequests.js +32 -0
  38. package/src/scm/github/resolveConversation.js +85 -0
  39. package/src/scm/github/searchComments.js +30 -0
  40. package/src/scm/gitlab/client.js +16 -0
  41. package/src/scm/gitlab/getCodeReview.js +18 -0
  42. package/src/scm/gitlab/getPullRequest.js +24 -0
  43. package/src/scm/gitlab/index.js +7 -0
  44. package/src/scm/gitlab/listCodeReviews.js +21 -0
  45. package/src/scm/gitlab/listPullRequestComments.js +44 -0
  46. package/src/scm/gitlab/listPullRequests.js +26 -0
  47. package/src/scm/gitlab/resolveConversation.js +14 -0
  48. package/src/scm/gitlab/searchComments.js +28 -0
  49. package/src/scm/index.js +97 -0
  50. package/src/tools/executeTool.js +31 -0
  51. package/src/tools/globTool.js +9 -0
  52. package/src/tools/grepTool.js +18 -0
  53. package/src/tools/lsTool.js +7 -0
  54. package/src/tools/readTool.js +12 -0
  55. package/src/utils/analytics.js +56 -0
  56. package/src/utils/baseUrl.js +11 -0
  57. package/src/utils/commonApiHelper.js +143 -0
  58. package/src/utils/config.js +42 -0
  59. package/src/utils/fetchApi.js +39 -0
  60. package/src/utils/gitDiffHelper.js +1203 -0
  61. package/src/utils/reviewApiHelper.js +291 -0
  62. package/src/utils/scmAuth.js +136 -0
  63. package/src/utils/secretsApiHelper.js +252 -0
  64. package/dist/178.index.js +0 -1
  65. package/dist/239.index.js +0 -1
  66. package/dist/701.index.js +0 -1
  67. package/dist/880.index.js +0 -1
  68. package/dist/935.index.js +0 -1
  69. package/dist/966.index.js +0 -1
  70. package/dist/index.js +0 -28
  71. package/dist/package.json +0 -1
  72. package/dist/review/239.index.js +0 -1
  73. package/dist/review/index.js +0 -8
  74. package/dist/review/package.json +0 -3
@@ -0,0 +1,17 @@
1
+ import { getConnection, splitRepo } from './client.js';
2
+
3
+ export async function listCodeReviews({ name, prNumber, limit = 20 }) {
4
+ const connection = getConnection();
5
+ const gitApi = await connection.getGitApi();
6
+ const { project, repo } = splitRepo(name);
7
+
8
+ const pr = await gitApi.getPullRequest(repo, prNumber, project);
9
+
10
+ return (pr.reviewers || []).slice(0, limit).map(r => ({
11
+ id: r.id,
12
+ user: r.displayName || r.uniqueName,
13
+ state: r.vote > 0 ? 'APPROVED' : r.vote < 0 ? 'REJECTED' : 'PENDING',
14
+ body: null,
15
+ submittedAt: null,
16
+ }));
17
+ }
@@ -0,0 +1,41 @@
1
+ import { getConnection, splitRepo } from './client.js';
2
+
3
+ export async function listPullRequestComments({ name, prNumber, codeantGenerated, createdAfter, createdBefore }) {
4
+ const connection = getConnection();
5
+ const gitApi = await connection.getGitApi();
6
+ const { project, repo } = splitRepo(name);
7
+
8
+ const threads = await gitApi.getThreads(repo, prNumber, project);
9
+
10
+ let comments = [];
11
+ for (const thread of (threads || [])) {
12
+ for (const comment of (thread.comments || [])) {
13
+ if (comment.commentType === 'system') continue;
14
+ comments.push({
15
+ id: comment.id,
16
+ threadId: thread.id,
17
+ type: thread.threadContext?.filePath ? 'review' : 'issue',
18
+ author: comment.author?.displayName || comment.author?.uniqueName,
19
+ body: comment.content,
20
+ path: thread.threadContext?.filePath || null,
21
+ line: thread.threadContext?.rightFileStart?.line || null,
22
+ createdAt: comment.publishedDate,
23
+ updatedAt: comment.lastUpdatedDate,
24
+ isCodeantComment: (comment.author?.displayName || '').toLowerCase().includes('codeant') || comment.content?.includes('Suggestion'),
25
+ resolved: thread.status === 2, // Fixed
26
+ });
27
+ }
28
+ }
29
+
30
+ if (codeantGenerated !== undefined) {
31
+ comments = comments.filter(c => c.isCodeantComment === codeantGenerated);
32
+ }
33
+ if (createdAfter) {
34
+ comments = comments.filter(c => new Date(c.createdAt) >= new Date(createdAfter));
35
+ }
36
+ if (createdBefore) {
37
+ comments = comments.filter(c => new Date(c.createdAt) <= new Date(createdBefore));
38
+ }
39
+
40
+ return comments;
41
+ }
@@ -0,0 +1,26 @@
1
+ import { getConnection, splitRepo } from './client.js';
2
+
3
+ export async function listPullRequests({ name, sourceBranch, authorLogin, state = 'open', limit = 20 }) {
4
+ const connection = getConnection();
5
+ const gitApi = await connection.getGitApi();
6
+ const { project, repo } = splitRepo(name);
7
+
8
+ const statusMap = { open: 1 /* Active */, closed: 3 /* Completed */ };
9
+ const searchCriteria = { status: statusMap[state] || 1 };
10
+ if (sourceBranch) searchCriteria.sourceRefName = `refs/heads/${sourceBranch}`;
11
+ if (authorLogin) searchCriteria.creatorId = authorLogin;
12
+
13
+ const prs = await gitApi.getPullRequests(repo, searchCriteria, project, undefined, undefined, limit);
14
+
15
+ return (prs || []).map(pr => ({
16
+ number: pr.pullRequestId,
17
+ title: pr.title,
18
+ state: pr.status === 1 ? 'open' : pr.status === 3 ? 'closed' : 'unknown',
19
+ merged: pr.status === 3 && pr.closedBy != null,
20
+ author: pr.createdBy?.displayName || pr.createdBy?.uniqueName,
21
+ sourceBranch: pr.sourceRefName?.replace('refs/heads/', ''),
22
+ targetBranch: pr.targetRefName?.replace('refs/heads/', ''),
23
+ createdAt: pr.creationDate,
24
+ url: pr.url,
25
+ }));
26
+ }
@@ -0,0 +1,22 @@
1
+ import { getConnection, splitRepo } from './client.js';
2
+
3
+ /**
4
+ * Resolve a comment thread on an Azure DevOps pull request.
5
+ * Azure uses thread status: 1 = Active, 2 = Fixed (resolved), 3 = WontFix, 4 = Closed, 5 = ByDesign, 6 = Pending
6
+ */
7
+ export async function resolveConversation({ name, prNumber, threadId }) {
8
+ if (!threadId) throw new Error('threadId is required to resolve an Azure DevOps thread');
9
+
10
+ const parsedThreadId = Number(threadId);
11
+ if (!Number.isInteger(parsedThreadId) || parsedThreadId <= 0) {
12
+ throw new Error('threadId must be a positive integer for Azure DevOps');
13
+ }
14
+
15
+ const connection = getConnection();
16
+ const gitApi = await connection.getGitApi();
17
+ const { project, repo } = splitRepo(name);
18
+
19
+ await gitApi.updateThread({ status: 2 }, repo, prNumber, parsedThreadId, project);
20
+
21
+ return { success: true };
22
+ }
@@ -0,0 +1,35 @@
1
+ import { getConnection, splitRepo } from './client.js';
2
+
3
+ export async function searchComments({ name, query, limit = 10 }) {
4
+ const connection = getConnection();
5
+ const gitApi = await connection.getGitApi();
6
+ const { project, repo } = splitRepo(name);
7
+
8
+ const prs = await gitApi.getPullRequests(repo, { status: 1 }, project, undefined, undefined, 10);
9
+
10
+ let allComments = [];
11
+ for (const pr of (prs || [])) {
12
+ const threads = await gitApi.getThreads(repo, pr.pullRequestId, project);
13
+ for (const thread of (threads || [])) {
14
+ for (const comment of (thread.comments || [])) {
15
+ if (comment.commentType === 'system') continue;
16
+ allComments.push({
17
+ prNumber: pr.pullRequestId,
18
+ prTitle: pr.title,
19
+ id: comment.id,
20
+ author: comment.author?.displayName,
21
+ body: comment.content,
22
+ path: thread.threadContext?.filePath || null,
23
+ line: thread.threadContext?.rightFileStart?.line || null,
24
+ createdAt: comment.publishedDate,
25
+ isCodeantComment: (comment.author?.displayName || '').toLowerCase().includes('codeant'),
26
+ });
27
+ }
28
+ }
29
+ }
30
+
31
+ const q = query.toLowerCase();
32
+ allComments = allComments.filter(c => c.body?.toLowerCase().includes(q));
33
+
34
+ return allComments.slice(0, limit);
35
+ }
@@ -0,0 +1,18 @@
1
+ import pkg from 'bitbucket';
2
+ const { Bitbucket } = pkg;
3
+ import { getBitbucketToken, getBitbucketBaseUrl } from '../../utils/scmAuth.js';
4
+
5
+ export function getClient() {
6
+ const token = getBitbucketToken();
7
+ if (!token) throw new Error('Bitbucket token not found. Set BITBUCKET_TOKEN env var or run `codeant set-token bitbucket <token>`.');
8
+ const opts = { auth: { token } };
9
+ const baseUrl = getBitbucketBaseUrl();
10
+ if (baseUrl) opts.baseUrl = baseUrl.replace(/\/+$/, '');
11
+ return new Bitbucket(opts);
12
+ }
13
+
14
+ export function splitRepo(name) {
15
+ const [workspace, repo_slug] = name.split('/');
16
+ if (!workspace || !repo_slug) throw new Error(`Invalid repo name "${name}". Use workspace/repo format.`);
17
+ return { workspace, repo_slug };
18
+ }
@@ -0,0 +1,3 @@
1
+ export async function getCodeReview() {
2
+ throw new Error('Bitbucket does not support individual code review retrieval. Use `pr comments` instead.');
3
+ }
@@ -0,0 +1,26 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function getPullRequest({ name, prNumber }) {
4
+ const client = getClient();
5
+ const { workspace, repo_slug } = splitRepo(name);
6
+
7
+ const { data: pr } = await client.repositories.getPullRequest({ workspace, repo_slug, pull_request_id: prNumber });
8
+
9
+ return {
10
+ number: pr.id,
11
+ title: pr.title,
12
+ state: pr.state?.toLowerCase(),
13
+ merged: pr.state === 'MERGED',
14
+ author: pr.author?.display_name || pr.author?.nickname,
15
+ sourceBranch: pr.source?.branch?.name,
16
+ targetBranch: pr.destination?.branch?.name,
17
+ body: pr.description,
18
+ createdAt: pr.created_on,
19
+ updatedAt: pr.updated_on,
20
+ url: pr.links?.html?.href,
21
+ reviewSummary: pr.participants?.filter(p => p.role === 'REVIEWER').map(p => ({
22
+ user: p.user?.display_name || p.user?.nickname,
23
+ state: p.approved ? 'APPROVED' : 'PENDING',
24
+ })) || [],
25
+ };
26
+ }
@@ -0,0 +1,7 @@
1
+ export { listPullRequests } from './listPullRequests.js';
2
+ export { getPullRequest } from './getPullRequest.js';
3
+ export { listPullRequestComments } from './listPullRequestComments.js';
4
+ export { listCodeReviews } from './listCodeReviews.js';
5
+ export { getCodeReview } from './getCodeReview.js';
6
+ export { searchComments } from './searchComments.js';
7
+ export { resolveConversation } from './resolveConversation.js';
@@ -0,0 +1,17 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function listCodeReviews({ name, prNumber, limit = 20 }) {
4
+ // Bitbucket doesn't have discrete "reviews" — participants with reviewer role are the equivalent
5
+ const client = getClient();
6
+ const { workspace, repo_slug } = splitRepo(name);
7
+
8
+ const { data: pr } = await client.repositories.getPullRequest({ workspace, repo_slug, pull_request_id: prNumber });
9
+
10
+ return (pr.participants || []).filter(p => p.role === 'REVIEWER').slice(0, limit).map(p => ({
11
+ id: null,
12
+ user: p.user?.display_name || p.user?.nickname,
13
+ state: p.approved ? 'APPROVED' : 'PENDING',
14
+ body: null,
15
+ submittedAt: null,
16
+ }));
17
+ }
@@ -0,0 +1,33 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function listPullRequestComments({ name, prNumber, codeantGenerated, createdAfter, createdBefore }) {
4
+ const client = getClient();
5
+ const { workspace, repo_slug } = splitRepo(name);
6
+
7
+ const { data } = await client.repositories.listPullRequestComments({ workspace, repo_slug, pull_request_id: prNumber, pagelen: 100 });
8
+
9
+ let comments = (data.values || []).map(c => ({
10
+ id: c.id,
11
+ type: c.inline ? 'review' : 'issue',
12
+ author: c.user?.display_name || c.user?.nickname,
13
+ body: c.content?.raw || c.content?.markup || '',
14
+ path: c.inline?.path || null,
15
+ line: c.inline?.to || null,
16
+ createdAt: c.created_on,
17
+ updatedAt: c.updated_on,
18
+ isCodeantComment: (c.user?.display_name || c.user?.nickname || '').toLowerCase().includes('codeant') || (c.content?.raw || c.content?.markup || '').includes('Suggestion'),
19
+ resolved: c.resolved || false,
20
+ }));
21
+
22
+ if (codeantGenerated !== undefined) {
23
+ comments = comments.filter(c => c.isCodeantComment === codeantGenerated);
24
+ }
25
+ if (createdAfter) {
26
+ comments = comments.filter(c => new Date(c.createdAt) >= new Date(createdAfter));
27
+ }
28
+ if (createdBefore) {
29
+ comments = comments.filter(c => new Date(c.createdAt) <= new Date(createdBefore));
30
+ }
31
+
32
+ return comments;
33
+ }
@@ -0,0 +1,33 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function listPullRequests({ name, sourceBranch, authorLogin, state = 'open', limit = 20 }) {
4
+ const client = getClient();
5
+ const { workspace, repo_slug } = splitRepo(name);
6
+
7
+ const stateMap = { open: 'OPEN', closed: 'MERGED,DECLINED,SUPERSEDED' };
8
+ const { data } = await client.repositories.listPullRequests({ workspace, repo_slug, state: stateMap[state] || 'OPEN', pagelen: limit });
9
+
10
+ let prs = (data.values || []).map(pr => ({
11
+ number: pr.id,
12
+ title: pr.title,
13
+ state: pr.state?.toLowerCase(),
14
+ merged: pr.state === 'MERGED',
15
+ author: pr.author?.display_name || pr.author?.nickname,
16
+ sourceBranch: pr.source?.branch?.name,
17
+ targetBranch: pr.destination?.branch?.name,
18
+ createdAt: pr.created_on,
19
+ updatedAt: pr.updated_on,
20
+ url: pr.links?.html?.href,
21
+ }));
22
+
23
+ if (sourceBranch) {
24
+ const sb = sourceBranch.toLowerCase();
25
+ prs = prs.filter(pr => pr.sourceBranch?.toLowerCase().includes(sb));
26
+ }
27
+ if (authorLogin) {
28
+ const author = authorLogin.toLowerCase();
29
+ prs = prs.filter(pr => pr.author?.toLowerCase().includes(author));
30
+ }
31
+
32
+ return prs;
33
+ }
@@ -0,0 +1,21 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ /**
4
+ * Resolve a comment (conversation) on a Bitbucket pull request.
5
+ */
6
+ export async function resolveConversation({ name, prNumber, commentId }) {
7
+ if (!commentId) throw new Error('commentId is required to resolve a Bitbucket comment');
8
+
9
+ const client = getClient();
10
+ const { workspace, repo_slug } = splitRepo(name);
11
+
12
+ await client.repositories.updatePullRequestComment({
13
+ workspace,
14
+ repo_slug,
15
+ pull_request_id: prNumber,
16
+ comment_id: commentId,
17
+ _body: { resolved: true },
18
+ });
19
+
20
+ return { success: true };
21
+ }
@@ -0,0 +1,29 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function searchComments({ name, query, limit = 10 }) {
4
+ const client = getClient();
5
+ const { workspace, repo_slug } = splitRepo(name);
6
+
7
+ const { data: prsData } = await client.repositories.listPullRequests({ workspace, repo_slug, state: 'OPEN', pagelen: 10 });
8
+
9
+ let allComments = [];
10
+ for (const pr of (prsData.values || [])) {
11
+ const { data: commentsData } = await client.repositories.listPullRequestComments({ workspace, repo_slug, pull_request_id: pr.id, pagelen: 50 });
12
+ allComments.push(...(commentsData.values || []).map(c => ({
13
+ prNumber: pr.id,
14
+ prTitle: pr.title,
15
+ id: c.id,
16
+ author: c.user?.display_name || c.user?.nickname,
17
+ body: c.content?.raw || '',
18
+ path: c.inline?.path || null,
19
+ line: c.inline?.to || null,
20
+ createdAt: c.created_on,
21
+ isCodeantComment: (c.user?.display_name || '').toLowerCase().includes('codeant'),
22
+ })));
23
+ }
24
+
25
+ const q = query.toLowerCase();
26
+ allComments = allComments.filter(c => c.body?.toLowerCase().includes(q));
27
+
28
+ return allComments.slice(0, limit);
29
+ }
@@ -0,0 +1,17 @@
1
+ import { Octokit } from '@octokit/rest';
2
+ import { getGitHubToken, getGitHubBaseUrl } from '../../utils/scmAuth.js';
3
+
4
+ export function getClient() {
5
+ const token = getGitHubToken();
6
+ if (!token) throw new Error('GitHub token not found. Set GITHUB_TOKEN env var, login via `gh auth login`, or run `codeant set-token github <token>`.');
7
+ const opts = { auth: token };
8
+ const baseUrl = getGitHubBaseUrl();
9
+ if (baseUrl) opts.baseUrl = baseUrl.replace(/\/+$/, '') + '/api/v3';
10
+ return new Octokit(opts);
11
+ }
12
+
13
+ export function splitRepo(name) {
14
+ const [owner, repo] = name.split('/');
15
+ if (!owner || !repo) throw new Error(`Invalid repo name "${name}". Use owner/repo format.`);
16
+ return { owner, repo };
17
+ }
@@ -0,0 +1,26 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function getCodeReview({ name, prNumber, reviewId }) {
4
+ const octokit = getClient();
5
+ const { owner, repo } = splitRepo(name);
6
+
7
+ const [{ data: review }, { data: reviewComments }] = await Promise.all([
8
+ octokit.pulls.getReview({ owner, repo, pull_number: prNumber, review_id: reviewId }),
9
+ octokit.pulls.listCommentsForReview({ owner, repo, pull_number: prNumber, review_id: reviewId, per_page: 100 }),
10
+ ]);
11
+
12
+ return {
13
+ id: review.id,
14
+ user: review.user?.login,
15
+ state: review.state,
16
+ body: review.body,
17
+ submittedAt: review.submitted_at,
18
+ comments: reviewComments.map(c => ({
19
+ id: c.id,
20
+ path: c.path,
21
+ line: c.line || c.original_line,
22
+ body: c.body,
23
+ createdAt: c.created_at,
24
+ })),
25
+ };
26
+ }
@@ -0,0 +1,27 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function getPullRequest({ name, prNumber }) {
4
+ const octokit = getClient();
5
+ const { owner, repo } = splitRepo(name);
6
+
7
+ const { data: pr } = await octokit.pulls.get({ owner, repo, pull_number: prNumber });
8
+ const { data: reviews } = await octokit.pulls.listReviews({ owner, repo, pull_number: prNumber });
9
+
10
+ return {
11
+ number: pr.number,
12
+ title: pr.title,
13
+ state: pr.state,
14
+ merged: pr.merged_at !== null,
15
+ author: pr.user?.login,
16
+ sourceBranch: pr.head?.ref,
17
+ targetBranch: pr.base?.ref,
18
+ body: pr.body,
19
+ createdAt: pr.created_at,
20
+ updatedAt: pr.updated_at,
21
+ url: pr.html_url,
22
+ additions: pr.additions,
23
+ deletions: pr.deletions,
24
+ changedFiles: pr.changed_files,
25
+ reviewSummary: reviews.map(r => ({ user: r.user?.login, state: r.state, submittedAt: r.submitted_at })),
26
+ };
27
+ }
@@ -0,0 +1,7 @@
1
+ export { listPullRequests } from './listPullRequests.js';
2
+ export { getPullRequest } from './getPullRequest.js';
3
+ export { listPullRequestComments } from './listPullRequestComments.js';
4
+ export { listCodeReviews } from './listCodeReviews.js';
5
+ export { getCodeReview } from './getCodeReview.js';
6
+ export { searchComments } from './searchComments.js';
7
+ export { resolveConversation } from './resolveConversation.js';
@@ -0,0 +1,17 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function listCodeReviews({ name, prNumber, limit = 20, offset = 0 }) {
4
+ const octokit = getClient();
5
+ const { owner, repo } = splitRepo(name);
6
+
7
+ const { data } = await octokit.pulls.listReviews({ owner, repo, pull_number: prNumber, per_page: limit, page: Math.floor(offset / limit) + 1 });
8
+
9
+ return data.map(r => ({
10
+ id: r.id,
11
+ user: r.user?.login,
12
+ state: r.state,
13
+ body: r.body,
14
+ submittedAt: r.submitted_at,
15
+ url: r.html_url,
16
+ }));
17
+ }
@@ -0,0 +1,51 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function listPullRequestComments({ name, prNumber, codeantGenerated, addressed, createdAfter, createdBefore }) {
4
+ const octokit = getClient();
5
+ const { owner, repo } = splitRepo(name);
6
+
7
+ // Get both review comments (inline) and issue comments (general)
8
+ const [{ data: reviewComments }, { data: issueComments }] = await Promise.all([
9
+ octokit.pulls.listReviewComments({ owner, repo, pull_number: prNumber, per_page: 100 }),
10
+ octokit.issues.listComments({ owner, repo, issue_number: prNumber, per_page: 100 }),
11
+ ]);
12
+
13
+ let comments = [
14
+ ...reviewComments.map(c => ({
15
+ id: c.id,
16
+ type: 'review',
17
+ author: c.user?.login,
18
+ body: c.body,
19
+ path: c.path,
20
+ line: c.line || c.original_line,
21
+ createdAt: c.created_at,
22
+ updatedAt: c.updated_at,
23
+ isCodeantComment: c.user?.login?.includes('codeant') || c.body?.includes('Suggestion'),
24
+ inReplyToId: c.in_reply_to_id || null,
25
+ })),
26
+ ...issueComments.map(c => ({
27
+ id: c.id,
28
+ type: 'issue',
29
+ author: c.user?.login,
30
+ body: c.body,
31
+ path: null,
32
+ line: null,
33
+ createdAt: c.created_at,
34
+ updatedAt: c.updated_at,
35
+ isCodeantComment: c.user?.login?.includes('codeant') || c.body?.includes('Suggestion'),
36
+ inReplyToId: null,
37
+ })),
38
+ ];
39
+
40
+ if (codeantGenerated !== undefined) {
41
+ comments = comments.filter(c => c.isCodeantComment === codeantGenerated);
42
+ }
43
+ if (createdAfter) {
44
+ comments = comments.filter(c => new Date(c.createdAt) >= new Date(createdAfter));
45
+ }
46
+ if (createdBefore) {
47
+ comments = comments.filter(c => new Date(c.createdAt) <= new Date(createdBefore));
48
+ }
49
+
50
+ return comments;
51
+ }
@@ -0,0 +1,32 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function listPullRequests({ name, sourceBranch, authorLogin, state = 'open', limit = 20, offset = 0 }) {
4
+ const octokit = getClient();
5
+ const { owner, repo } = splitRepo(name);
6
+
7
+ const perPage = Math.max(limit, 1);
8
+ const params = { owner, repo, state: state === 'closed' ? 'closed' : 'open', per_page: perPage, page: Math.floor(offset / perPage) + 1, sort: 'updated', direction: 'desc' };
9
+ if (sourceBranch) params.head = sourceBranch.includes(':') ? sourceBranch : `${owner}:${sourceBranch}`;
10
+
11
+ const { data } = await octokit.pulls.list(params);
12
+
13
+ let prs = data.map(pr => ({
14
+ number: pr.number,
15
+ title: pr.title,
16
+ state: pr.state,
17
+ merged: pr.merged_at !== null,
18
+ author: pr.user?.login,
19
+ sourceBranch: pr.head?.ref,
20
+ targetBranch: pr.base?.ref,
21
+ createdAt: pr.created_at,
22
+ updatedAt: pr.updated_at,
23
+ url: pr.html_url,
24
+ }));
25
+
26
+ if (authorLogin) {
27
+ const author = authorLogin.toLowerCase();
28
+ prs = prs.filter(pr => pr.author?.toLowerCase().includes(author));
29
+ }
30
+
31
+ return prs;
32
+ }
@@ -0,0 +1,85 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ /**
4
+ * Resolve a review conversation on GitHub.
5
+ * GitHub doesn't have a native "resolve conversation" API for individual comments.
6
+ * We use the GraphQL API to minimize (hide) the comment, which is the closest equivalent.
7
+ * For review comments that are part of a PR review thread, we resolve via GraphQL resolveReviewThread.
8
+ */
9
+ export async function resolveConversation({ name, prNumber, commentId, threadId }) {
10
+ const octokit = getClient();
11
+ const { owner, repo } = splitRepo(name);
12
+
13
+ // If a threadId (graphql node_id of the review thread) is provided, resolve the thread
14
+ if (threadId) {
15
+ await octokit.graphql(
16
+ `mutation($threadId: ID!) {
17
+ resolveReviewThread(input: { threadId: $threadId }) {
18
+ thread { isResolved }
19
+ }
20
+ }`,
21
+ { threadId }
22
+ );
23
+ return { success: true };
24
+ }
25
+
26
+ // Otherwise, try to find the review comment and resolve its thread via node_id
27
+ if (commentId) {
28
+ // Fetch the review comment to get its node_id for the thread
29
+ const { data: comment } = await octokit.pulls.getReviewComment({
30
+ owner, repo, comment_id: commentId,
31
+ });
32
+
33
+ if (comment.node_id) {
34
+ // Attempt to resolve the thread this comment belongs to
35
+ // The comment's node_id is not the thread node_id, so we need to query for the thread
36
+ const { repository } = await octokit.graphql(
37
+ `query($owner: String!, $repo: String!, $prNumber: Int!) {
38
+ repository(owner: $owner, name: $repo) {
39
+ pullRequest(number: $prNumber) {
40
+ reviewThreads(first: 100) {
41
+ nodes {
42
+ id
43
+ isResolved
44
+ comments(first: 1) {
45
+ nodes { databaseId }
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }`,
52
+ { owner, repo, prNumber }
53
+ );
54
+
55
+ const threads = repository?.pullRequest?.reviewThreads?.nodes;
56
+ if (!threads) {
57
+ throw new Error(`Pull request ${prNumber} was not found or is not accessible`);
58
+ }
59
+
60
+ const thread = threads.find(
61
+ t => t.comments.nodes.some(c => c.databaseId === commentId)
62
+ );
63
+
64
+ if (thread && !thread.isResolved) {
65
+ await octokit.graphql(
66
+ `mutation($threadId: ID!) {
67
+ resolveReviewThread(input: { threadId: $threadId }) {
68
+ thread { isResolved }
69
+ }
70
+ }`,
71
+ { threadId: thread.id }
72
+ );
73
+ return { success: true };
74
+ }
75
+
76
+ if (thread?.isResolved) {
77
+ return { success: true, message: 'Thread already resolved' };
78
+ }
79
+ }
80
+
81
+ throw new Error(`Could not find a review thread for comment ${commentId}`);
82
+ }
83
+
84
+ throw new Error('Either commentId or threadId is required');
85
+ }
@@ -0,0 +1,30 @@
1
+ import { getClient, splitRepo } from './client.js';
2
+
3
+ export async function searchComments({ name, query, limit = 10, includeAddressed = false }) {
4
+ const octokit = getClient();
5
+ const { owner, repo } = splitRepo(name);
6
+
7
+ // GitHub doesn't have a native comment search API, so we fetch recent PR comments and filter
8
+ const { data: prs } = await octokit.pulls.list({ owner, repo, state: 'all', per_page: 10, sort: 'updated' });
9
+
10
+ let allComments = [];
11
+ for (const pr of prs) {
12
+ const { data: comments } = await octokit.pulls.listReviewComments({ owner, repo, pull_number: pr.number, per_page: 50 });
13
+ allComments.push(...comments.map(c => ({
14
+ prNumber: pr.number,
15
+ prTitle: pr.title,
16
+ id: c.id,
17
+ author: c.user?.login,
18
+ body: c.body,
19
+ path: c.path,
20
+ line: c.line || c.original_line,
21
+ createdAt: c.created_at,
22
+ isCodeantComment: c.user?.login?.includes('codeant'),
23
+ })));
24
+ }
25
+
26
+ const q = query.toLowerCase();
27
+ allComments = allComments.filter(c => c.body?.toLowerCase().includes(q));
28
+
29
+ return allComments.slice(0, limit);
30
+ }
@@ -0,0 +1,16 @@
1
+ import { Gitlab } from '@gitbeaker/rest';
2
+ import { getGitLabToken, getGitLabBaseUrl } from '../../utils/scmAuth.js';
3
+
4
+ export function getClient() {
5
+ const token = getGitLabToken();
6
+ if (!token) throw new Error('GitLab token not found. Set GITLAB_TOKEN env var, login via `glab auth login`, or run `codeant set-token gitlab <token>`.');
7
+ const opts = { token };
8
+ const host = getGitLabBaseUrl();
9
+ if (host) opts.host = host.replace(/\/+$/, '');
10
+ return new Gitlab(opts);
11
+ }
12
+
13
+ // GitLab uses project ID or "owner/repo" encoded as "owner%2Frepo"
14
+ export function projectId(name) {
15
+ return encodeURIComponent(name);
16
+ }